mirror of https://github.com/portainer/portainer
fix(notifications): cleanup notifications code [EE-4274] (#7790)
* fix(notifications): cleanup notifications code [EE-4274] * break long wordspull/7848/head
parent
c6ae8467c0
commit
724f1f63b7
|
@ -36,5 +36,8 @@ function config($stateRegistryProvider) {
|
||||||
component: 'notifications',
|
component: 'notifications',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
params: {
|
||||||
|
id: '',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
box-shadow: 0 6px 12px rgb(0 0 0 / 18%);
|
box-shadow: 0 6px 12px rgb(0 0 0 / 18%);
|
||||||
@apply th-dark:!border-none;
|
@apply th-dark:!border-none;
|
||||||
@apply th-highcontrast:!border-none;
|
@apply th-highcontrast:!border-none;
|
||||||
|
z-index: 9999;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu-link {
|
.menu-link {
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
.root {
|
||||||
|
width: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
.badge {
|
.badge {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 8px;
|
top: 8px;
|
||||||
|
@ -23,6 +27,16 @@
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notifications {
|
||||||
|
max-height: 80vh;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notification {
|
||||||
|
border-bottom: 1px solid var(--ui-gray-3);
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
@ -31,12 +45,23 @@
|
||||||
width: 5rem;
|
width: 5rem;
|
||||||
}
|
}
|
||||||
.notificationBody {
|
.notificationBody {
|
||||||
flex-basis: 30rem;
|
flex-basis: 80rem;
|
||||||
}
|
}
|
||||||
.deleteButton {
|
.deleteButton {
|
||||||
flex-basis: 5rem;
|
flex-basis: 5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.notification-details {
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: normal;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: 5;
|
||||||
|
line-clamp: 5;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
.container > div {
|
.container > div {
|
||||||
padding: 0px 10px;
|
padding: 0px 10px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
|
|
|
@ -34,6 +34,13 @@ export function NotificationsMenu() {
|
||||||
(state) => state.userNotifications[user.Id]
|
(state) => state.userNotifications[user.Id]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (userNotifications && userNotifications.length > 1) {
|
||||||
|
userNotifications.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.timeStamp).getTime() - new Date(a.timeStamp).getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const [badge, setBadge] = useState(false);
|
const [badge, setBadge] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -63,21 +70,26 @@ export function NotificationsMenu() {
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<Icon icon="bell" feather />
|
<Icon icon="bell" feather />
|
||||||
<span className={badge ? clsx(notificationStyles.badge) : ''} />
|
<span className={badge ? notificationStyles.badge : ''} />
|
||||||
</div>
|
</div>
|
||||||
</MenuButton>
|
</MenuButton>
|
||||||
|
|
||||||
<MenuList
|
<MenuList
|
||||||
className={headerStyles.menuList}
|
className={clsx(headerStyles.menuList, notificationStyles.root)}
|
||||||
aria-label="Notifications Menu"
|
aria-label="Notifications Menu"
|
||||||
data-cy="notificationsMenu"
|
data-cy="notificationsMenu"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<div className={clsx(notificationStyles.notificationContainer)}>
|
<div
|
||||||
|
className={clsx(
|
||||||
|
notificationStyles.notificationContainer,
|
||||||
|
'vertical-center'
|
||||||
|
)}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<h4>Notifications</h4>
|
<h4>Notifications</h4>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(notificationStyles.itemLast)}>
|
<div className={notificationStyles.itemLast}>
|
||||||
{userNotifications?.length > 0 && (
|
{userNotifications?.length > 0 && (
|
||||||
<Button
|
<Button
|
||||||
color="none"
|
color="none"
|
||||||
|
@ -96,26 +108,26 @@ export function NotificationsMenu() {
|
||||||
</div>
|
</div>
|
||||||
{userNotifications?.length > 0 ? (
|
{userNotifications?.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
{userNotifications.map((notification) => (
|
<div className={notificationStyles.notifications}>
|
||||||
<MenuLink
|
{userNotifications.map((notification) => (
|
||||||
to="portainer.notifications"
|
<MenuLink
|
||||||
params={{ notificationFrom: notification.timeStamp }}
|
to="portainer.notifications"
|
||||||
notification={notification}
|
params={{ id: notification.id }}
|
||||||
key={notification.id}
|
notification={notification}
|
||||||
onDelete={() => onDelete(notification.id)}
|
key={notification.id}
|
||||||
/>
|
onDelete={() => onDelete(notification.id)}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className={clsx(notificationStyles.notificationLink)}>
|
<div className={notificationStyles.notificationLink}>
|
||||||
<Link to="portainer.notifications">View all notifications</Link>
|
<Link to="portainer.notifications">View all notifications</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div className="flex flex-col items-center">
|
||||||
<Icon icon="bell" feather size="xl" />
|
<Icon icon="bell" feather size="xl" />
|
||||||
<div>
|
<p className="my-5">You have no notifications yet.</p>
|
||||||
<p>You have no notifications yet.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</MenuList>
|
</MenuList>
|
||||||
|
@ -136,35 +148,35 @@ interface MenuLinkProps extends AutomationTestingProps, UISrefProps {
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MenuLink({
|
function MenuLink({ to, params, notification, onDelete }: MenuLinkProps) {
|
||||||
to,
|
const anchorProps = useSref(to, params);
|
||||||
params,
|
|
||||||
options,
|
|
||||||
notification,
|
|
||||||
onDelete,
|
|
||||||
}: MenuLinkProps) {
|
|
||||||
const anchorProps = useSref(to, params, options);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ReachMenuLink href={anchorProps.href} className={headerStyles.menuLink}>
|
<ReachMenuLink
|
||||||
<div className={clsx(notificationStyles.container)}>
|
href={anchorProps.href}
|
||||||
<div className={clsx(notificationStyles.notificationIcon)}>
|
onClick={anchorProps.onClick}
|
||||||
|
className={clsx(headerStyles.menuLink, notificationStyles.notification)}
|
||||||
|
>
|
||||||
|
<div className={notificationStyles.container}>
|
||||||
|
<div className={notificationStyles.notificationIcon}>
|
||||||
{notification.type === 'success' ? (
|
{notification.type === 'success' ? (
|
||||||
<Icon icon="check-circle" feather size="lg" mode="success" />
|
<Icon icon="check-circle" feather size="lg" mode="success" />
|
||||||
) : (
|
) : (
|
||||||
<Icon icon="alert-circle" feather size="lg" mode="danger" />
|
<Icon icon="alert-circle" feather size="lg" mode="danger" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(notificationStyles.notificationBody)}>
|
<div className={notificationStyles.notificationBody}>
|
||||||
<p className={clsx(notificationStyles.notificationTitle)}>
|
<p className={notificationStyles.notificationTitle}>
|
||||||
{notification.title}
|
{notification.title}
|
||||||
</p>
|
</p>
|
||||||
<p>{notification.details}</p>
|
<p className={notificationStyles.notificationDetails}>
|
||||||
|
{notification.details}
|
||||||
|
</p>
|
||||||
<p className="small text-muted">
|
<p className="small text-muted">
|
||||||
{formatTime(notification.timeStamp)}
|
{formatTime(notification.timeStamp)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={clsx(notificationStyles.deleteButton)}>
|
<div className={notificationStyles.deleteButton}>
|
||||||
<Button
|
<Button
|
||||||
color="none"
|
color="none"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
|
|
|
@ -9,8 +9,9 @@ import {
|
||||||
TableInstance,
|
TableInstance,
|
||||||
TableState,
|
TableState,
|
||||||
} from 'react-table';
|
} from 'react-table';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode, useEffect } from 'react';
|
||||||
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
import { useRowSelectColumn } from '@lineup-lite/hooks';
|
||||||
|
import clsx from 'clsx';
|
||||||
|
|
||||||
import { PaginationControls } from '@@/PaginationControls';
|
import { PaginationControls } from '@@/PaginationControls';
|
||||||
import { IconProps } from '@@/Icon';
|
import { IconProps } from '@@/Icon';
|
||||||
|
@ -55,6 +56,7 @@ interface Props<
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
description?: JSX.Element;
|
description?: JSX.Element;
|
||||||
|
initialActiveItem?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Datatable<
|
export function Datatable<
|
||||||
|
@ -76,6 +78,7 @@ export function Datatable<
|
||||||
isLoading,
|
isLoading,
|
||||||
totalCount = dataset.length,
|
totalCount = dataset.length,
|
||||||
description,
|
description,
|
||||||
|
initialActiveItem,
|
||||||
}: Props<D, TSettings>) {
|
}: Props<D, TSettings>) {
|
||||||
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
|
||||||
|
|
||||||
|
@ -120,6 +123,7 @@ export function Datatable<
|
||||||
);
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
rows,
|
||||||
selectedFlatRows,
|
selectedFlatRows,
|
||||||
getTableProps,
|
getTableProps,
|
||||||
getTableBodyProps,
|
getTableBodyProps,
|
||||||
|
@ -132,6 +136,21 @@ export function Datatable<
|
||||||
state: { pageIndex, pageSize },
|
state: { pageIndex, pageSize },
|
||||||
} = tableInstance;
|
} = tableInstance;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (initialActiveItem && pageSize !== rows.length) {
|
||||||
|
const paginatedData = [...Array(Math.ceil(rows.length / pageSize))].map(
|
||||||
|
(_, i) => rows.slice(pageSize * i, pageSize + pageSize * i)
|
||||||
|
);
|
||||||
|
|
||||||
|
const itemPage = paginatedData.findIndex((sub) =>
|
||||||
|
sub.some((row) => row.id === initialActiveItem)
|
||||||
|
);
|
||||||
|
|
||||||
|
gotoPage(itemPage);
|
||||||
|
}
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [initialActiveItem]);
|
||||||
|
|
||||||
const tableProps = getTableProps();
|
const tableProps = getTableProps();
|
||||||
const tbodyProps = getTableBodyProps();
|
const tbodyProps = getTableBodyProps();
|
||||||
|
|
||||||
|
@ -194,7 +213,12 @@ export function Datatable<
|
||||||
<Table.Row<D>
|
<Table.Row<D>
|
||||||
cells={row.cells}
|
cells={row.cells}
|
||||||
key={key}
|
key={key}
|
||||||
className={className}
|
className={clsx(
|
||||||
|
className,
|
||||||
|
initialActiveItem &&
|
||||||
|
initialActiveItem === row.id &&
|
||||||
|
'active'
|
||||||
|
)}
|
||||||
role={role}
|
role={role}
|
||||||
style={style}
|
style={style}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import { Bell, Trash2 } from 'react-feather';
|
import { Bell, Trash2 } from 'react-feather';
|
||||||
import { useStore } from 'zustand';
|
import { useStore } from 'zustand';
|
||||||
|
import { useCurrentStateAndParams } from '@uirouter/react';
|
||||||
|
|
||||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||||
import { react2angular } from '@/react-tools/react2angular';
|
import { react2angular } from '@/react-tools/react2angular';
|
||||||
|
@ -17,19 +18,22 @@ import { columns } from './columns';
|
||||||
import { createStore } from './datatable-store';
|
import { createStore } from './datatable-store';
|
||||||
|
|
||||||
const storageKey = 'notifications-list';
|
const storageKey = 'notifications-list';
|
||||||
const useSettingsStore = createStore(storageKey);
|
const useSettingsStore = createStore(storageKey, 'time');
|
||||||
|
|
||||||
export function NotificationsView() {
|
export function NotificationsView() {
|
||||||
const settingsStore = useSettingsStore();
|
const settingsStore = useSettingsStore();
|
||||||
const { user } = useUser();
|
const { user } = useUser();
|
||||||
|
|
||||||
const userNotifications: ToastNotification[] = useStore(
|
const userNotifications: ToastNotification[] =
|
||||||
notificationsStore,
|
useStore(notificationsStore, (state) => state.userNotifications[user.Id]) ||
|
||||||
(state) => state.userNotifications[user.Id]
|
[];
|
||||||
);
|
|
||||||
|
|
||||||
const breadcrumbs = 'Notifications';
|
const breadcrumbs = 'Notifications';
|
||||||
|
|
||||||
|
const {
|
||||||
|
params: { id },
|
||||||
|
} = useCurrentStateAndParams();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
|
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
|
||||||
|
@ -47,6 +51,7 @@ export function NotificationsView() {
|
||||||
renderTableActions={(selectedRows) => (
|
renderTableActions={(selectedRows) => (
|
||||||
<TableActions selectedRows={selectedRows} />
|
<TableActions selectedRows={selectedRows} />
|
||||||
)}
|
)}
|
||||||
|
initialActiveItem={id}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -19,11 +19,11 @@ interface TableSettings
|
||||||
SettableColumnsTableSettings,
|
SettableColumnsTableSettings,
|
||||||
RefreshableTableSettings {}
|
RefreshableTableSettings {}
|
||||||
|
|
||||||
export function createStore(storageKey: string) {
|
export function createStore(storageKey: string, initialSortBy?: string) {
|
||||||
return create<TableSettings>()(
|
return create<TableSettings>()(
|
||||||
persist(
|
persist(
|
||||||
(set) => ({
|
(set) => ({
|
||||||
...sortableSettings(set),
|
...sortableSettings(set, initialSortBy),
|
||||||
...paginationSettings(set),
|
...paginationSettings(set),
|
||||||
...hiddenColumnsSettings(set),
|
...hiddenColumnsSettings(set),
|
||||||
...refreshableSettings(set),
|
...refreshableSettings(set),
|
||||||
|
|
Loading…
Reference in New Issue