mirror of
https://github.com/portainer/portainer.git
synced 2025-08-09 15:55:23 +02:00
feat(notifications): track toast notifications [EE-4132] (#7711)
* feat(notifications): track toast notifications [EE-4132] * suggested refactoring * fix failing test * remove duplicate styles * applying spacing to context icon
This commit is contained in:
parent
4e20d70a99
commit
648c1db437
18 changed files with 608 additions and 59 deletions
79
app/react/portainer/notifications/NotificationsView.tsx
Normal file
79
app/react/portainer/notifications/NotificationsView.tsx
Normal file
|
@ -0,0 +1,79 @@
|
|||
import { Bell, Trash2 } from 'react-feather';
|
||||
import { useStore } from 'zustand';
|
||||
|
||||
import { withCurrentUser } from '@/react-tools/withCurrentUser';
|
||||
import { react2angular } from '@/react-tools/react2angular';
|
||||
import { useUser } from '@/portainer/hooks/useUser';
|
||||
import { withUIRouter } from '@/react-tools/withUIRouter';
|
||||
import { withReactQuery } from '@/react-tools/withReactQuery';
|
||||
|
||||
import { PageHeader } from '@@/PageHeader';
|
||||
import { Datatable } from '@@/datatables';
|
||||
import { Button } from '@@/buttons';
|
||||
|
||||
import { notificationsStore } from './notifications-store';
|
||||
import { ToastNotification } from './types';
|
||||
import { columns } from './columns';
|
||||
import { createStore } from './datatable-store';
|
||||
|
||||
const storageKey = 'notifications-list';
|
||||
const useSettingsStore = createStore(storageKey);
|
||||
|
||||
export function NotificationsView() {
|
||||
const settingsStore = useSettingsStore();
|
||||
const { user } = useUser();
|
||||
|
||||
const userNotifications: ToastNotification[] = useStore(
|
||||
notificationsStore,
|
||||
(state) => state.userNotifications[user.Id]
|
||||
);
|
||||
|
||||
const breadcrumbs = 'Notifications';
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageHeader title="Notifications" breadcrumbs={breadcrumbs} reload />
|
||||
<Datatable
|
||||
columns={columns}
|
||||
titleOptions={{
|
||||
title: 'Notifications',
|
||||
icon: Bell,
|
||||
}}
|
||||
dataset={userNotifications}
|
||||
settingsStore={settingsStore}
|
||||
storageKey="notifications"
|
||||
emptyContentLabel="No notifications found"
|
||||
totalCount={userNotifications.length}
|
||||
renderTableActions={(selectedRows) => (
|
||||
<TableActions selectedRows={selectedRows} />
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function TableActions({ selectedRows }: { selectedRows: ToastNotification[] }) {
|
||||
const { user } = useUser();
|
||||
const notificationsStoreState = useStore(notificationsStore);
|
||||
return (
|
||||
<Button
|
||||
icon={Trash2}
|
||||
color="dangerlight"
|
||||
onClick={() => handleRemove()}
|
||||
disabled={selectedRows.length === 0}
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
);
|
||||
|
||||
function handleRemove() {
|
||||
const { removeNotifications } = notificationsStoreState;
|
||||
const ids = selectedRows.map((row) => row.id);
|
||||
removeNotifications(user.Id, ids);
|
||||
}
|
||||
}
|
||||
|
||||
export const NotificationsViewAngular = react2angular(
|
||||
withUIRouter(withReactQuery(withCurrentUser(NotificationsView))),
|
||||
[]
|
||||
);
|
11
app/react/portainer/notifications/columns/details.tsx
Normal file
11
app/react/portainer/notifications/columns/details.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const details: Column<ToastNotification> = {
|
||||
Header: 'Details',
|
||||
accessor: 'details',
|
||||
id: 'details',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
6
app/react/portainer/notifications/columns/index.tsx
Normal file
6
app/react/portainer/notifications/columns/index.tsx
Normal file
|
@ -0,0 +1,6 @@
|
|||
import { type } from './type';
|
||||
import { title } from './title';
|
||||
import { details } from './details';
|
||||
import { time } from './time';
|
||||
|
||||
export const columns = [type, title, details, time];
|
13
app/react/portainer/notifications/columns/time.tsx
Normal file
13
app/react/portainer/notifications/columns/time.tsx
Normal file
|
@ -0,0 +1,13 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { isoDate } from '@/portainer/filters/filters';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const time: Column<ToastNotification> = {
|
||||
Header: 'Time',
|
||||
accessor: (row) => (row.timeStamp ? isoDate(row.timeStamp) : '-'),
|
||||
id: 'time',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
11
app/react/portainer/notifications/columns/title.tsx
Normal file
11
app/react/portainer/notifications/columns/title.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const title: Column<ToastNotification> = {
|
||||
Header: 'Title',
|
||||
accessor: 'title',
|
||||
id: 'title',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
11
app/react/portainer/notifications/columns/type.tsx
Normal file
11
app/react/portainer/notifications/columns/type.tsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { Column } from 'react-table';
|
||||
|
||||
import { ToastNotification } from '../types';
|
||||
|
||||
export const type: Column<ToastNotification> = {
|
||||
Header: 'Type',
|
||||
accessor: (row) => row.type.charAt(0).toUpperCase() + row.type.slice(1),
|
||||
id: 'type',
|
||||
disableFilters: true,
|
||||
canHide: true,
|
||||
};
|
36
app/react/portainer/notifications/datatable-store.ts
Normal file
36
app/react/portainer/notifications/datatable-store.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
import create from 'zustand';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
|
||||
import {
|
||||
paginationSettings,
|
||||
sortableSettings,
|
||||
refreshableSettings,
|
||||
hiddenColumnsSettings,
|
||||
PaginationTableSettings,
|
||||
RefreshableTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
SortableTableSettings,
|
||||
} from '@/react/components/datatables/types';
|
||||
|
||||
interface TableSettings
|
||||
extends SortableTableSettings,
|
||||
PaginationTableSettings,
|
||||
SettableColumnsTableSettings,
|
||||
RefreshableTableSettings {}
|
||||
|
||||
export function createStore(storageKey: string) {
|
||||
return create<TableSettings>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
...sortableSettings(set),
|
||||
...paginationSettings(set),
|
||||
...hiddenColumnsSettings(set),
|
||||
...refreshableSettings(set),
|
||||
}),
|
||||
{
|
||||
name: keyBuilder(storageKey),
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
64
app/react/portainer/notifications/notifications-store.ts
Normal file
64
app/react/portainer/notifications/notifications-store.ts
Normal file
|
@ -0,0 +1,64 @@
|
|||
import create from 'zustand/vanilla';
|
||||
import { persist } from 'zustand/middleware';
|
||||
|
||||
import { keyBuilder } from '@/portainer/hooks/useLocalStorage';
|
||||
|
||||
import { ToastNotification } from './types';
|
||||
|
||||
interface NotificationsState {
|
||||
userNotifications: Record<string, ToastNotification[]>;
|
||||
addNotification: (userId: number, notification: ToastNotification) => void;
|
||||
removeNotification: (userId: number, notificationId: string) => void;
|
||||
removeNotifications: (userId: number, notifications: string[]) => void;
|
||||
clearUserNotifications: (userId: number) => void;
|
||||
}
|
||||
|
||||
export const notificationsStore = create<NotificationsState>()(
|
||||
persist(
|
||||
(set) => ({
|
||||
userNotifications: {},
|
||||
addNotification: (userId: number, notification: ToastNotification) => {
|
||||
set((state) => ({
|
||||
userNotifications: {
|
||||
...state.userNotifications,
|
||||
[userId]: [
|
||||
...(state.userNotifications[userId] || []),
|
||||
notification,
|
||||
],
|
||||
},
|
||||
}));
|
||||
},
|
||||
removeNotification: (userId: number, notificationId: string) => {
|
||||
set((state) => ({
|
||||
userNotifications: {
|
||||
...state.userNotifications,
|
||||
[userId]: state.userNotifications[userId].filter(
|
||||
(notif) => notif.id !== notificationId
|
||||
),
|
||||
},
|
||||
}));
|
||||
},
|
||||
removeNotifications: (userId: number, notificationIds: string[]) => {
|
||||
set((state) => ({
|
||||
userNotifications: {
|
||||
...state.userNotifications,
|
||||
[userId]: state.userNotifications[userId].filter(
|
||||
(notification) => !notificationIds.includes(notification.id)
|
||||
),
|
||||
},
|
||||
}));
|
||||
},
|
||||
clearUserNotifications: (userId: number) => {
|
||||
set((state) => ({
|
||||
userNotifications: {
|
||||
...state.userNotifications,
|
||||
[userId]: [],
|
||||
},
|
||||
}));
|
||||
},
|
||||
}),
|
||||
{
|
||||
name: keyBuilder('notifications'),
|
||||
}
|
||||
)
|
||||
);
|
7
app/react/portainer/notifications/types.ts
Normal file
7
app/react/portainer/notifications/types.ts
Normal file
|
@ -0,0 +1,7 @@
|
|||
export type ToastNotification = {
|
||||
id: string;
|
||||
title: string;
|
||||
details: string;
|
||||
type: string;
|
||||
timeStamp: Date;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue