Skip to content

Commit

Permalink
Add NotificationContext and useNotification hook
Browse files Browse the repository at this point in the history
Signed-off-by: Griffin-Sullivan <[email protected]>
  • Loading branch information
Griffin-Sullivan committed Oct 29, 2024
1 parent 9dfbd8e commit 7b6e4c2
Show file tree
Hide file tree
Showing 12 changed files with 306 additions and 20 deletions.
17 changes: 14 additions & 3 deletions clients/ui/frontend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 5 additions & 4 deletions clients/ui/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,8 @@
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.5",
"@cypress/code-coverage": "^3.13.4",
"@mui/material": "^6.1.3",
"@mui/icons-material": "^6.1.5",
"@mui/material": "^6.1.3",
"@mui/types": "^7.2.17",
"@testing-library/cypress": "^10.0.1",
"@testing-library/dom": "^10.4.0",
Expand All @@ -47,6 +47,7 @@
"@types/dompurify": "^3.0.5",
"@types/jest": "^29.5.13",
"@types/lodash-es": "^4.17.8",
"@types/react-dom": "^18.3.1",
"@types/react-router-dom": "^5.3.3",
"@types/showdown": "^2.0.3",
"chai-subset": "^1.6.0",
Expand Down Expand Up @@ -100,15 +101,15 @@
"@patternfly/react-styles": "6.0.0-prerelease.6",
"@patternfly/react-table": "6.0.0-prerelease.20",
"@patternfly/react-templates": "6.0.0-alpha.50",
"classnames": "^2.2.6",
"dompurify": "^3.1.6",
"lodash-es": "^4.17.15",
"npm-run-all": "^4.1.5",
"react": "^18",
"react-dom": "^18",
"react-router": "^6.26.2",
"sass": "^1.78.0",
"dompurify": "^3.1.6",
"showdown": "^2.1.0",
"classnames": "^2.2.6"
"showdown": "^2.1.0"
},
"optionalDependencies": {
"@typescript-eslint/eslint-plugin": "^8.8.1",
Expand Down
2 changes: 2 additions & 0 deletions clients/ui/frontend/src/app/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
StackItem,
} from '@patternfly/react-core';
import { BarsIcon } from '@patternfly/react-icons';
import ToastNotifications from '~/components/ToastNotifications';
import NavSidebar from './NavSidebar';
import AppRoutes from './AppRoutes';
import { AppContext } from './AppContext';
Expand Down Expand Up @@ -112,6 +113,7 @@ const App: React.FC = () => {
<ModelRegistrySelectorContextProvider>
<AppRoutes />
</ModelRegistrySelectorContextProvider>
<ToastNotifications />
</Page>
</AppContext.Provider>
);
Expand Down
88 changes: 88 additions & 0 deletions clients/ui/frontend/src/app/context/NotificationContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { AlertVariant } from '@patternfly/react-core';
import React, { createContext } from 'react';

export type Notification = {
id?: number;
status: AlertVariant;
title: string;
message?: React.ReactNode;
hidden?: boolean;
read?: boolean;
timestamp: Date;
};

export enum NotificationActionTypes {
ADD_NOTIFICATION = 'add_notification',
DELETE_NOTIFICATION = 'delete_notification',
}

type NotificationAction =
| {
type: NotificationActionTypes.ADD_NOTIFICATION;
payload: Notification;
}
| {
type: NotificationActionTypes.DELETE_NOTIFICATION;
payload: { id: Notification['id'] };
};

type NotificationContextProps = {
notifications: Notification[];
dispatch: React.Dispatch<NotificationAction>;
};

export const NotificationContext = createContext<NotificationContextProps>({
notifications: [],
// eslint-disable-next-line @typescript-eslint/no-empty-function
dispatch: () => {},
});

const notificationReducer: React.Reducer<Notification[], NotificationAction> = (
notifications,
action,
) => {
switch (action.type) {
case NotificationActionTypes.ADD_NOTIFICATION: {
return [
...notifications,
{
status: action.payload.status,
title: action.payload.title,
timestamp: action.payload.timestamp,
message: action.payload.message,
id: action.payload.id,
},
];
}
case NotificationActionTypes.DELETE_NOTIFICATION: {
return notifications.filter((t) => t.id !== action.payload.id);
}
default: {
return notifications;
}
}
};

type NotificationContextProviderProps = {
children: React.ReactNode;
};

export const NotificationContextProvider: React.FC<NotificationContextProviderProps> = ({
children,
}) => {
const [notifications, dispatch] = React.useReducer(notificationReducer, []);

return (
<NotificationContext.Provider
value={React.useMemo(
() => ({
notifications,
dispatch,
}),
[notifications, dispatch],
)}
>
{children}
</NotificationContext.Provider>
);
};
109 changes: 109 additions & 0 deletions clients/ui/frontend/src/app/hooks/useNotification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import React, { useContext } from 'react';
import { AlertVariant } from '@patternfly/react-core';
import { NotificationActionTypes, NotificationContext } from '~/app/context/NotificationContext';

enum NotificationTypes {
SUCCESS = 'success',
ERROR = 'error',
INFO = 'info',
WARNING = 'warning',
}

type NotificationProps = (title: string, message?: React.ReactNode) => void;

type NotificationRemoveProps = (id: number | undefined) => void;

type NotificationTypeFunc = {
[key in NotificationTypes]: NotificationProps;
};

interface NotificationFunc extends NotificationTypeFunc {
remove: NotificationRemoveProps;
}

export const useNotification = (): NotificationFunc => {
const { dispatch } = useContext(NotificationContext);
// need to move this count somewhere else since it will reset on every new useNotification instance (like switching pages)
let notificationCount = 0;

const success: NotificationProps = React.useCallback(
(title, message?) => {
dispatch({
type: NotificationActionTypes.ADD_NOTIFICATION,
payload: {
status: AlertVariant.success,
title,
timestamp: new Date(),
message,
id: ++notificationCount,
},
});
},
[dispatch, notificationCount],
);

const warning: NotificationProps = React.useCallback(
(title, message?) => {
dispatch({
type: NotificationActionTypes.ADD_NOTIFICATION,
payload: {
status: AlertVariant.warning,
title,
timestamp: new Date(),
message,
id: ++notificationCount,
},
});
},
[dispatch, notificationCount],
);

const error: NotificationProps = React.useCallback(
(title, message?) => {
dispatch({
type: NotificationActionTypes.ADD_NOTIFICATION,
payload: {
status: AlertVariant.danger,
title,
timestamp: new Date(),
message,
id: ++notificationCount,
},
});
},
[dispatch, notificationCount],
);

const info: NotificationProps = React.useCallback(
(title, message?) => {
dispatch({
type: NotificationActionTypes.ADD_NOTIFICATION,
payload: {
status: AlertVariant.info,
title,
timestamp: new Date(),
message,
id: ++notificationCount,
},
});
},
[dispatch, notificationCount],
);

const remove: NotificationRemoveProps = React.useCallback(
(id) => {
dispatch({
type: NotificationActionTypes.DELETE_NOTIFICATION,
payload: { id },
});
},
[dispatch],
);

const notification = React.useMemo(
() => ({ success, error, info, warning, remove }),
[success, error, info, warning, remove],
);

return notification;
};
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
TextInput,
} from '@patternfly/react-core';
import DashboardModalFooter from '~/app/components/DashboardModalFooter';
import { useNotification } from '~/app/hooks/useNotification';

interface ArchiveModelVersionModalProps {
onCancel: () => void;
Expand All @@ -27,6 +28,7 @@ export const ArchiveModelVersionModal: React.FC<ArchiveModelVersionModalProps> =
const [error, setError] = React.useState<Error>();
const [confirmInputValue, setConfirmInputValue] = React.useState('');
const isDisabled = confirmInputValue.trim() !== modelVersionName || isSubmitting;
const notification = useNotification();

const onClose = React.useCallback(() => {
setConfirmInputValue('');
Expand All @@ -39,14 +41,15 @@ export const ArchiveModelVersionModal: React.FC<ArchiveModelVersionModalProps> =
try {
await onSubmit();
onClose();
notification.success(`${modelVersionName} archived.`);
} catch (e) {
if (e instanceof Error) {
setError(e);
}
} finally {
setIsSubmitting(false);
}
}, [onSubmit, onClose]);
}, [notification, modelVersionName, onSubmit, onClose]);

const description = (
<>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import {
TextInput,
} from '@patternfly/react-core';
import DashboardModalFooter from '~/app/components/DashboardModalFooter';

// import useNotification from '~/utilities/useNotification'; // TODO: Implement useNotification
import { useNotification } from '~/app/hooks/useNotification';

interface ArchiveRegisteredModelModalProps {
onCancel: () => void;
Expand All @@ -25,7 +24,7 @@ export const ArchiveRegisteredModelModal: React.FC<ArchiveRegisteredModelModalPr
isOpen,
registeredModelName,
}) => {
// const notification = useNotification();
const notification = useNotification();
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [error, setError] = React.useState<Error>();
const [confirmInputValue, setConfirmInputValue] = React.useState('');
Expand All @@ -42,15 +41,15 @@ export const ArchiveRegisteredModelModal: React.FC<ArchiveRegisteredModelModalPr
try {
await onSubmit();
onClose();
// notification.success(`${registeredModelName} and all its versions archived.`);
notification.success(`${registeredModelName} and all its versions archived.`);
} catch (e) {
if (e instanceof Error) {
setError(e);
}
} finally {
setIsSubmitting(false);
}
}, [onSubmit, onClose]);
}, [notification, registeredModelName, onSubmit, onClose]);

const description = (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as React from 'react';
import { Form, Modal, ModalHeader, ModalBody, Alert } from '@patternfly/react-core';
import DashboardModalFooter from '~/app/components/DashboardModalFooter';
import { useNotification } from '~/app/hooks/useNotification';

interface RestoreModelVersionModalProps {
onCancel: () => void;
Expand All @@ -17,6 +18,7 @@ export const RestoreModelVersionModal: React.FC<RestoreModelVersionModalProps> =
}) => {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [error, setError] = React.useState<Error>();
const notification = useNotification();

const onClose = React.useCallback(() => {
onCancel();
Expand All @@ -28,14 +30,15 @@ export const RestoreModelVersionModal: React.FC<RestoreModelVersionModalProps> =
try {
await onSubmit();
onClose();
notification.success(`${modelVersionName} restored.`);
} catch (e) {
if (e instanceof Error) {
setError(e);
}
} finally {
setIsSubmitting(false);
}
}, [onSubmit, onClose]);
}, [notification, modelVersionName, onSubmit, onClose]);

const description = (
<>
Expand Down
Loading

0 comments on commit 7b6e4c2

Please sign in to comment.