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',
},
},
params: {
id: '',
},
});
}

View File

@ -21,6 +21,8 @@
box-shadow: 0 6px 12px rgb(0 0 0 / 18%);
@apply th-dark:!border-none;
@apply th-highcontrast:!border-none;
z-index: 9999;
position: relative;
}
.menu-link {

View File

@ -1,3 +1,7 @@
.root {
width: 500px;
}
.badge {
position: absolute;
top: 8px;
@ -23,6 +27,16 @@
margin-left: auto;
}
.notifications {
max-height: 80vh;
overflow-y: auto;
}
.notification {
border-bottom: 1px solid var(--ui-gray-3);
border-radius: 0;
}
.container {
display: flex;
}
@ -31,12 +45,23 @@
width: 5rem;
}
.notificationBody {
flex-basis: 30rem;
flex-basis: 80rem;
}
.deleteButton {
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 {
padding: 0px 10px;
margin: auto;

View File

@ -34,6 +34,13 @@ export function NotificationsMenu() {
(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);
useEffect(() => {
@ -63,21 +70,26 @@ export function NotificationsMenu() {
)}
>
<Icon icon="bell" feather />
<span className={badge ? clsx(notificationStyles.badge) : ''} />
<span className={badge ? notificationStyles.badge : ''} />
</div>
</MenuButton>
<MenuList
className={headerStyles.menuList}
className={clsx(headerStyles.menuList, notificationStyles.root)}
aria-label="Notifications Menu"
data-cy="notificationsMenu"
>
<div>
<div className={clsx(notificationStyles.notificationContainer)}>
<div
className={clsx(
notificationStyles.notificationContainer,
'vertical-center'
)}
>
<div>
<h4>Notifications</h4>
</div>
<div className={clsx(notificationStyles.itemLast)}>
<div className={notificationStyles.itemLast}>
{userNotifications?.length > 0 && (
<Button
color="none"
@ -96,26 +108,26 @@ export function NotificationsMenu() {
</div>
{userNotifications?.length > 0 ? (
<>
{userNotifications.map((notification) => (
<MenuLink
to="portainer.notifications"
params={{ notificationFrom: notification.timeStamp }}
notification={notification}
key={notification.id}
onDelete={() => onDelete(notification.id)}
/>
))}
<div className={notificationStyles.notifications}>
{userNotifications.map((notification) => (
<MenuLink
to="portainer.notifications"
params={{ id: notification.id }}
notification={notification}
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>
</div>
</>
) : (
<div>
<div className="flex flex-col items-center">
<Icon icon="bell" feather size="xl" />
<div>
<p>You have no notifications yet.</p>
</div>
<p className="my-5">You have no notifications yet.</p>
</div>
)}
</MenuList>
@ -136,35 +148,35 @@ interface MenuLinkProps extends AutomationTestingProps, UISrefProps {
onDelete: () => void;
}
function MenuLink({
to,
params,
options,
notification,
onDelete,
}: MenuLinkProps) {
const anchorProps = useSref(to, params, options);
function MenuLink({ to, params, notification, onDelete }: MenuLinkProps) {
const anchorProps = useSref(to, params);
return (
<ReachMenuLink href={anchorProps.href} className={headerStyles.menuLink}>
<div className={clsx(notificationStyles.container)}>
<div className={clsx(notificationStyles.notificationIcon)}>
<ReachMenuLink
href={anchorProps.href}
onClick={anchorProps.onClick}
className={clsx(headerStyles.menuLink, notificationStyles.notification)}
>
<div className={notificationStyles.container}>
<div className={notificationStyles.notificationIcon}>
{notification.type === 'success' ? (
<Icon icon="check-circle" feather size="lg" mode="success" />
) : (
<Icon icon="alert-circle" feather size="lg" mode="danger" />
)}
</div>
<div className={clsx(notificationStyles.notificationBody)}>
<p className={clsx(notificationStyles.notificationTitle)}>
<div className={notificationStyles.notificationBody}>
<p className={notificationStyles.notificationTitle}>
{notification.title}
</p>
<p>{notification.details}</p>
<p className={notificationStyles.notificationDetails}>
{notification.details}
</p>
<p className="small text-muted">
{formatTime(notification.timeStamp)}
</p>
</div>
<div className={clsx(notificationStyles.deleteButton)}>
<div className={notificationStyles.deleteButton}>
<Button
color="none"
onClick={(e) => {

View File

@ -9,8 +9,9 @@ import {
TableInstance,
TableState,
} from 'react-table';
import { ReactNode } from 'react';
import { ReactNode, useEffect } from 'react';
import { useRowSelectColumn } from '@lineup-lite/hooks';
import clsx from 'clsx';
import { PaginationControls } from '@@/PaginationControls';
import { IconProps } from '@@/Icon';
@ -55,6 +56,7 @@ interface Props<
isLoading?: boolean;
totalCount?: number;
description?: JSX.Element;
initialActiveItem?: string;
}
export function Datatable<
@ -76,6 +78,7 @@ export function Datatable<
isLoading,
totalCount = dataset.length,
description,
initialActiveItem,
}: Props<D, TSettings>) {
const [searchBarValue, setSearchBarValue] = useSearchBarState(storageKey);
@ -120,6 +123,7 @@ export function Datatable<
);
const {
rows,
selectedFlatRows,
getTableProps,
getTableBodyProps,
@ -132,6 +136,21 @@ export function Datatable<
state: { pageIndex, pageSize },
} = 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 tbodyProps = getTableBodyProps();
@ -194,7 +213,12 @@ export function Datatable<
<Table.Row<D>
cells={row.cells}
key={key}
className={className}
className={clsx(
className,
initialActiveItem &&
initialActiveItem === row.id &&
'active'
)}
role={role}
style={style}
/>

View File

@ -1,5 +1,6 @@
import { Bell, Trash2 } from 'react-feather';
import { useStore } from 'zustand';
import { useCurrentStateAndParams } from '@uirouter/react';
import { withCurrentUser } from '@/react-tools/withCurrentUser';
import { react2angular } from '@/react-tools/react2angular';
@ -17,19 +18,22 @@ import { columns } from './columns';
import { createStore } from './datatable-store';
const storageKey = 'notifications-list';
const useSettingsStore = createStore(storageKey);
const useSettingsStore = createStore(storageKey, 'time');
export function NotificationsView() {
const settingsStore = useSettingsStore();
const { user } = useUser();
const userNotifications: ToastNotification[] = useStore(
notificationsStore,
(state) => state.userNotifications[user.Id]
);
const userNotifications: ToastNotification[] =
useStore(notificationsStore, (state) => state.userNotifications[user.Id]) ||
[];
const breadcrumbs = 'Notifications';
const {
params: { id },
} = useCurrentStateAndParams();
return (
<>
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
@ -47,6 +51,7 @@ export function NotificationsView() {
renderTableActions={(selectedRows) => (
<TableActions selectedRows={selectedRows} />
)}
initialActiveItem={id}
/>
</>
);

View File

@ -19,11 +19,11 @@ interface TableSettings
SettableColumnsTableSettings,
RefreshableTableSettings {}
export function createStore(storageKey: string) {
export function createStore(storageKey: string, initialSortBy?: string) {
return create<TableSettings>()(
persist(
(set) => ({
...sortableSettings(set),
...sortableSettings(set, initialSortBy),
...paginationSettings(set),
...hiddenColumnsSettings(set),
...refreshableSettings(set),