fix(notifications): cleanup notifications code [EE-4274] (#7790)

* fix(notifications): cleanup notifications code [EE-4274]

* break long words
pull/7848/head
itsconquest 2022-10-11 14:05:53 +13:00 committed by GitHub
parent c6ae8467c0
commit 724f1f63b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 114 additions and 43 deletions

View File

@ -36,5 +36,8 @@ function config($stateRegistryProvider) {
component: 'notifications', component: 'notifications',
}, },
}, },
params: {
id: '',
},
}); });
} }

View File

@ -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 {

View File

@ -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;

View File

@ -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) => {

View File

@ -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}
/> />

View File

@ -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}
/> />
</> </>
); );

View File

@ -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),