From 5a01da29168973fefb985391de485f999c2fffa4 Mon Sep 17 00:00:00 2001 From: Jamie Curnow Date: Wed, 3 Sep 2025 18:01:00 +1000 Subject: [PATCH] Notification toasts, nicer loading, add new user support --- backend/internal/token.js | 16 +- backend/internal/user.js | 6 +- backend/models/token.js | 6 +- frontend/package.json | 1 + frontend/src/App.css | 7 + frontend/src/App.tsx | 10 + frontend/src/api/backend/index.ts | 1 + frontend/src/components/HasPermission.tsx | 24 +- ...dingPage.module.css => Loading.module.css} | 0 frontend/src/components/Loading.tsx | 22 ++ frontend/src/components/LoadingPage.tsx | 16 +- frontend/src/components/index.ts | 1 + frontend/src/hooks/useUser.ts | 20 +- frontend/src/locale/lang/en.json | 4 + frontend/src/locale/src/en.json | 12 + frontend/src/modals/UserModal.tsx | 353 +++++++++--------- frontend/src/notifications/Msg.module.css | 14 + frontend/src/notifications/Msg.tsx | 36 ++ frontend/src/notifications/helpers.tsx | 27 ++ frontend/src/notifications/index.ts | 1 + frontend/src/pages/Access/index.tsx | 2 +- frontend/src/pages/AuditLog/index.tsx | 2 +- frontend/src/pages/Certificates/index.tsx | 2 +- frontend/src/pages/Nginx/DeadHosts/index.tsx | 2 +- frontend/src/pages/Nginx/ProxyHosts/index.tsx | 2 +- .../pages/Nginx/RedirectionHosts/index.tsx | 2 +- frontend/src/pages/Nginx/Streams/index.tsx | 2 +- frontend/src/pages/Settings/index.tsx | 2 +- frontend/src/pages/Users/Empty.tsx | 7 +- frontend/src/pages/Users/Table.tsx | 10 +- frontend/src/pages/Users/TableWrapper.tsx | 5 +- frontend/src/pages/Users/index.tsx | 2 +- frontend/yarn.lock | 12 + 33 files changed, 414 insertions(+), 215 deletions(-) rename frontend/src/components/{LoadingPage.module.css => Loading.module.css} (100%) create mode 100644 frontend/src/components/Loading.tsx create mode 100644 frontend/src/notifications/Msg.module.css create mode 100644 frontend/src/notifications/Msg.tsx create mode 100644 frontend/src/notifications/helpers.tsx create mode 100644 frontend/src/notifications/index.ts diff --git a/backend/internal/token.js b/backend/internal/token.js index 810ac6aa..f1d2b370 100644 --- a/backend/internal/token.js +++ b/backend/internal/token.js @@ -134,24 +134,24 @@ export default { * @param {Object} user * @returns {Promise} */ - getTokenFromUser: (user) => { + getTokenFromUser: async (user) => { const expire = "1d"; const Token = new TokenModel(); const expiry = parseDatePeriod(expire); - return Token.create({ + const signed = await Token.create({ iss: "api", attrs: { id: user.id, }, scope: ["user"], expiresIn: expire, - }).then((signed) => { - return { - token: signed.token, - expires: expiry.toISOString(), - user: user, - }; }); + + return { + token: signed.token, + expires: expiry.toISOString(), + user: user, + }; }, }; diff --git a/backend/internal/user.js b/backend/internal/user.js index e3d7ca4c..1c1f3a86 100644 --- a/backend/internal/user.js +++ b/backend/internal/user.js @@ -337,11 +337,11 @@ const internalUser = { * @param {Integer} [id_requested] * @returns {[String]} */ - getUserOmisionsByAccess: (access, id_requested) => { + getUserOmisionsByAccess: (access, idRequested) => { let response = []; // Admin response - if (!access.token.hasScope("admin") && access.token.getUserId(0) !== id_requested) { - response = ["roles", "is_deleted"]; // Restricted response + if (!access.token.hasScope("admin") && access.token.getUserId(0) !== idRequested) { + response = ["is_deleted"]; // Restricted response } return response; diff --git a/backend/models/token.js b/backend/models/token.js index 5edad90d..fb40e2a0 100644 --- a/backend/models/token.js +++ b/backend/models/token.js @@ -123,16 +123,16 @@ export default () => { }, /** - * @param [default_value] + * @param [defaultValue] * @returns {Integer} */ - getUserId: (default_value) => { + getUserId: (defaultValue) => { const attrs = self.get("attrs"); if (attrs && typeof attrs.id !== "undefined" && attrs.id) { return attrs.id; } - return default_value || 0; + return defaultValue || 0; }, }; diff --git a/frontend/package.json b/frontend/package.json index eac969ec..c94566ff 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -30,6 +30,7 @@ "react-dom": "^19.1.1", "react-intl": "^7.1.11", "react-router-dom": "^7.8.2", + "react-toastify": "^11.0.5", "rooks": "^9.2.0" }, "devDependencies": { diff --git a/frontend/src/App.css b/frontend/src/App.css index 1e642918..59677e8c 100644 --- a/frontend/src/App.css +++ b/frontend/src/App.css @@ -5,3 +5,10 @@ .domain-name { font-family: monospace; } + +.mr-1 { + margin-right: 0.25rem; +} +.ml-1 { + margin-left: 0.25rem; +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index aa2fb873..f2312f3f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import { RawIntlProvider } from "react-intl"; +import { ToastContainer } from "react-toastify"; import { AuthProvider, LocaleProvider, ThemeProvider } from "src/context"; import { intl } from "src/locale"; import Router from "src/Router.tsx"; @@ -16,6 +17,15 @@ function App() { + diff --git a/frontend/src/api/backend/index.ts b/frontend/src/api/backend/index.ts index 745ce3ce..1bfccb4b 100644 --- a/frontend/src/api/backend/index.ts +++ b/frontend/src/api/backend/index.ts @@ -4,6 +4,7 @@ export * from "./createDeadHost"; export * from "./createProxyHost"; export * from "./createRedirectionHost"; export * from "./createStream"; +export * from "./createUser"; export * from "./deleteAccessList"; export * from "./deleteCertificate"; export * from "./deleteDeadHost"; diff --git a/frontend/src/components/HasPermission.tsx b/frontend/src/components/HasPermission.tsx index 9b45e7f6..c4779b9e 100644 --- a/frontend/src/components/HasPermission.tsx +++ b/frontend/src/components/HasPermission.tsx @@ -1,5 +1,6 @@ import type { ReactNode } from "react"; import Alert from "react-bootstrap/Alert"; +import { Loading, LoadingPage } from "src/components"; import { useUser } from "src/hooks"; import { intl } from "src/locale"; @@ -8,11 +9,30 @@ interface Props { type: "manage" | "view"; hideError?: boolean; children?: ReactNode; + pageLoading?: boolean; + loadingNoLogo?: boolean; } -function HasPermission({ permission, type, children, hideError = false }: Props) { - const { data } = useUser("me"); +function HasPermission({ + permission, + type, + children, + hideError = false, + pageLoading = false, + loadingNoLogo = false, +}: Props) { + const { data, isLoading } = useUser("me"); const perms = data?.permissions; + if (isLoading) { + if (hideError) { + return null; + } + if (pageLoading) { + return ; + } + return ; + } + let allowed = permission === ""; const acceptable = ["manage", type]; diff --git a/frontend/src/components/LoadingPage.module.css b/frontend/src/components/Loading.module.css similarity index 100% rename from frontend/src/components/LoadingPage.module.css rename to frontend/src/components/Loading.module.css diff --git a/frontend/src/components/Loading.tsx b/frontend/src/components/Loading.tsx new file mode 100644 index 00000000..35a054db --- /dev/null +++ b/frontend/src/components/Loading.tsx @@ -0,0 +1,22 @@ +import { intl } from "src/locale"; +import styles from "./Loading.module.css"; + +interface Props { + label?: string; + noLogo?: boolean; +} +export function Loading({ label, noLogo }: Props) { + return ( +
+ {noLogo ? null : ( +
+ +
+ )} +
{label || intl.formatMessage({ id: "loading" })}
+
+
+
+
+ ); +} diff --git a/frontend/src/components/LoadingPage.tsx b/frontend/src/components/LoadingPage.tsx index 6be77ebb..7d3bec1e 100644 --- a/frontend/src/components/LoadingPage.tsx +++ b/frontend/src/components/LoadingPage.tsx @@ -1,6 +1,4 @@ -import { Page } from "src/components"; -import { intl } from "src/locale"; -import styles from "./LoadingPage.module.css"; +import { Loading, Page } from "src/components"; interface Props { label?: string; @@ -10,17 +8,7 @@ export function LoadingPage({ label, noLogo }: Props) { return (
-
- {noLogo ? null : ( -
- -
- )} -
{label || intl.formatMessage({ id: "loading" })}
-
-
-
-
+
); diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 66e12a74..be87e068 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -2,6 +2,7 @@ export * from "./Button"; export * from "./ErrorNotFound"; export * from "./Flag"; export * from "./HasPermission"; +export * from "./Loading"; export * from "./LoadingPage"; export * from "./LocalePicker"; export * from "./NavLink"; diff --git a/frontend/src/hooks/useUser.ts b/frontend/src/hooks/useUser.ts index 180032c2..fb991438 100644 --- a/frontend/src/hooks/useUser.ts +++ b/frontend/src/hooks/useUser.ts @@ -1,7 +1,20 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; -import { getUser, type User, updateUser } from "src/api/backend"; +import { createUser, getUser, type User, updateUser } from "src/api/backend"; const fetchUser = (id: number | string) => { + if (id === "new") { + return Promise.resolve({ + id: 0, + createdOn: "", + modifiedOn: "", + isDisabled: false, + email: "", + name: "", + nickname: "", + roles: [], + avatar: "", + } as User); + } return getUser(id, { expand: "permissions" }); }; @@ -17,8 +30,11 @@ const useUser = (id: string | number, options = {}) => { const useSetUser = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (values: User) => updateUser(values), + mutationFn: (values: User) => (values.id ? updateUser(values) : createUser(values)), onMutate: (values: User) => { + if (!values.id) { + return; + } const previousObject = queryClient.getQueryData(["user", values.id]); queryClient.setQueryData(["user", values.id], (old: User) => ({ ...old, diff --git a/frontend/src/locale/lang/en.json b/frontend/src/locale/lang/en.json index eae027b8..f59a6d4d 100644 --- a/frontend/src/locale/lang/en.json +++ b/frontend/src/locale/lang/en.json @@ -51,6 +51,9 @@ "notfound.action": "Take me home", "notfound.text": "We are sorry but the page you are looking for was not found", "notfound.title": "Oops… You just found an error page", + "notification.error": "Error", + "notification.success": "Success", + "notification.user-saved": "User has been saved", "offline": "Offline", "online": "Online", "password": "Password", @@ -82,6 +85,7 @@ "user.edit-profile": "Edit Profile", "user.full-name": "Full Name", "user.logout": "Logout", + "user.new": "New User", "user.new-password": "New Password", "user.nickname": "Nickname", "user.switch-dark": "Switch to Dark mode", diff --git a/frontend/src/locale/src/en.json b/frontend/src/locale/src/en.json index b3e881de..d37553e9 100644 --- a/frontend/src/locale/src/en.json +++ b/frontend/src/locale/src/en.json @@ -155,6 +155,15 @@ "notfound.title": { "defaultMessage": "Oops… You just found an error page" }, + "notification.error": { + "defaultMessage": "Error" + }, + "notification.user-saved": { + "defaultMessage": "User has been saved" + }, + "notification.success": { + "defaultMessage": "Success" + }, "offline": { "defaultMessage": "Offline" }, @@ -248,6 +257,9 @@ "user.logout": { "defaultMessage": "Logout" }, + "user.new": { + "defaultMessage": "New User" + }, "user.new-password": { "defaultMessage": "New Password" }, diff --git a/frontend/src/modals/UserModal.tsx b/frontend/src/modals/UserModal.tsx index 0f25817a..293b2ae9 100644 --- a/frontend/src/modals/UserModal.tsx +++ b/frontend/src/modals/UserModal.tsx @@ -2,31 +2,35 @@ import { Field, Form, Formik } from "formik"; import { useState } from "react"; import { Alert } from "react-bootstrap"; import Modal from "react-bootstrap/Modal"; -import { Button } from "src/components"; +import { Button, Loading } from "src/components"; import { useSetUser, useUser } from "src/hooks"; import { intl } from "src/locale"; import { validateEmail, validateString } from "src/modules/Validations"; +import { showSuccess } from "src/notifications"; interface Props { - userId: number | "me"; + userId: number | "me" | "new"; onClose: () => void; } export function UserModal({ userId, onClose }: Props) { - const { data } = useUser(userId); - const { data: currentUser } = useUser("me"); + const { data, isLoading, error } = useUser(userId); + const { data: currentUser, isLoading: currentIsLoading } = useUser("me"); const { mutate: setUser } = useSetUser(); - const [error, setError] = useState(null); + const [errorMsg, setErrorMsg] = useState(null); + + if (data && currentUser) { + console.log("DATA:", data); + console.log("CURRENT:", currentUser); + } const onSubmit = async (values: any, { setSubmitting }: any) => { - setError(null); + setErrorMsg(null); const { ...payload } = { - id: userId, + id: userId === "new" ? undefined : userId, roles: [], ...values, }; - console.log("values", values); - if (data?.id === currentUser?.id) { // Prevent user from locking themselves out delete payload.isDisabled; @@ -39,175 +43,188 @@ export function UserModal({ userId, onClose }: Props) { delete payload.isAdmin; setUser(payload, { - onError: (err: any) => setError(err.message), - onSuccess: () => onClose(), + onError: (err: any) => setErrorMsg(err.message), + onSuccess: () => { + showSuccess(intl.formatMessage({ id: "notification.user-saved" })); + onClose(); + }, onSettled: () => setSubmitting(false), }); }; return ( - - {({ isSubmitting }) => ( -
- - {intl.formatMessage({ id: "user.edit" })} - - - setError(null)} dismissible> - {error} - -
-
-
- - {({ field, form }: any) => ( -
- - - {form.errors.name ? ( -
- {form.errors.name && form.touched.name - ? form.errors.name - : null} -
- ) : null} -
- )} -
-
-
-
-
- - {({ field, form }: any) => ( -
- - - {form.errors.nickname ? ( -
- {form.errors.nickname && form.touched.nickname - ? form.errors.nickname - : null} -
- ) : null} -
- )} -
-
-
-
-
- - {({ field, form }: any) => ( -
- - - {form.errors.email ? ( -
- {form.errors.email && form.touched.email ? form.errors.email : null} -
- ) : null} + {!isLoading && error && {error?.message || "Unknown error"}} + {(isLoading || currentIsLoading) && } + {!isLoading && !currentIsLoading && data && currentUser && ( + + {({ isSubmitting }) => ( + + + + {intl.formatMessage({ id: data?.id ? "user.edit" : "user.new" })} + + + + setErrorMsg(null)} dismissible> + {errorMsg} + +
+
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.name ? ( +
+ {form.errors.name && form.touched.name + ? form.errors.name + : null} +
+ ) : null} +
+ )} +
- )} - -
- {currentUser && data && currentUser?.id !== data?.id ? ( -
-

Properties

+
+
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.nickname ? ( +
+ {form.errors.nickname && form.touched.nickname + ? form.errors.nickname + : null} +
+ ) : null} +
+ )} +
+
+
+
+
+ + {({ field, form }: any) => ( +
+ + + {form.errors.email ? ( +
+ {form.errors.email && form.touched.email + ? form.errors.email + : null} +
+ ) : null} +
+ )} +
+
+ {currentUser && data && currentUser?.id !== data?.id ? ( +
+

Properties

-
-
- -
-
- +
+
+ +
+
+ +
-
- ) : null} - - - - - - - )} - + ) : null} + + + + + + + )} + + )} ); } diff --git a/frontend/src/notifications/Msg.module.css b/frontend/src/notifications/Msg.module.css new file mode 100644 index 00000000..24c55921 --- /dev/null +++ b/frontend/src/notifications/Msg.module.css @@ -0,0 +1,14 @@ +.toaster { + padding: 0; + background: transparent !important; + box-shadow: none !important; + border: none !important; + + &.toast { + border-radius: 0; + box-shadow: none; + font-size: 14px; + padding: 16px 24px; + background: transparent; + } +} diff --git a/frontend/src/notifications/Msg.tsx b/frontend/src/notifications/Msg.tsx new file mode 100644 index 00000000..b68b8671 --- /dev/null +++ b/frontend/src/notifications/Msg.tsx @@ -0,0 +1,36 @@ +import { IconCheck, IconExclamationCircle } from "@tabler/icons-react"; +import cn from "classnames"; +import type { ReactNode } from "react"; + +function Msg({ data }: any) { + const cns = cn("toast", "show", data.type || null); + + let icon: ReactNode = null; + switch (data.type) { + case "success": + icon = ; + break; + case "error": + icon = ; + break; + } + + return ( +
+ {data.title && ( +
+ {icon} {data.title} +
+ )} +
{data.message}
+
+ ); +} +export { Msg }; diff --git a/frontend/src/notifications/helpers.tsx b/frontend/src/notifications/helpers.tsx new file mode 100644 index 00000000..99334c0f --- /dev/null +++ b/frontend/src/notifications/helpers.tsx @@ -0,0 +1,27 @@ +import { toast } from "react-toastify"; +import { intl } from "src/locale"; +import { Msg } from "./Msg"; +import styles from "./Msg.module.css"; + +const showSuccess = (message: string) => { + toast(Msg, { + className: styles.toaster, + data: { + type: "success", + title: intl.formatMessage({ id: "notification.success" }), + message, + }, + }); +}; + +const showError = (message: string) => { + toast(, { + data: { + type: "error", + title: intl.formatMessage({ id: "notification.error" }), + message, + }, + }); +}; + +export { showSuccess, showError }; diff --git a/frontend/src/notifications/index.ts b/frontend/src/notifications/index.ts new file mode 100644 index 00000000..d4e09d7b --- /dev/null +++ b/frontend/src/notifications/index.ts @@ -0,0 +1 @@ +export * from "./helpers"; diff --git a/frontend/src/pages/Access/index.tsx b/frontend/src/pages/Access/index.tsx index 2708f85f..71fd23fe 100644 --- a/frontend/src/pages/Access/index.tsx +++ b/frontend/src/pages/Access/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const Access = () => { return ( - + ); diff --git a/frontend/src/pages/AuditLog/index.tsx b/frontend/src/pages/AuditLog/index.tsx index 3680ecfb..4e251871 100644 --- a/frontend/src/pages/AuditLog/index.tsx +++ b/frontend/src/pages/AuditLog/index.tsx @@ -3,7 +3,7 @@ import AuditTable from "./AuditTable"; const AuditLog = () => { return ( - + ); diff --git a/frontend/src/pages/Certificates/index.tsx b/frontend/src/pages/Certificates/index.tsx index b4dca48c..7f1bd3a0 100644 --- a/frontend/src/pages/Certificates/index.tsx +++ b/frontend/src/pages/Certificates/index.tsx @@ -3,7 +3,7 @@ import CertificateTable from "./CertificateTable"; const Certificates = () => { return ( - + ); diff --git a/frontend/src/pages/Nginx/DeadHosts/index.tsx b/frontend/src/pages/Nginx/DeadHosts/index.tsx index 94e0ab68..99693045 100644 --- a/frontend/src/pages/Nginx/DeadHosts/index.tsx +++ b/frontend/src/pages/Nginx/DeadHosts/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const DeadHosts = () => { return ( - + ); diff --git a/frontend/src/pages/Nginx/ProxyHosts/index.tsx b/frontend/src/pages/Nginx/ProxyHosts/index.tsx index aa0e4774..80a8fd09 100644 --- a/frontend/src/pages/Nginx/ProxyHosts/index.tsx +++ b/frontend/src/pages/Nginx/ProxyHosts/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const ProxyHosts = () => { return ( - + ); diff --git a/frontend/src/pages/Nginx/RedirectionHosts/index.tsx b/frontend/src/pages/Nginx/RedirectionHosts/index.tsx index bc636b35..3df72827 100644 --- a/frontend/src/pages/Nginx/RedirectionHosts/index.tsx +++ b/frontend/src/pages/Nginx/RedirectionHosts/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const RedirectionHosts = () => { return ( - + ); diff --git a/frontend/src/pages/Nginx/Streams/index.tsx b/frontend/src/pages/Nginx/Streams/index.tsx index ab3f8fc6..c997ea69 100644 --- a/frontend/src/pages/Nginx/Streams/index.tsx +++ b/frontend/src/pages/Nginx/Streams/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const Streams = () => { return ( - + ); diff --git a/frontend/src/pages/Settings/index.tsx b/frontend/src/pages/Settings/index.tsx index bb2e84e2..2774c795 100644 --- a/frontend/src/pages/Settings/index.tsx +++ b/frontend/src/pages/Settings/index.tsx @@ -3,7 +3,7 @@ import SettingTable from "./SettingTable"; const Settings = () => { return ( - + ); diff --git a/frontend/src/pages/Users/Empty.tsx b/frontend/src/pages/Users/Empty.tsx index 162e7094..f17b1f66 100644 --- a/frontend/src/pages/Users/Empty.tsx +++ b/frontend/src/pages/Users/Empty.tsx @@ -4,15 +4,18 @@ import { intl } from "src/locale"; interface Props { tableInstance: ReactTable; + onNewUser?: () => void; } -export default function Empty({ tableInstance }: Props) { +export default function Empty({ tableInstance, onNewUser }: Props) { return (

{intl.formatMessage({ id: "proxy-hosts.empty" })}

{intl.formatMessage({ id: "empty-subtitle" })}

- +
diff --git a/frontend/src/pages/Users/Table.tsx b/frontend/src/pages/Users/Table.tsx index 6f09892d..e011528c 100644 --- a/frontend/src/pages/Users/Table.tsx +++ b/frontend/src/pages/Users/Table.tsx @@ -12,8 +12,9 @@ interface Props { isFetching?: boolean; currentUserId?: number; onEditUser?: (id: number) => void; + onNewUser?: () => void; } -export default function Table({ data, isFetching, currentUserId, onEditUser }: Props) { +export default function Table({ data, isFetching, currentUserId, onEditUser, onNewUser }: Props) { const columnHelper = createColumnHelper(); const columns = useMemo( () => [ @@ -124,5 +125,10 @@ export default function Table({ data, isFetching, currentUserId, onEditUser }: P enableSortingRemoval: false, }); - return } />; + return ( + } + /> + ); } diff --git a/frontend/src/pages/Users/TableWrapper.tsx b/frontend/src/pages/Users/TableWrapper.tsx index 4ec62e1e..be8eecea 100644 --- a/frontend/src/pages/Users/TableWrapper.tsx +++ b/frontend/src/pages/Users/TableWrapper.tsx @@ -8,7 +8,7 @@ import { UserModal } from "src/modals"; import Table from "./Table"; export default function TableWrapper() { - const [editUserId, setEditUserId] = useState(0); + const [editUserId, setEditUserId] = useState(0 as number | "new"); const { isFetching, isLoading, isError, error, data } = useUsers(["permissions"]); const { data: currentUser } = useUser("me"); @@ -42,7 +42,7 @@ export default function TableWrapper() { autoComplete="off" />
-
@@ -54,6 +54,7 @@ export default function TableWrapper() { isFetching={isFetching} currentUserId={currentUser?.id} onEditUser={(id: number) => setEditUserId(id)} + onNewUser={() => setEditUserId("new")} /> {editUserId ? setEditUserId(0)} /> : null}
diff --git a/frontend/src/pages/Users/index.tsx b/frontend/src/pages/Users/index.tsx index 64e8e6d7..3f20b165 100644 --- a/frontend/src/pages/Users/index.tsx +++ b/frontend/src/pages/Users/index.tsx @@ -3,7 +3,7 @@ import TableWrapper from "./TableWrapper"; const Users = () => { return ( - + ); diff --git a/frontend/yarn.lock b/frontend/yarn.lock index 90085666..dcad569f 100644 --- a/frontend/yarn.lock +++ b/frontend/yarn.lock @@ -1064,6 +1064,11 @@ classnames@^2.3.2, classnames@^2.5.1: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b" integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow== +clsx@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999" + integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA== + convert-source-map@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" @@ -1607,6 +1612,13 @@ react-router@7.8.2: cookie "^1.0.1" set-cookie-parser "^2.6.0" +react-toastify@^11.0.5: + version "11.0.5" + resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313" + integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA== + dependencies: + clsx "^2.1.1" + react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1"