From b1fa0e1c6a39a5b517f247e7b81bfaa763ee8392 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Fri, 16 Aug 2024 00:52:54 +0300 Subject: [PATCH 01/30] refactor: Update progress bar component and provider --- frontend/package-lock.json | 62 +++++++++++++++++- frontend/package.json | 5 +- frontend/src/components/layout/TopBar.tsx | 3 +- frontend/src/units/progress/ProgressBar.tsx | 15 +++++ .../units/progress/ProgressBarProvider.tsx | 65 +++++++++++++++++++ frontend/src/units/progress/hooks.ts | 14 ++++ frontend/src/units/progress/index.ts | 4 ++ 7 files changed, 165 insertions(+), 3 deletions(-) create mode 100644 frontend/src/units/progress/ProgressBar.tsx create mode 100644 frontend/src/units/progress/ProgressBarProvider.tsx create mode 100644 frontend/src/units/progress/hooks.ts create mode 100644 frontend/src/units/progress/index.ts diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a7f1422..a49a5ba 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -19,6 +19,7 @@ "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -26,10 +27,12 @@ "react-lazylog": "^4.5.3", "react-router-dom": "^6.10.0", "react-toastify": "^9.1.3", - "react-world-flags": "^1.6.0" + "react-world-flags": "^1.6.0", + "swr": "^2.2.5" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.33", "@types/react-dom": "^18.2.14", "@types/react-world-flags": "^1.4.5", @@ -4890,6 +4893,12 @@ "dev": true, "peer": true }, + "node_modules/@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -6973,6 +6982,11 @@ "node": ">=0.10.0" } }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "node_modules/cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -19483,6 +19497,18 @@ "boolbase": "~1.0.0" } }, + "node_modules/swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "dependencies": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -20209,6 +20235,14 @@ "requires-port": "^1.0.0" } }, + "node_modules/use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -24773,6 +24807,12 @@ "dev": true, "peer": true }, + "@types/lodash": { + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", + "dev": true + }, "@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", @@ -26391,6 +26431,11 @@ } } }, + "client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + }, "cliui": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", @@ -35637,6 +35682,15 @@ } } }, + "swr": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz", + "integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==", + "requires": { + "client-only": "^0.0.1", + "use-sync-external-store": "^1.2.0" + } + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -36186,6 +36240,12 @@ "requires-port": "^1.0.0" } }, + "use-sync-external-store": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", + "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "requires": {} + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index f3f0c63..2c581ea 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -24,6 +24,7 @@ "i18next": "^22.4.15", "i18next-browser-languagedetector": "^7.0.1", "i18next-http-backend": "^2.2.0", + "lodash": "^4.17.21", "moment": "^2.29.4", "react": "^18.2.0", "react-dom": "^18.2.0", @@ -31,10 +32,12 @@ "react-lazylog": "^4.5.3", "react-router-dom": "^6.10.0", "react-toastify": "^9.1.3", - "react-world-flags": "^1.6.0" + "react-world-flags": "^1.6.0", + "swr": "^2.2.5" }, "devDependencies": { "@babel/plugin-proposal-private-property-in-object": "^7.21.11", + "@types/lodash": "^4.17.7", "@types/react": "^18.2.33", "@types/react-dom": "^18.2.14", "@types/react-world-flags": "^1.4.5", diff --git a/frontend/src/components/layout/TopBar.tsx b/frontend/src/components/layout/TopBar.tsx index 80705a4..4e4378d 100644 --- a/frontend/src/components/layout/TopBar.tsx +++ b/frontend/src/components/layout/TopBar.tsx @@ -7,6 +7,7 @@ import LightDarkToggle from "./LightDarkToggle"; import SensitiveInfoToggle from "./SensitiveInfoToggle"; import { styled } from "@mui/material/styles"; import { drawerWidth } from "./constants"; +import { ProgressBar } from "units/progress"; interface AppBarProps extends MuiAppBarProps { open?: boolean; @@ -64,7 +65,7 @@ const TopBar: React.FC = ({ open, onDrawerOpen }) => { - {/* */} + ); }; diff --git a/frontend/src/units/progress/ProgressBar.tsx b/frontend/src/units/progress/ProgressBar.tsx new file mode 100644 index 0000000..5682420 --- /dev/null +++ b/frontend/src/units/progress/ProgressBar.tsx @@ -0,0 +1,15 @@ +import React from "react"; +import { Box, LinearProgress } from "@mui/material"; +import { useProgressBar } from "./hooks"; + +const ProgressBar: React.FC = () => { + const { progress } = useProgressBar(); + if (!progress) return <>; + return ( + + + + ); +}; + +export default ProgressBar; diff --git a/frontend/src/units/progress/ProgressBarProvider.tsx b/frontend/src/units/progress/ProgressBarProvider.tsx new file mode 100644 index 0000000..dcf445e --- /dev/null +++ b/frontend/src/units/progress/ProgressBarProvider.tsx @@ -0,0 +1,65 @@ +import { isArray } from "lodash"; +import React, { createContext, useCallback, useMemo, useState } from "react"; + +export type ProgressBarUnit = { key: string | null; acting: boolean }; + +type ProgressBarContextPayload = { + progress: boolean; + actions: { + add: (key: string) => void; + remove: (key: string) => void; + observe: (unit?: ProgressBarUnit | ProgressBarUnit[]) => void; + }; +}; + +const initialContext: ProgressBarContextPayload = { + progress: false, + actions: { + add: () => undefined, + remove: () => undefined, + observe: () => undefined + } +}; + +const ProgressBarContext = createContext(initialContext); + +type Props = { children: React.ReactNode }; +const ProgressBarProvider: React.FC = ({ children }) => { + const [network, setNetwork] = useState([]); + const handleAdd = useCallback((key: string) => setNetwork(prev => [...prev, key]), []); + const handleRemove = useCallback((key: string) => setNetwork(prev => prev.filter(z => z !== key)), []); + + const handleObserve = useCallback( + (unit?: ProgressBarUnit | ProgressBarUnit[]) => { + const items: ProgressBarUnit[] = []; + + if (unit) { + if (isArray(unit)) { + items.push(...unit); + } else { + items.push(unit); + } + } + + for (const z of items) { + if (!z.key) continue; + if (z.acting) { + handleAdd(z.key); + } else { + handleRemove(z.key); + } + } + }, + [handleAdd, handleRemove] + ); + + const progress = useMemo(() => network.length > 0, [network.length]); + const payload = useMemo( + () => ({ progress, actions: { add: handleAdd, remove: handleRemove, observe: handleObserve } }), + [progress, handleAdd, handleRemove, handleObserve] + ); + return {children}; +}; + +export { ProgressBarContext }; +export default ProgressBarProvider; diff --git a/frontend/src/units/progress/hooks.ts b/frontend/src/units/progress/hooks.ts new file mode 100644 index 0000000..e4d45e9 --- /dev/null +++ b/frontend/src/units/progress/hooks.ts @@ -0,0 +1,14 @@ +import { useContext } from "react"; +import { ProgressBarContext, ProgressBarUnit } from "./ProgressBarProvider"; + +type UseProgressBarResult = { + progress: boolean; + observe: (unit?: ProgressBarUnit | ProgressBarUnit[]) => void; +}; + +const useProgressBar = (): UseProgressBarResult => { + const context = useContext(ProgressBarContext); + return { progress: context.progress, observe: context.actions.observe }; +}; + +export { useProgressBar }; diff --git a/frontend/src/units/progress/index.ts b/frontend/src/units/progress/index.ts new file mode 100644 index 0000000..c206efa --- /dev/null +++ b/frontend/src/units/progress/index.ts @@ -0,0 +1,4 @@ +import ProgressBarProvider from "./ProgressBarProvider"; +import ProgressBar from "./ProgressBar"; +export { ProgressBarProvider, ProgressBar }; +export * from "./hooks"; From 0a3257dca34bd3bdfd61be121f6f3f0692963dea Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Fri, 16 Aug 2024 00:58:40 +0300 Subject: [PATCH 02/30] move themes in units --- frontend/src/providers/ThemeProvider.js | 2 +- frontend/src/{ => units}/themes/defaults.ts | 0 frontend/src/{ => units}/themes/index.ts | 0 3 files changed, 1 insertion(+), 1 deletion(-) rename frontend/src/{ => units}/themes/defaults.ts (100%) rename frontend/src/{ => units}/themes/index.ts (100%) diff --git a/frontend/src/providers/ThemeProvider.js b/frontend/src/providers/ThemeProvider.js index 740a1f1..f81ecf1 100644 --- a/frontend/src/providers/ThemeProvider.js +++ b/frontend/src/providers/ThemeProvider.js @@ -2,7 +2,7 @@ import React, { useReducer, useMemo, useContext } from "react"; import PropTypes from "prop-types"; import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; import { localStorage } from "@flare/js-utils"; -import { getThemes } from "../themes"; +import { getThemes } from "../units/themes"; const ApplicationThemeContext = React.createContext(); const LOCAL_STORAGE_COLOR_SCHEME_KEY = "network-resurrector-color-scheme"; diff --git a/frontend/src/themes/defaults.ts b/frontend/src/units/themes/defaults.ts similarity index 100% rename from frontend/src/themes/defaults.ts rename to frontend/src/units/themes/defaults.ts diff --git a/frontend/src/themes/index.ts b/frontend/src/units/themes/index.ts similarity index 100% rename from frontend/src/themes/index.ts rename to frontend/src/units/themes/index.ts From 1ba19eb96c15680536f12a937611579099ad3103 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 01:11:02 +0200 Subject: [PATCH 03/30] Refactor handleOpenInNewTab function in AboutSystemContainer.js --- .../src/features/about/system/AboutSystemContainer.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/features/about/system/AboutSystemContainer.js b/frontend/src/features/about/system/AboutSystemContainer.js index 3396f98..dd1e75d 100644 --- a/frontend/src/features/about/system/AboutSystemContainer.js +++ b/frontend/src/features/about/system/AboutSystemContainer.js @@ -13,12 +13,12 @@ const styles = { } }; -const AboutSystemContainer = () => { - const handleOpenInNewTab = url => event => { - window.open(url, "_blank"); - event.preventDefault(); - }; +const handleOpenInNewTab = url => event => { + window.open(url, "_blank"); + event.preventDefault(); +}; +const AboutSystemContainer = () => { return ( From f8d0d7c486bed30c51532ca89d5f04ff52f31bfc Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 01:12:01 +0200 Subject: [PATCH 04/30] Refactor handleOpenInNewTab function in AboutSystemContainer.js --- frontend/src/features/about/system/AboutSystemComponent.js | 6 +++--- frontend/src/features/about/system/AboutSystemContainer.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/features/about/system/AboutSystemComponent.js b/frontend/src/features/about/system/AboutSystemComponent.js index 2108211..e2b4909 100644 --- a/frontend/src/features/about/system/AboutSystemComponent.js +++ b/frontend/src/features/about/system/AboutSystemComponent.js @@ -38,7 +38,7 @@ const buttons = [ } ]; -const AboutSystemComponent = ({ handleOpenInNewTab }) => { +const AboutSystemComponent = ({ onOpenInNewTab }) => { const { t } = useTranslation(); const bullet = ; @@ -80,7 +80,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => { size="small" color="primary" startIcon={} - onClick={handleOpenInNewTab(button.url)} + onClick={onOpenInNewTab(button.url)} > {t(button.code)} @@ -91,7 +91,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => { }; AboutSystemComponent.propTypes = { - handleOpenInNewTab: PropTypes.func.isRequired + onOpenInNewTab: PropTypes.func.isRequired }; export default AboutSystemComponent; diff --git a/frontend/src/features/about/system/AboutSystemContainer.js b/frontend/src/features/about/system/AboutSystemContainer.js index dd1e75d..1b72003 100644 --- a/frontend/src/features/about/system/AboutSystemContainer.js +++ b/frontend/src/features/about/system/AboutSystemContainer.js @@ -21,7 +21,7 @@ const handleOpenInNewTab = url => event => { const AboutSystemContainer = () => { return ( - + From c5ba810605368673be3df16b7ebb7a9585e1c020 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 01:52:39 +0200 Subject: [PATCH 05/30] blip toast proxy --- .../src/components/layout/ProfileButton.js | 7 ++-- .../about/system/SystemVersionContainer.js | 18 --------- .../about/system/SystemVersionContainer.tsx | 26 ++++++++++++ .../login/components/LoginContainer.js | 7 ++-- .../machines/components/MachineContainer.js | 10 ++--- .../components/common/WakeComponent.js | 9 ++--- .../settings/system/CacheSettingsContainer.js | 4 +- frontend/src/hooks/index.js | 3 +- frontend/src/hooks/useClipboard.js | 7 ++-- frontend/src/hooks/useToast.js | 5 --- frontend/src/utils/api.js | 21 +++++++--- frontend/src/utils/{toast.js => blip.js} | 4 +- frontend/src/utils/index.js | 3 -- frontend/src/utils/index.ts | 4 ++ frontend/src/utils/swr.ts | 40 +++++++++++++++++++ 15 files changed, 108 insertions(+), 60 deletions(-) delete mode 100644 frontend/src/features/about/system/SystemVersionContainer.js create mode 100644 frontend/src/features/about/system/SystemVersionContainer.tsx delete mode 100644 frontend/src/hooks/useToast.js rename frontend/src/utils/{toast.js => blip.js} (75%) delete mode 100644 frontend/src/utils/index.js create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/utils/swr.ts diff --git a/frontend/src/components/layout/ProfileButton.js b/frontend/src/components/layout/ProfileButton.js index 5843a57..5e92221 100644 --- a/frontend/src/components/layout/ProfileButton.js +++ b/frontend/src/components/layout/ProfileButton.js @@ -6,17 +6,16 @@ import AccountBoxIcon from "@mui/icons-material/AccountBox"; import SettingsIcon from "@mui/icons-material/Settings"; import { useNavigate } from "react-router-dom"; import { useTuitioClient } from "@flare/tuitio-client-react"; -import { useToast } from "../../hooks"; +import { blip } from "../../utils"; import { useTranslation } from "react-i18next"; const ProfileButton = () => { const navigate = useNavigate(); - const { error } = useToast(); const { t } = useTranslation(); const { logout } = useTuitioClient({ - onLogoutFailed: errorMessage => error(errorMessage), - onLogoutError: err => error(err.message) + onLogoutFailed: errorMessage => blip.error(errorMessage), + onLogoutError: err => blip.error(err.message) }); const [anchorEl, setAnchorEl] = useState(null); diff --git a/frontend/src/features/about/system/SystemVersionContainer.js b/frontend/src/features/about/system/SystemVersionContainer.js deleted file mode 100644 index c4731fa..0000000 --- a/frontend/src/features/about/system/SystemVersionContainer.js +++ /dev/null @@ -1,18 +0,0 @@ -import React, { useState, useEffect } from "react"; -import SystemVersionComponent from "./SystemVersionComponent"; -import { routes, get } from "../../../utils/api"; - -const SystemVersionContainer = () => { - const [state, setState] = useState({ data: {}, loaded: false }); - - useEffect(() => { - if (state.loaded) return; - get(routes.systemVersion, { - onCompleted: data => setState({ data, loaded: true }) - }); - }, [state.loaded]); - - return <>{state.loaded && }; -}; - -export default SystemVersionContainer; diff --git a/frontend/src/features/about/system/SystemVersionContainer.tsx b/frontend/src/features/about/system/SystemVersionContainer.tsx new file mode 100644 index 0000000..46aaa75 --- /dev/null +++ b/frontend/src/features/about/system/SystemVersionContainer.tsx @@ -0,0 +1,26 @@ +import React, { useState, useEffect } from "react"; +import SystemVersionComponent from "./SystemVersionComponent"; +import { routes, get, endpoints } from "../../../utils/api"; +import useSWR from "swr"; +import { fetcher } from "utils/swr"; +import { blip } from "utils"; + +const SystemVersionContainer: React.FC = () => { + const { data: namespaces } = useSWR(endpoints.system.version, fetcher, { + revalidateOnFocus: false, + onError: err => blip.error(err.message) + }); + + const [state, setState] = useState({ data: {}, loaded: false }); + + useEffect(() => { + if (state.loaded) return; + get(routes.systemVersion, { + onCompleted: (data: any) => setState({ data, loaded: true }) + }); + }, [state.loaded]); + + return <>{state.loaded && }; +}; + +export default SystemVersionContainer; diff --git a/frontend/src/features/login/components/LoginContainer.js b/frontend/src/features/login/components/LoginContainer.js index c3f23af..150790d 100644 --- a/frontend/src/features/login/components/LoginContainer.js +++ b/frontend/src/features/login/components/LoginContainer.js @@ -1,6 +1,6 @@ import React, { useState } from "react"; import LoginCard from "./LoginCard"; -import { useToast } from "../../../hooks"; +import { blip } from "../../../utils"; import { useTranslation } from "react-i18next"; import { useTuitioClient } from "@flare/tuitio-client-react"; @@ -10,11 +10,10 @@ const LoginContainer = () => { password: "" }); - const { error } = useToast(); const { t } = useTranslation(); const { login } = useTuitioClient({ - onLoginFailed: () => error(t("Login.IncorrectCredentials")), - onLoginError: err => error(err.message) + onLoginFailed: () => blip.error(t("Login.IncorrectCredentials")), + onLoginError: err => blip.error(err.message) }); const handleChange = prop => event => { diff --git a/frontend/src/features/machines/components/MachineContainer.js b/frontend/src/features/machines/components/MachineContainer.js index e1a3236..54fcf4e 100644 --- a/frontend/src/features/machines/components/MachineContainer.js +++ b/frontend/src/features/machines/components/MachineContainer.js @@ -3,15 +3,13 @@ import PropTypes from "prop-types"; import MachineTableRow from "./MachineTableRow"; import MachineAccordion from "./MachineAccordion"; import { ViewModes } from "./ViewModeSelection"; -import { useToast } from "../../../hooks"; +import { blip } from "../../../utils"; import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { routes, post } from "../../../utils/api"; const MachineContainer = ({ machine, viewMode }) => { const [logs, setLogs] = useState([]); - - const { success, error } = useToast(); const { t } = useTranslation(); const addLog = useCallback( @@ -25,12 +23,12 @@ const MachineContainer = ({ machine, viewMode }) => { response => { addLog(`Success: ${response.success}. Status: ${response.status}`); if (response.success) { - success(response.status); + blip.success(response.status); } else { - error(response.status); + blip.error(response.status); } }, - [error, success, addLog] + [addLog] ); const pingMachine = useCallback( diff --git a/frontend/src/features/machines/components/common/WakeComponent.js b/frontend/src/features/machines/components/common/WakeComponent.js index 6f7b0bc..69a9d6b 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.js +++ b/frontend/src/features/machines/components/common/WakeComponent.js @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { IconButton, Tooltip } from "@mui/material"; import { PowerSettingsNew } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -import { useToast } from "../../../../hooks"; +import { blip } from "../../../../utils"; import { msToMinAndSec } from "../../../../utils/time"; import { routes, post } from "../../../../utils/api"; @@ -16,7 +16,6 @@ const WakeComponent = ({ machine, addLog, disabled }) => { const [trigger, setTrigger] = useState(false); const { t } = useTranslation(); - const { success, error } = useToast(); const pingInterval = process.env.REACT_APP_MACHINE_PING_INTERVAL || defaultPingInterval; const startingTime = process.env.REACT_APP_MACHINE_STARTING_TIME || defaultStartingTime; @@ -40,7 +39,7 @@ const WakeComponent = ({ machine, addLog, disabled }) => { setState(prev => ({ ...prev, on: result.success })); log(`[Wake]: Success: ${result.success}. Status: ${result.status}`); if (result.success) { - success(result.status); + blip.success(result.status); //retrigger log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`); @@ -48,12 +47,12 @@ const WakeComponent = ({ machine, addLog, disabled }) => { setTrigger(prev => !prev); }, startingTime); } else { - error(result.status); + blip.error(result.status); } } } ); - }, [log, success, error, startingTime, machine.machineId]); + }, [log, startingTime, machine.machineId]); const pingInLoop = useCallback(async () => { if (disabled) return; diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.js b/frontend/src/features/settings/system/CacheSettingsContainer.js index 4283fcd..3e94592 100644 --- a/frontend/src/features/settings/system/CacheSettingsContainer.js +++ b/frontend/src/features/settings/system/CacheSettingsContainer.js @@ -2,7 +2,7 @@ import React, { useCallback } from "react"; import CacheSettingsComponent from "./CacheSettingsComponent"; import { useTranslation } from "react-i18next"; import { routes, post } from "utils/api"; -import { info } from "utils/toast"; +import { blip } from "utils"; const CacheSettingsContainer = () => { const { t } = useTranslation(); @@ -11,7 +11,7 @@ const CacheSettingsContainer = () => { routes.resetCache, {}, { - onCompleted: () => info(t("Settings.Cache.ResetInfo")) + onCompleted: () => blip.info(t("Settings.Cache.ResetInfo")) } ); }, [t]); diff --git a/frontend/src/hooks/index.js b/frontend/src/hooks/index.js index 44ec796..f28fc19 100644 --- a/frontend/src/hooks/index.js +++ b/frontend/src/hooks/index.js @@ -1,6 +1,5 @@ -import { useToast } from "./useToast"; import { useSensitiveInfo } from "../providers/SensitiveInfoProvider"; import { usePermissions } from "../providers/UserPermissionsProvider"; import { useClipboard } from "./useClipboard"; -export { useToast, useSensitiveInfo, usePermissions, useClipboard }; +export { useSensitiveInfo, usePermissions, useClipboard }; diff --git a/frontend/src/hooks/useClipboard.js b/frontend/src/hooks/useClipboard.js index b2fd329..956f5e2 100644 --- a/frontend/src/hooks/useClipboard.js +++ b/frontend/src/hooks/useClipboard.js @@ -1,16 +1,15 @@ import { useCallback } from "react"; import { useTranslation } from "react-i18next"; -import { useToast } from "./useToast"; +import { blip } from "../utils"; const useClipboard = () => { const { t } = useTranslation(); - const { info } = useToast(); const copy = useCallback( url => () => { navigator.clipboard.writeText(url); - info(t("Generic.CopiedToClipboard")); + blip.info(t("Generic.CopiedToClipboard")); }, - [info, t] + [t] ); return { copy }; }; diff --git a/frontend/src/hooks/useToast.js b/frontend/src/hooks/useToast.js deleted file mode 100644 index 2255151..0000000 --- a/frontend/src/hooks/useToast.js +++ /dev/null @@ -1,5 +0,0 @@ -import { info, success, warning, error, dark } from "utils/toast"; - -export const useToast = () => { - return { info, success, warning, error, dark }; -}; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 08dd64b..6b9e85f 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -2,10 +2,12 @@ import * as axios from "../utils/axios"; import { toast } from "react-toastify"; import env from "../utils/env"; -const networkRoute = `${env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/network`; -const systemRoute = `${env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/system`; -const powerActionsRoute = `${env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/resurrector`; -const securityRoute = `${env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/security`; +const apiHost = env.REACT_APP_NETWORK_RESURRECTOR_API_URL; + +const networkRoute = `${apiHost}/network`; +const systemRoute = `${apiHost}/system`; +const powerActionsRoute = `${apiHost}/resurrector`; +const securityRoute = `${apiHost}/security`; const routes = { permissions: `${securityRoute}/permissions`, @@ -16,7 +18,12 @@ const routes = { wakeMachine: `${powerActionsRoute}/wake`, pingMachine: `${powerActionsRoute}/ping`, shutdownMachine: `${powerActionsRoute}/shutdown`, - restartMachine: `${powerActionsRoute}/restart` + restartMachine: `${powerActionsRoute}/restart`, + system: { + version: `${systemRoute}/version`, + releaseNotes: `${systemRoute}/release-notes`, + resetCache: `${systemRoute}/reset-cache` + } }; const handleError = err => { @@ -64,4 +71,6 @@ const post = (route, data, options) => { return promise; }; -export { routes, get, post }; +const endpoints = routes; + +export { routes, get, post, endpoints }; diff --git a/frontend/src/utils/toast.js b/frontend/src/utils/blip.js similarity index 75% rename from frontend/src/utils/toast.js rename to frontend/src/utils/blip.js index 4045135..82c7f0e 100644 --- a/frontend/src/utils/toast.js +++ b/frontend/src/utils/blip.js @@ -6,4 +6,6 @@ const warning = message => toast.warning(message); const error = message => toast.error(message); const dark = message => toast.dark(message); -export { info, success, warning, error, dark }; +const blip = { info, success, warning, error, dark }; +export { blip }; +export default blip; diff --git a/frontend/src/utils/index.js b/frontend/src/utils/index.js deleted file mode 100644 index 0dbd747..0000000 --- a/frontend/src/utils/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import { getRandomElement } from "./random"; - -export { getRandomElement }; diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..2115555 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,4 @@ +import { getRandomElement } from "./random"; +import blip from "./blip"; + +export { getRandomElement, blip }; diff --git a/frontend/src/utils/swr.ts b/frontend/src/utils/swr.ts new file mode 100644 index 0000000..2c718f3 --- /dev/null +++ b/frontend/src/utils/swr.ts @@ -0,0 +1,40 @@ +import i18next from "i18next"; +import { acquire as fetchTuitioData } from "@flare/tuitio-client"; + +const getHeaders = (): HeadersInit => { + const { token } = fetchTuitioData(); + const language = i18next.language; + + const headers: HeadersInit = { + "Content-Type": "application/json" + }; + + if (token) { + headers.Authorization = `Tuitio ${token}`; + } + + if (language) { + headers["Accept-Language"] = language; + } + + return headers; +}; + +const fetcher = (url: string) => fetch(url, { method: "GET", headers: getHeaders() }).then(res => res.json()); + +async function mutationFetcher(url: string, { arg }: { arg: Command }) { + return fetch(url, { + method: "POST", + headers: getHeaders(), + body: JSON.stringify(arg) + }).then(res => res.json()); +} + +// async function deleteFetcher(url: string, { arg }: { arg: Command }) { +// return fetch(combine(url, arg as number), { +// method: "DELETE", +// headers: getHeaders() +// }).then(res => res.json()); +// } + +export { fetcher, mutationFetcher }; From 74a176b9aac13446dce1bf148dd3f9e0f7535b5c Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 02:05:32 +0200 Subject: [PATCH 06/30] Refactor SystemVersionContainer and add dtos to types --- .../about/system/SystemVersionContainer.tsx | 19 ++++++------------- frontend/src/types/dtos.ts | 9 +++++++++ frontend/src/types/index.ts | 3 ++- frontend/src/utils/api.js | 1 - 4 files changed, 17 insertions(+), 15 deletions(-) create mode 100644 frontend/src/types/dtos.ts diff --git a/frontend/src/features/about/system/SystemVersionContainer.tsx b/frontend/src/features/about/system/SystemVersionContainer.tsx index 46aaa75..8c3204c 100644 --- a/frontend/src/features/about/system/SystemVersionContainer.tsx +++ b/frontend/src/features/about/system/SystemVersionContainer.tsx @@ -1,26 +1,19 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import SystemVersionComponent from "./SystemVersionComponent"; -import { routes, get, endpoints } from "../../../utils/api"; +import { endpoints } from "../../../utils/api"; import useSWR from "swr"; import { fetcher } from "utils/swr"; import { blip } from "utils"; +import { dtos } from "types"; const SystemVersionContainer: React.FC = () => { - const { data: namespaces } = useSWR(endpoints.system.version, fetcher, { + const { data, isLoading } = useSWR(endpoints.system.version, fetcher, { revalidateOnFocus: false, onError: err => blip.error(err.message) }); - const [state, setState] = useState({ data: {}, loaded: false }); - - useEffect(() => { - if (state.loaded) return; - get(routes.systemVersion, { - onCompleted: (data: any) => setState({ data, loaded: true }) - }); - }, [state.loaded]); - - return <>{state.loaded && }; + if (isLoading || !data) return null; + return ; }; export default SystemVersionContainer; diff --git a/frontend/src/types/dtos.ts b/frontend/src/types/dtos.ts new file mode 100644 index 0000000..6b90b65 --- /dev/null +++ b/frontend/src/types/dtos.ts @@ -0,0 +1,9 @@ +export type SystemVersionElement = { + version: string; + lastReleaseDate: string; +}; + +export type SystemVersion = { + api: SystemVersionElement; + server: SystemVersionElement; +}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index b471b7f..05ad3f9 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,5 +1,6 @@ import * as models from "./models"; +import * as dtos from "./dtos"; export * from "./models"; -export { models }; +export { models, dtos }; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 6b9e85f..ded593a 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -11,7 +11,6 @@ const securityRoute = `${apiHost}/security`; const routes = { permissions: `${securityRoute}/permissions`, - systemVersion: `${systemRoute}/version`, releaseNotes: `${systemRoute}/release-notes`, resetCache: `${systemRoute}/reset-cache`, machines: `${networkRoute}/machines`, From 97b22c70476549424e6282d18bd91a726cc3ffd8 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 02:16:25 +0200 Subject: [PATCH 07/30] Refactor SystemVersionComponent and add dtos to types --- ...omponent.js => SystemVersionComponent.tsx} | 31 ++++++++++--------- 1 file changed, 17 insertions(+), 14 deletions(-) rename frontend/src/features/about/system/{SystemVersionComponent.js => SystemVersionComponent.tsx} (84%) diff --git a/frontend/src/features/about/system/SystemVersionComponent.js b/frontend/src/features/about/system/SystemVersionComponent.tsx similarity index 84% rename from frontend/src/features/about/system/SystemVersionComponent.js rename to frontend/src/features/about/system/SystemVersionComponent.tsx index 2b49f80..8465ec1 100644 --- a/frontend/src/features/about/system/SystemVersionComponent.js +++ b/frontend/src/features/about/system/SystemVersionComponent.tsx @@ -1,6 +1,6 @@ import React, { useMemo, useEffect, useState } from "react"; import PropTypes from "prop-types"; -import { List, ListItem, ListItemText, ListItemAvatar } from "@mui/material"; +import { List, ListItem, ListItemText, ListItemAvatar, Theme } from "@mui/material"; import Avatar from "@mui/material/Avatar"; import WebAssetIcon from "@mui/icons-material/WebAsset"; import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; @@ -9,8 +9,9 @@ import { useTranslation } from "react-i18next"; import packageData from "../../../../package.json"; import Paper from "@mui/material/Paper"; import { useTheme } from "@mui/material/styles"; +import { dtos } from "types"; -const getStyles = theme => ({ +const getStyles = (theme: Theme) => ({ horizontally: { display: "flex", flexDirection: "row", @@ -28,25 +29,31 @@ const getStyles = theme => ({ } }); -const SystemVersionComponent = ({ data }) => { +type Props = { + data: dtos.SystemVersion; +}; + +const SystemVersionComponent: React.FC = ({ data }) => { const { t } = useTranslation(); const theme = useTheme(); - const styles = getStyles(theme); + const styles = useMemo(() => getStyles(theme), [theme]); const [listClass, setListClass] = useState(styles.horizontally); useEffect(() => { const mediaQuery = window.matchMedia("(max-width: 800px)"); - function handleMatches(event) { - const cssClass = event.matches ? styles.vertical : styles.horizontally; + const handleMatches = (event: MediaQueryListEvent) => { + const cssClass: any = event.matches ? styles.vertical : styles.horizontally; setListClass(cssClass); - } + }; - handleMatches(mediaQuery); - mediaQuery.addListener(handleMatches); + mediaQuery.addEventListener("change", handleMatches); + + // Initial check + handleMatches(mediaQuery as unknown as MediaQueryListEvent); return () => { - mediaQuery.removeListener(handleMatches); + mediaQuery.removeEventListener("change", handleMatches); }; }, [styles.horizontally, styles.vertical]); @@ -141,8 +148,4 @@ const SystemVersionComponent = ({ data }) => { ); }; -SystemVersionComponent.propTypes = { - data: PropTypes.object.isRequired -}; - export default SystemVersionComponent; From 5f234938b8d02bc412e8b6bd85fbc64e12e0a98c Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 02:19:57 +0200 Subject: [PATCH 08/30] Refactor SystemVersionComponent to remove unused imports and update styling --- .../about/system/SystemVersionComponent.tsx | 31 ++++++------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/frontend/src/features/about/system/SystemVersionComponent.tsx b/frontend/src/features/about/system/SystemVersionComponent.tsx index 8465ec1..39fa583 100644 --- a/frontend/src/features/about/system/SystemVersionComponent.tsx +++ b/frontend/src/features/about/system/SystemVersionComponent.tsx @@ -1,5 +1,4 @@ -import React, { useMemo, useEffect, useState } from "react"; -import PropTypes from "prop-types"; +import React, { useMemo } from "react"; import { List, ListItem, ListItemText, ListItemAvatar, Theme } from "@mui/material"; import Avatar from "@mui/material/Avatar"; import WebAssetIcon from "@mui/icons-material/WebAsset"; @@ -37,25 +36,6 @@ const SystemVersionComponent: React.FC = ({ data }) => { const { t } = useTranslation(); const theme = useTheme(); const styles = useMemo(() => getStyles(theme), [theme]); - const [listClass, setListClass] = useState(styles.horizontally); - - useEffect(() => { - const mediaQuery = window.matchMedia("(max-width: 800px)"); - - const handleMatches = (event: MediaQueryListEvent) => { - const cssClass: any = event.matches ? styles.vertical : styles.horizontally; - setListClass(cssClass); - }; - - mediaQuery.addEventListener("change", handleMatches); - - // Initial check - handleMatches(mediaQuery as unknown as MediaQueryListEvent); - - return () => { - mediaQuery.removeEventListener("change", handleMatches); - }; - }, [styles.horizontally, styles.vertical]); const lastReleaseDate = useMemo(() => { const format = "DD-MM-YYYY HH:mm:ss"; @@ -85,7 +65,14 @@ const SystemVersionComponent: React.FC = ({ data }) => { return ( - + From 9389058b1f5378bb4cc589810c25c49cd88e8939 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 02:27:30 +0200 Subject: [PATCH 09/30] Refactor SystemVersionComponent to update styling and add dtos to types --- .../about/system/SystemVersionComponent.tsx | 52 ++++++++----------- 1 file changed, 21 insertions(+), 31 deletions(-) diff --git a/frontend/src/features/about/system/SystemVersionComponent.tsx b/frontend/src/features/about/system/SystemVersionComponent.tsx index 39fa583..a9d317f 100644 --- a/frontend/src/features/about/system/SystemVersionComponent.tsx +++ b/frontend/src/features/about/system/SystemVersionComponent.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { List, ListItem, ListItemText, ListItemAvatar, Theme } from "@mui/material"; +import { List, ListItem, ListItemText, ListItemAvatar, Theme, styled } from "@mui/material"; import Avatar from "@mui/material/Avatar"; import WebAssetIcon from "@mui/icons-material/WebAsset"; import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; @@ -10,23 +10,14 @@ import Paper from "@mui/material/Paper"; import { useTheme } from "@mui/material/styles"; import { dtos } from "types"; -const getStyles = (theme: Theme) => ({ - horizontally: { - display: "flex", - flexDirection: "row", - padding: 0 - }, - vertical: { - width: "100%" - }, - value: { - fontSize: "0.9rem", - fontWeight: theme.typography.fontWeightMedium - }, - versionAvatar: { - backgroundColor: theme.palette.secondary.main - } -}); +const VersionAvatar = styled(Avatar)(({ theme }) => ({ + backgroundColor: theme.palette.secondary.main +})); + +const VersionLabel = styled("span")(({ theme }) => ({ + fontSize: "0.9rem", + fontWeight: theme.typography.fontWeightMedium +})); type Props = { data: dtos.SystemVersion; @@ -35,7 +26,6 @@ type Props = { const SystemVersionComponent: React.FC = ({ data }) => { const { t } = useTranslation(); const theme = useTheme(); - const styles = useMemo(() => getStyles(theme), [theme]); const lastReleaseDate = useMemo(() => { const format = "DD-MM-YYYY HH:mm:ss"; @@ -75,17 +65,17 @@ const SystemVersionComponent: React.FC = ({ data }) => { > - + - + + {t("About.System.Version.Server", { version: data.server.version })} - + } secondary={t("About.System.Version.LastReleaseDate", { date: lastReleaseDate.server @@ -94,17 +84,17 @@ const SystemVersionComponent: React.FC = ({ data }) => { - + - + + {t("About.System.Version.Api", { version: data.api.version })} - + } secondary={t("About.System.Version.LastReleaseDate", { date: lastReleaseDate.api @@ -113,17 +103,17 @@ const SystemVersionComponent: React.FC = ({ data }) => { - + - + + {t("About.System.Version.Frontend", { version: process.env.APP_VERSION ?? packageData.version })} - + } secondary={t("About.System.Version.LastReleaseDate", { date: lastReleaseDate.frontend From 19415f312a5cffa1e527f6a31d526c294a58ca9d Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 23:21:37 +0200 Subject: [PATCH 10/30] Refactor ReleaseNotesContainer to TypeScript, implement SWR for data fetching, and update release note structure --- .../releaseNotes/ReleaseNotesContainer.js | 35 ------------------- .../releaseNotes/ReleaseNotesContainer.tsx | 32 +++++++++++++++++ frontend/src/types/dtos.ts | 6 ++++ frontend/src/utils/api.js | 1 - 4 files changed, 38 insertions(+), 36 deletions(-) delete mode 100644 frontend/src/features/about/releaseNotes/ReleaseNotesContainer.js create mode 100644 frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx diff --git a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.js b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.js deleted file mode 100644 index 39e849b..0000000 --- a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useState, useEffect } from "react"; -import PropTypes from "prop-types"; -import ReleaseNotesList from "./ReleaseNotesList"; -import TimelineComponent from "../timeline/TimelineComponent"; -import { routes, get } from "../../../utils/api"; - -const sort = releases => releases.sort((a, b) => new Date(b.date) - new Date(a.date)); - -const ReleaseNotesContainer = ({ view }) => { - const [state, setState] = useState({ data: [], loaded: false }); - - useEffect(() => { - if (state.loaded) return; - get(routes.releaseNotes, { - onCompleted: data => setState({ data, loaded: true }) - }); - }, [state.loaded]); - - return ( - <> - {state.loaded && - (view === "timeline" ? ( - - ) : ( - - ))} - - ); -}; - -ReleaseNotesContainer.propTypes = { - view: PropTypes.string -}; - -export default ReleaseNotesContainer; diff --git a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx new file mode 100644 index 0000000..62c2efc --- /dev/null +++ b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx @@ -0,0 +1,32 @@ +import React, { useState, useEffect } from "react"; +import ReleaseNotesList from "./ReleaseNotesList"; +import TimelineComponent from "../timeline/TimelineComponent"; +import { routes, get, endpoints } from "../../../utils/api"; +import useSWR from "swr"; +import { fetcher } from "utils/swr"; +import { blip } from "utils"; +import { dtos } from "types"; + +const sort = (releases: dtos.ReleaseNote[]) => + releases.sort((a, b) => new Date(b.date).getTime() - new Date(a.date).getTime()); + +type Props = { + view: string; +}; + +const ReleaseNotesContainer: React.FC = ({ view }) => { + const { data, isLoading } = useSWR(endpoints.system.releaseNotes, fetcher, { + revalidateOnFocus: false, + onError: err => blip.error(err.message) + }); + + if (isLoading || !data) return null; + + return ( + <> + {view === "timeline" ? : } + + ); +}; + +export default ReleaseNotesContainer; diff --git a/frontend/src/types/dtos.ts b/frontend/src/types/dtos.ts index 6b90b65..95679d4 100644 --- a/frontend/src/types/dtos.ts +++ b/frontend/src/types/dtos.ts @@ -7,3 +7,9 @@ export type SystemVersion = { api: SystemVersionElement; server: SystemVersionElement; }; + +export type ReleaseNote = { + version: string; + date: string; + notes: string[]; +}; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index ded593a..035df60 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -11,7 +11,6 @@ const securityRoute = `${apiHost}/security`; const routes = { permissions: `${securityRoute}/permissions`, - releaseNotes: `${systemRoute}/release-notes`, resetCache: `${systemRoute}/reset-cache`, machines: `${networkRoute}/machines`, wakeMachine: `${powerActionsRoute}/wake`, From 8af7b64a608ea016665426dc93318c6cc82721e2 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sun, 10 Nov 2024 23:25:14 +0200 Subject: [PATCH 11/30] Add version display to TimelineComponent in release notes --- frontend/src/features/about/timeline/TimelineComponent.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/frontend/src/features/about/timeline/TimelineComponent.js b/frontend/src/features/about/timeline/TimelineComponent.js index d8eb446..2868018 100644 --- a/frontend/src/features/about/timeline/TimelineComponent.js +++ b/frontend/src/features/about/timeline/TimelineComponent.js @@ -74,6 +74,9 @@ const TimelineComponent = ({ releases }) => { date: { value: release.date, format: "DD-MM-YYYY HH:mm" } })} + + {t("About.ReleaseNotes.Version")}: {release.version} + From ca388cb639f1113c13109eeb589e5a3068742b10 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 11 Nov 2024 01:29:07 +0200 Subject: [PATCH 12/30] Refactor cache reset functionality: replace old CacheSettingsContainer with TypeScript version, update SystemController to handle reset cache, and enhance SWR mutation fetcher. --- .../Controllers/SystemController.cs | 3 ++- .../settings/system/CacheSettingsContainer.js | 22 ---------------- .../system/CacheSettingsContainer.tsx | 25 +++++++++++++++++++ frontend/src/utils/swr.ts | 8 ++++-- 4 files changed, 33 insertions(+), 25 deletions(-) delete mode 100644 frontend/src/features/settings/system/CacheSettingsContainer.js create mode 100644 frontend/src/features/settings/system/CacheSettingsContainer.tsx diff --git a/backend/src/api/NetworkResurrector.Api/Controllers/SystemController.cs b/backend/src/api/NetworkResurrector.Api/Controllers/SystemController.cs index 2964d54..d793932 100644 --- a/backend/src/api/NetworkResurrector.Api/Controllers/SystemController.cs +++ b/backend/src/api/NetworkResurrector.Api/Controllers/SystemController.cs @@ -44,8 +44,9 @@ namespace NetworkResurrector.Api.Controllers [HttpPost("reset-cache")] [Authorize(Policy = Policies.SystemAdministration)] - public async Task WakeMachine([FromBody] ResetCache resetCache) + public async Task ResetCache() { + var resetCache = new ResetCache(); var result = await _mediator.Send(resetCache); return Ok(result); } diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.js b/frontend/src/features/settings/system/CacheSettingsContainer.js deleted file mode 100644 index 3e94592..0000000 --- a/frontend/src/features/settings/system/CacheSettingsContainer.js +++ /dev/null @@ -1,22 +0,0 @@ -import React, { useCallback } from "react"; -import CacheSettingsComponent from "./CacheSettingsComponent"; -import { useTranslation } from "react-i18next"; -import { routes, post } from "utils/api"; -import { blip } from "utils"; - -const CacheSettingsContainer = () => { - const { t } = useTranslation(); - const handleResetCache = useCallback(async () => { - await post( - routes.resetCache, - {}, - { - onCompleted: () => blip.info(t("Settings.Cache.ResetInfo")) - } - ); - }, [t]); - - return ; -}; - -export default CacheSettingsContainer; diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.tsx b/frontend/src/features/settings/system/CacheSettingsContainer.tsx new file mode 100644 index 0000000..8ec88a8 --- /dev/null +++ b/frontend/src/features/settings/system/CacheSettingsContainer.tsx @@ -0,0 +1,25 @@ +import React, { useCallback } from "react"; +import CacheSettingsComponent from "./CacheSettingsComponent"; +import { useTranslation } from "react-i18next"; +import { endpoints } from "utils/api"; +import { blip } from "utils"; +import useSWRMutation from "swr/mutation"; +import { Key } from "swr"; +import { mutationFetcher } from "utils/swr"; + +const CacheSettingsContainer: React.FC = () => { + const { t } = useTranslation(); + + const { trigger } = useSWRMutation(endpoints.system.resetCache, mutationFetcher, { + onError: err => { + blip.error(err.message); + }, + onSuccess: () => blip.info(t("Settings.Cache.ResetInfo")) + }); + + const handleResetCache = useCallback(() => trigger(), [t]); + + return ; +}; + +export default CacheSettingsContainer; diff --git a/frontend/src/utils/swr.ts b/frontend/src/utils/swr.ts index 2c718f3..a407355 100644 --- a/frontend/src/utils/swr.ts +++ b/frontend/src/utils/swr.ts @@ -23,10 +23,14 @@ const getHeaders = (): HeadersInit => { const fetcher = (url: string) => fetch(url, { method: "GET", headers: getHeaders() }).then(res => res.json()); async function mutationFetcher(url: string, { arg }: { arg: Command }) { + const hasBody = arg !== undefined && arg !== null; + const headers = getHeaders(); + const body = hasBody ? JSON.stringify(arg) : undefined; + return fetch(url, { method: "POST", - headers: getHeaders(), - body: JSON.stringify(arg) + headers, + body }).then(res => res.json()); } From 0468af03bea570c1b0b986b2605929da6a8bf910 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 11 Nov 2024 01:48:15 +0200 Subject: [PATCH 13/30] Refactor UserPermissionsProvider to TypeScript, remove unused permissions API routes, and enhance cache reset functionality --- .../releaseNotes/ReleaseNotesContainer.tsx | 4 +- .../about/system/SystemVersionComponent.tsx | 4 +- .../system/CacheSettingsContainer.tsx | 2 +- ...rovider.js => UserPermissionsProvider.tsx} | 45 +++++++++++-------- frontend/src/types/dtos.ts | 4 ++ frontend/src/utils/api.js | 5 ++- 6 files changed, 38 insertions(+), 26 deletions(-) rename frontend/src/providers/{UserPermissionsProvider.js => UserPermissionsProvider.tsx} (60%) diff --git a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx index 62c2efc..2669bc9 100644 --- a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx +++ b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx @@ -1,7 +1,7 @@ -import React, { useState, useEffect } from "react"; +import React from "react"; import ReleaseNotesList from "./ReleaseNotesList"; import TimelineComponent from "../timeline/TimelineComponent"; -import { routes, get, endpoints } from "../../../utils/api"; +import { endpoints } from "../../../utils/api"; import useSWR from "swr"; import { fetcher } from "utils/swr"; import { blip } from "utils"; diff --git a/frontend/src/features/about/system/SystemVersionComponent.tsx b/frontend/src/features/about/system/SystemVersionComponent.tsx index a9d317f..853bc73 100644 --- a/frontend/src/features/about/system/SystemVersionComponent.tsx +++ b/frontend/src/features/about/system/SystemVersionComponent.tsx @@ -1,5 +1,5 @@ import React, { useMemo } from "react"; -import { List, ListItem, ListItemText, ListItemAvatar, Theme, styled } from "@mui/material"; +import { List, ListItem, ListItemText, ListItemAvatar, styled } from "@mui/material"; import Avatar from "@mui/material/Avatar"; import WebAssetIcon from "@mui/icons-material/WebAsset"; import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; @@ -7,7 +7,6 @@ import SettingsInputSvideoIcon from "@mui/icons-material/SettingsInputSvideo"; import { useTranslation } from "react-i18next"; import packageData from "../../../../package.json"; import Paper from "@mui/material/Paper"; -import { useTheme } from "@mui/material/styles"; import { dtos } from "types"; const VersionAvatar = styled(Avatar)(({ theme }) => ({ @@ -25,7 +24,6 @@ type Props = { const SystemVersionComponent: React.FC = ({ data }) => { const { t } = useTranslation(); - const theme = useTheme(); const lastReleaseDate = useMemo(() => { const format = "DD-MM-YYYY HH:mm:ss"; diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.tsx b/frontend/src/features/settings/system/CacheSettingsContainer.tsx index 8ec88a8..dd7140f 100644 --- a/frontend/src/features/settings/system/CacheSettingsContainer.tsx +++ b/frontend/src/features/settings/system/CacheSettingsContainer.tsx @@ -17,7 +17,7 @@ const CacheSettingsContainer: React.FC = () => { onSuccess: () => blip.info(t("Settings.Cache.ResetInfo")) }); - const handleResetCache = useCallback(() => trigger(), [t]); + const handleResetCache = useCallback(() => trigger(), [trigger]); return ; }; diff --git a/frontend/src/providers/UserPermissionsProvider.js b/frontend/src/providers/UserPermissionsProvider.tsx similarity index 60% rename from frontend/src/providers/UserPermissionsProvider.js rename to frontend/src/providers/UserPermissionsProvider.tsx index fbc29af..7125935 100644 --- a/frontend/src/providers/UserPermissionsProvider.js +++ b/frontend/src/providers/UserPermissionsProvider.tsx @@ -1,6 +1,9 @@ -import React, { useState, useEffect, useContext, useMemo } from "react"; -import PropTypes from "prop-types"; -import { routes, get } from "../utils/api"; +import React, { useState, useContext, useMemo, ReactNode } from "react"; +import { endpoints } from "../utils/api"; +import { PermissionsDto } from "types/dtos"; +import useSWR from "swr"; +import { fetcher } from "utils/swr"; +import { blip } from "utils"; const permissionCodes = { VIEW_DASHBOARD: "VIEW_DASHBOARD", @@ -13,12 +16,17 @@ const permissionCodes = { SYSTEM_ADMINISTRATION: "SYSTEM_ADMINISTRATION" }; -const initialState = { +type UserPermissionsContextPayload = { + permissions: string[]; + loading: boolean; +}; + +const initialState: UserPermissionsContextPayload = { permissions: [], loading: true }; -const getPermissionFlags = permissions => { +const getPermissionFlags = (permissions: string[]) => { const viewDashboard = permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false; const manageUsers = permissions.includes(permissionCodes.MANAGE_USERS) ?? false; const manageSettings = permissions.includes(permissionCodes.MANAGE_SETTINGS) ?? false; @@ -49,7 +57,7 @@ const getPermissionFlags = permissions => { }; }; -const UserPermissionsContext = React.createContext(initialState); +const UserPermissionsContext = React.createContext(initialState); const usePermissions = () => { const { permissions, loading } = useContext(UserPermissionsContext); @@ -57,20 +65,21 @@ const usePermissions = () => { return { loading, ...flags }; }; -const UserPermissionsProvider = ({ children }) => { - const [permissions, setPermissions] = useState(initialState); - - useEffect(() => { - get(routes.permissions, { - onCompleted: data => setPermissions({ ...data, loading: false }) - }); - }, []); - - return {children}; +type Props = { + children: ReactNode; }; -UserPermissionsProvider.propTypes = { - children: PropTypes.node.isRequired +const UserPermissionsProvider: React.FC = ({ children }) => { + const [state, setState] = useState(initialState); + + const url = useMemo(() => (state.permissions?.length ? null : endpoints.security.permissions), [state.permissions]); + useSWR(url, fetcher, { + revalidateOnFocus: false, + onError: err => blip.error(err.message), + onSuccess: data => setState({ ...data, loading: false }) + }); + + return {children}; }; export { UserPermissionsProvider, usePermissions }; diff --git a/frontend/src/types/dtos.ts b/frontend/src/types/dtos.ts index 95679d4..bdaee66 100644 --- a/frontend/src/types/dtos.ts +++ b/frontend/src/types/dtos.ts @@ -13,3 +13,7 @@ export type ReleaseNote = { date: string; notes: string[]; }; + +export type PermissionsDto = { + permissions: string[]; +}; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 035df60..1b104b4 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -10,8 +10,6 @@ const powerActionsRoute = `${apiHost}/resurrector`; const securityRoute = `${apiHost}/security`; const routes = { - permissions: `${securityRoute}/permissions`, - resetCache: `${systemRoute}/reset-cache`, machines: `${networkRoute}/machines`, wakeMachine: `${powerActionsRoute}/wake`, pingMachine: `${powerActionsRoute}/ping`, @@ -21,6 +19,9 @@ const routes = { version: `${systemRoute}/version`, releaseNotes: `${systemRoute}/release-notes`, resetCache: `${systemRoute}/reset-cache` + }, + security: { + permissions: `${securityRoute}/permissions` } }; From 338aa17fe8525c006ab222ec02c910c01351d391 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 11 Nov 2024 01:54:01 +0200 Subject: [PATCH 14/30] Refactor SWR imports: consolidate fetcher and mutationFetcher into units/swr, update component imports accordingly --- .../features/about/releaseNotes/ReleaseNotesContainer.tsx | 3 +-- .../src/features/about/system/SystemVersionContainer.tsx | 3 +-- .../features/settings/system/CacheSettingsContainer.tsx | 4 +--- frontend/src/providers/UserPermissionsProvider.tsx | 3 +-- frontend/src/{utils/swr.ts => units/swr/fetchers.ts} | 0 frontend/src/units/swr/index.ts | 8 ++++++++ 6 files changed, 12 insertions(+), 9 deletions(-) rename frontend/src/{utils/swr.ts => units/swr/fetchers.ts} (100%) create mode 100644 frontend/src/units/swr/index.ts diff --git a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx index 2669bc9..28d837b 100644 --- a/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx +++ b/frontend/src/features/about/releaseNotes/ReleaseNotesContainer.tsx @@ -2,8 +2,7 @@ import React from "react"; import ReleaseNotesList from "./ReleaseNotesList"; import TimelineComponent from "../timeline/TimelineComponent"; import { endpoints } from "../../../utils/api"; -import useSWR from "swr"; -import { fetcher } from "utils/swr"; +import { useSWR, fetcher } from "units/swr"; import { blip } from "utils"; import { dtos } from "types"; diff --git a/frontend/src/features/about/system/SystemVersionContainer.tsx b/frontend/src/features/about/system/SystemVersionContainer.tsx index 8c3204c..95cde2d 100644 --- a/frontend/src/features/about/system/SystemVersionContainer.tsx +++ b/frontend/src/features/about/system/SystemVersionContainer.tsx @@ -1,8 +1,7 @@ import React from "react"; import SystemVersionComponent from "./SystemVersionComponent"; import { endpoints } from "../../../utils/api"; -import useSWR from "swr"; -import { fetcher } from "utils/swr"; +import { useSWR, fetcher } from "units/swr"; import { blip } from "utils"; import { dtos } from "types"; diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.tsx b/frontend/src/features/settings/system/CacheSettingsContainer.tsx index dd7140f..a423589 100644 --- a/frontend/src/features/settings/system/CacheSettingsContainer.tsx +++ b/frontend/src/features/settings/system/CacheSettingsContainer.tsx @@ -3,9 +3,7 @@ import CacheSettingsComponent from "./CacheSettingsComponent"; import { useTranslation } from "react-i18next"; import { endpoints } from "utils/api"; import { blip } from "utils"; -import useSWRMutation from "swr/mutation"; -import { Key } from "swr"; -import { mutationFetcher } from "utils/swr"; +import { useSWRMutation, mutationFetcher, Key } from "units/swr"; const CacheSettingsContainer: React.FC = () => { const { t } = useTranslation(); diff --git a/frontend/src/providers/UserPermissionsProvider.tsx b/frontend/src/providers/UserPermissionsProvider.tsx index 7125935..6d80d41 100644 --- a/frontend/src/providers/UserPermissionsProvider.tsx +++ b/frontend/src/providers/UserPermissionsProvider.tsx @@ -1,8 +1,7 @@ import React, { useState, useContext, useMemo, ReactNode } from "react"; import { endpoints } from "../utils/api"; import { PermissionsDto } from "types/dtos"; -import useSWR from "swr"; -import { fetcher } from "utils/swr"; +import { useSWR, fetcher } from "units/swr"; import { blip } from "utils"; const permissionCodes = { diff --git a/frontend/src/utils/swr.ts b/frontend/src/units/swr/fetchers.ts similarity index 100% rename from frontend/src/utils/swr.ts rename to frontend/src/units/swr/fetchers.ts diff --git a/frontend/src/units/swr/index.ts b/frontend/src/units/swr/index.ts new file mode 100644 index 0000000..80fe9fd --- /dev/null +++ b/frontend/src/units/swr/index.ts @@ -0,0 +1,8 @@ +import useSWR from "swr"; +import type { Key } from "swr"; +import useSWRMutation from "swr/mutation"; + +export * from "./fetchers"; + +export { useSWR, useSWRMutation }; +export type { Key }; From 8148da132dac55baecafb27415eba0e0b4fe83a5 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 11 Nov 2024 02:04:02 +0200 Subject: [PATCH 15/30] Refactor MachinesContainer: convert to TypeScript, implement SWR for data fetching, and update API routes --- ...inesContainer.js => MachinesContainer.tsx} | 30 +++++++++---------- frontend/src/utils/api.js | 4 ++- 2 files changed, 17 insertions(+), 17 deletions(-) rename frontend/src/features/machines/components/{MachinesContainer.js => MachinesContainer.tsx} (58%) diff --git a/frontend/src/features/machines/components/MachinesContainer.js b/frontend/src/features/machines/components/MachinesContainer.tsx similarity index 58% rename from frontend/src/features/machines/components/MachinesContainer.js rename to frontend/src/features/machines/components/MachinesContainer.tsx index 46a09d1..e731139 100644 --- a/frontend/src/features/machines/components/MachinesContainer.js +++ b/frontend/src/features/machines/components/MachinesContainer.tsx @@ -1,32 +1,30 @@ -import React, { useContext, useEffect, useCallback, useState } from "react"; +import React, { useContext, useState } from "react"; import { NetworkStateContext, NetworkDispatchContext } from "../../network/state/contexts"; import MachinesListComponent from "./MachinesListComponent"; import PageTitle from "../../../components/common/PageTitle"; import { useTranslation } from "react-i18next"; import ViewModeSelection, { ViewModes } from "./ViewModeSelection"; -import { routes, get } from "../../../utils/api"; +import { endpoints } from "../../../utils/api"; +import { useSWR, fetcher } from "units/swr"; +import { blip } from "utils"; +import { Machine } from "types"; -const MachinesContainer = () => { +const MachinesContainer: React.FC = () => { const [viewMode, setViewMode] = useState(null); const state = useContext(NetworkStateContext); const dispatchActions = useContext(NetworkDispatchContext); const { t } = useTranslation(); - const handleReadMachines = useCallback(async () => { - await get(routes.machines, { - onCompleted: machines => { - const data = Object.assign(machines, { loaded: true }); - dispatchActions.onNetworkChange("machines", data); - } - }); - }, [dispatchActions]); - - useEffect(() => { - if (!state.network.machines.loaded) { - handleReadMachines(); + const url = state.network.machines.loaded ? null : endpoints.network.machines; + useSWR(url, fetcher, { + revalidateOnFocus: false, + onError: err => blip.error(err.message), + onSuccess: machines => { + const data = Object.assign(machines, { loaded: true }); + dispatchActions.onNetworkChange("machines", data); } - }, [handleReadMachines, state.network.machines.loaded]); + }); return ( <> diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 1b104b4..b9d84f9 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -10,11 +10,13 @@ const powerActionsRoute = `${apiHost}/resurrector`; const securityRoute = `${apiHost}/security`; const routes = { - machines: `${networkRoute}/machines`, wakeMachine: `${powerActionsRoute}/wake`, pingMachine: `${powerActionsRoute}/ping`, shutdownMachine: `${powerActionsRoute}/shutdown`, restartMachine: `${powerActionsRoute}/restart`, + network: { + machines: `${networkRoute}/machines` + }, system: { version: `${systemRoute}/version`, releaseNotes: `${systemRoute}/release-notes`, From 253ee1953cabd5d77ad67a64ce14859bd3c63170 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Mon, 11 Nov 2024 02:13:03 +0200 Subject: [PATCH 16/30] Refactor WakeComponent: convert to TypeScript, update API routes, and enhance logging functionality --- .../{WakeComponent.js => WakeComponent.tsx} | 43 ++++++++++--------- frontend/src/utils/api.js | 6 ++- 2 files changed, 28 insertions(+), 21 deletions(-) rename frontend/src/features/machines/components/common/{WakeComponent.js => WakeComponent.tsx} (79%) diff --git a/frontend/src/features/machines/components/common/WakeComponent.js b/frontend/src/features/machines/components/common/WakeComponent.tsx similarity index 79% rename from frontend/src/features/machines/components/common/WakeComponent.js rename to frontend/src/features/machines/components/common/WakeComponent.tsx index 69a9d6b..40d3ee3 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.js +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -1,17 +1,23 @@ import React, { useState, useEffect, useCallback } from "react"; -import PropTypes from "prop-types"; import { IconButton, Tooltip } from "@mui/material"; import { PowerSettingsNew } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { blip } from "../../../../utils"; import { msToMinAndSec } from "../../../../utils/time"; import { routes, post } from "../../../../utils/api"; +import { Machine } from "types"; const initialState = { on: false }; const defaultPingInterval = 1200000; //20 minutes const defaultStartingTime = 300000; //5 minutes -const WakeComponent = ({ machine, addLog, disabled }) => { +type Props = { + machine: Machine; + addLog: (message: string) => void; + disabled: boolean; +}; + +const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { const [state, setState] = useState(initialState); const [trigger, setTrigger] = useState(false); @@ -28,14 +34,17 @@ const WakeComponent = ({ machine, addLog, disabled }) => { return result; }, [t]); - const log = useCallback(message => addLog(`[${getCurrentDateTime()}] ${message}`), [addLog, getCurrentDateTime]); + const log = useCallback( + (message: string) => addLog(`[${getCurrentDateTime()}] ${message}`), + [addLog, getCurrentDateTime] + ); const wakeMachine = useCallback(async () => { await post( routes.wakeMachine, { machineId: machine.machineId }, { - onCompleted: result => { + onCompleted: (result: any) => { setState(prev => ({ ...prev, on: result.success })); log(`[Wake]: Success: ${result.success}. Status: ${result.status}`); if (result.success) { @@ -43,9 +52,9 @@ const WakeComponent = ({ machine, addLog, disabled }) => { //retrigger log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`); - setTimeout(() => { - setTrigger(prev => !prev); - }, startingTime); + // setTimeout(() => { + // setTrigger(prev => !prev); + // }, startingTime); } else { blip.error(result.status); } @@ -60,15 +69,15 @@ const WakeComponent = ({ machine, addLog, disabled }) => { routes.pingMachine, { machineId: machine.machineId }, { - onCompleted: result => { + onCompleted: (result: any) => { setState(prev => ({ ...prev, on: result.success })); log(`[Ping]: Success: ${result.success}. Status: ${result.status}`); - if (result.success) { - setTimeout(() => { - setTrigger(prev => !prev); - }, pingInterval); - } + // if (result.success) { + // setTimeout(() => { + // setTrigger(prev => !prev); + // }, pingInterval); + // } }, onError: () => { // to do: handle error @@ -81,7 +90,7 @@ const WakeComponent = ({ machine, addLog, disabled }) => { pingInLoop(); }, [trigger, pingInLoop]); - const handleWakeClick = event => { + const handleWakeClick = (event: any) => { wakeMachine(); event.stopPropagation(); }; @@ -105,10 +114,4 @@ const WakeComponent = ({ machine, addLog, disabled }) => { ); }; -WakeComponent.propTypes = { - machine: PropTypes.object.isRequired, - addLog: PropTypes.func.isRequired, - disabled: PropTypes.bool -}; - export default WakeComponent; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index b9d84f9..3b0847b 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -15,7 +15,11 @@ const routes = { shutdownMachine: `${powerActionsRoute}/shutdown`, restartMachine: `${powerActionsRoute}/restart`, network: { - machines: `${networkRoute}/machines` + machines: `${networkRoute}/machines`, + machine: { + wake: `${powerActionsRoute}/wake`, + ping: `${powerActionsRoute}/ping` + } }, system: { version: `${systemRoute}/version`, From 7234b857a67dde0c6d49bfb72b905729402e7e63 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 01:29:18 +0200 Subject: [PATCH 17/30] Refactor MachineContainer: convert to TypeScript, implement SWR for data fetching, and update action handling --- .../machines/components/MachineAccordion.tsx | 6 +- ...chineContainer.js => MachineContainer.tsx} | 66 ++++++++++--------- frontend/src/types/commands.ts | 11 ++++ frontend/src/types/events.ts | 9 +++ frontend/src/types/index.ts | 6 +- frontend/src/utils/api.js | 7 +- 6 files changed, 66 insertions(+), 39 deletions(-) rename frontend/src/features/machines/components/{MachineContainer.js => MachineContainer.tsx} (60%) create mode 100644 frontend/src/types/commands.ts create mode 100644 frontend/src/types/events.ts diff --git a/frontend/src/features/machines/components/MachineAccordion.tsx b/frontend/src/features/machines/components/MachineAccordion.tsx index e78598b..dc45a6e 100644 --- a/frontend/src/features/machines/components/MachineAccordion.tsx +++ b/frontend/src/features/machines/components/MachineAccordion.tsx @@ -66,9 +66,9 @@ const GridCell: React.FC = ({ label, value }) => { type Props = { machine: models.Machine; - actions: Array; // Replace any with the actual type of the actions - logs: Array; // Replace any with the actual type of the logs - addLog: () => void; // Replace with the actual function signature + actions: Array; + logs: Array; + addLog: (message: string) => void; }; const MachineAccordion: React.FC = ({ machine, actions, logs, addLog }) => { diff --git a/frontend/src/features/machines/components/MachineContainer.js b/frontend/src/features/machines/components/MachineContainer.tsx similarity index 60% rename from frontend/src/features/machines/components/MachineContainer.js rename to frontend/src/features/machines/components/MachineContainer.tsx index 54fcf4e..51f9641 100644 --- a/frontend/src/features/machines/components/MachineContainer.js +++ b/frontend/src/features/machines/components/MachineContainer.tsx @@ -1,26 +1,31 @@ import React, { useState, useCallback } from "react"; -import PropTypes from "prop-types"; import MachineTableRow from "./MachineTableRow"; import MachineAccordion from "./MachineAccordion"; import { ViewModes } from "./ViewModeSelection"; import { blip } from "../../../utils"; import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -import { routes, post } from "../../../utils/api"; +import { routes, post, endpoints } from "../../../utils/api"; +import { Machine, MachineRestarted, MachineShutdown, RestartMachine, ShutdownMachine } from "types"; +import { Key, mutationFetcher, useSWRMutation } from "units/swr"; -const MachineContainer = ({ machine, viewMode }) => { - const [logs, setLogs] = useState([]); +type Props = { + machine: Machine; + viewMode: string; +}; +const MachineContainer: React.FC = ({ machine, viewMode }) => { + const [logs, setLogs] = useState([]); const { t } = useTranslation(); const addLog = useCallback( - text => { + (text: string) => { setLogs(prev => [...prev, text]); }, [setLogs] ); const manageActionResponse = useCallback( - response => { + (response: any) => { addLog(`Success: ${response.success}. Status: ${response.status}`); if (response.success) { blip.success(response.status); @@ -31,8 +36,26 @@ const MachineContainer = ({ machine, viewMode }) => { [addLog] ); + const { trigger: shutdownMachineTrigger } = useSWRMutation( + endpoints.network.machine.shutdown, + mutationFetcher, + { + onError: err => blip.error(err.message), + onSuccess: manageActionResponse + } + ); + + const { trigger: restartMachineTrigger } = useSWRMutation( + endpoints.network.machine.restart, + mutationFetcher, + { + onError: err => blip.error(err.message), + onSuccess: manageActionResponse + } + ); + const pingMachine = useCallback( - async machine => { + async (machine: Machine) => { await post( routes.pingMachine, { machineId: machine.machineId }, @@ -45,29 +68,13 @@ const MachineContainer = ({ machine, viewMode }) => { ); const shutdownMachine = useCallback( - async machine => { - await post( - routes.shutdownMachine, - { machineId: machine.machineId, delay: 0, force: false }, - { - onCompleted: manageActionResponse - } - ); - }, - [manageActionResponse] + async (machine: Machine) => shutdownMachineTrigger({ machineId: machine.machineId, delay: 0, force: false }), + [shutdownMachineTrigger] ); const restartMachine = useCallback( - async machine => { - await post( - routes.restartMachine, - { machineId: machine.machineId, delay: 0, force: false }, - { - onCompleted: manageActionResponse - } - ); - }, - [manageActionResponse] + async (machine: Machine) => restartMachineTrigger({ machineId: machine.machineId, delay: 0, force: false }), + [restartMachineTrigger] ); const actions = [ @@ -115,9 +122,4 @@ const MachineContainer = ({ machine, viewMode }) => { ); }; -MachineContainer.propTypes = { - machine: PropTypes.object.isRequired, - viewMode: PropTypes.string.isRequired -}; - export default MachineContainer; diff --git a/frontend/src/types/commands.ts b/frontend/src/types/commands.ts new file mode 100644 index 0000000..9f664b4 --- /dev/null +++ b/frontend/src/types/commands.ts @@ -0,0 +1,11 @@ +export type ShutdownMachine = { + machineId: number; + delay?: number; + force?: boolean; +}; + +export type RestartMachine = { + machineId: number; + delay?: number; + force?: boolean; +}; diff --git a/frontend/src/types/events.ts b/frontend/src/types/events.ts new file mode 100644 index 0000000..3996d20 --- /dev/null +++ b/frontend/src/types/events.ts @@ -0,0 +1,9 @@ +export type MachineShutdown = { + success: boolean; + status: string; +}; + +export type MachineRestarted = { + success: boolean; + status: string; +}; diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts index 05ad3f9..fef5878 100644 --- a/frontend/src/types/index.ts +++ b/frontend/src/types/index.ts @@ -1,6 +1,10 @@ import * as models from "./models"; import * as dtos from "./dtos"; +import * as commands from "./commands"; +import * as events from "./events"; export * from "./models"; +export * from "./commands"; +export * from "./events"; -export { models, dtos }; +export { models, dtos, commands, events }; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 3b0847b..e71d320 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -12,13 +12,14 @@ const securityRoute = `${apiHost}/security`; const routes = { wakeMachine: `${powerActionsRoute}/wake`, pingMachine: `${powerActionsRoute}/ping`, - shutdownMachine: `${powerActionsRoute}/shutdown`, - restartMachine: `${powerActionsRoute}/restart`, + network: { machines: `${networkRoute}/machines`, machine: { wake: `${powerActionsRoute}/wake`, - ping: `${powerActionsRoute}/ping` + ping: `${powerActionsRoute}/ping`, + shutdown: `${powerActionsRoute}/shutdown`, + restart: `${powerActionsRoute}/restart` } }, system: { From 09e447f4b373a672fa4f1fc23dd14548adb97ab0 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 01:52:47 +0200 Subject: [PATCH 18/30] Refactor MachineContainer and WakeComponent: implement usePingTrigger for pinging machines, update action result types, and clean up API calls --- .../machines/components/MachineContainer.tsx | 30 ++++++++------- .../components/common/WakeComponent.tsx | 37 +++++++++---------- frontend/src/features/machines/hooks/index.ts | 3 ++ .../features/machines/hooks/usePingTrigger.ts | 25 +++++++++++++ frontend/src/types/commands.ts | 4 ++ frontend/src/types/events.ts | 11 +++--- frontend/src/utils/api.js | 2 - 7 files changed, 73 insertions(+), 39 deletions(-) create mode 100644 frontend/src/features/machines/hooks/index.ts create mode 100644 frontend/src/features/machines/hooks/usePingTrigger.ts diff --git a/frontend/src/features/machines/components/MachineContainer.tsx b/frontend/src/features/machines/components/MachineContainer.tsx index 51f9641..3328a46 100644 --- a/frontend/src/features/machines/components/MachineContainer.tsx +++ b/frontend/src/features/machines/components/MachineContainer.tsx @@ -5,9 +5,17 @@ import { ViewModes } from "./ViewModeSelection"; import { blip } from "../../../utils"; import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; -import { routes, post, endpoints } from "../../../utils/api"; -import { Machine, MachineRestarted, MachineShutdown, RestartMachine, ShutdownMachine } from "types"; +import { endpoints } from "../../../utils/api"; +import { + Machine, + MachineActionResult, + MachineRestarted, + MachineShutdown, + RestartMachine, + ShutdownMachine +} from "types"; import { Key, mutationFetcher, useSWRMutation } from "units/swr"; +import { usePingTrigger } from "../hooks"; type Props = { machine: Machine; @@ -25,7 +33,7 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { ); const manageActionResponse = useCallback( - (response: any) => { + (response: MachineActionResult) => { addLog(`Success: ${response.success}. Status: ${response.status}`); if (response.success) { blip.success(response.status); @@ -36,6 +44,10 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { [addLog] ); + const { pingMachineTrigger } = usePingTrigger({ + onSuccess: manageActionResponse + }); + const { trigger: shutdownMachineTrigger } = useSWRMutation( endpoints.network.machine.shutdown, mutationFetcher, @@ -55,16 +67,8 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { ); const pingMachine = useCallback( - async (machine: Machine) => { - await post( - routes.pingMachine, - { machineId: machine.machineId }, - { - onCompleted: manageActionResponse - } - ); - }, - [manageActionResponse] + async (machine: Machine) => pingMachineTrigger({ machineId: machine.machineId }), + [pingMachineTrigger] ); const shutdownMachine = useCallback( diff --git a/frontend/src/features/machines/components/common/WakeComponent.tsx b/frontend/src/features/machines/components/common/WakeComponent.tsx index 40d3ee3..c24fdfc 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.tsx +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -6,6 +6,7 @@ import { blip } from "../../../../utils"; import { msToMinAndSec } from "../../../../utils/time"; import { routes, post } from "../../../../utils/api"; import { Machine } from "types"; +import { usePingTrigger } from "../../hooks"; const initialState = { on: false }; const defaultPingInterval = 1200000; //20 minutes @@ -39,6 +40,22 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { [addLog, getCurrentDateTime] ); + const { pingMachineTrigger } = usePingTrigger({ + onSuccess: result => { + setState(prev => ({ ...prev, on: result.success })); + log(`[Ping]: Success: ${result.success}. Status: ${result.status}`); + + // if (result.success) { + // setTimeout(() => { + // setTrigger(prev => !prev); + // }, pingInterval); + // } + }, + onError: () => { + // to do: handle error + } + }); + const wakeMachine = useCallback(async () => { await post( routes.wakeMachine, @@ -65,25 +82,7 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { const pingInLoop = useCallback(async () => { if (disabled) return; - await post( - routes.pingMachine, - { machineId: machine.machineId }, - { - onCompleted: (result: any) => { - setState(prev => ({ ...prev, on: result.success })); - log(`[Ping]: Success: ${result.success}. Status: ${result.status}`); - - // if (result.success) { - // setTimeout(() => { - // setTrigger(prev => !prev); - // }, pingInterval); - // } - }, - onError: () => { - // to do: handle error - } - } - ); + pingMachineTrigger({ machineId: machine.machineId }); }, [machine, log, pingInterval, disabled]); useEffect(() => { diff --git a/frontend/src/features/machines/hooks/index.ts b/frontend/src/features/machines/hooks/index.ts new file mode 100644 index 0000000..90fd598 --- /dev/null +++ b/frontend/src/features/machines/hooks/index.ts @@ -0,0 +1,3 @@ +import usePingTrigger from "./usePingTrigger"; + +export { usePingTrigger }; diff --git a/frontend/src/features/machines/hooks/usePingTrigger.ts b/frontend/src/features/machines/hooks/usePingTrigger.ts new file mode 100644 index 0000000..110c787 --- /dev/null +++ b/frontend/src/features/machines/hooks/usePingTrigger.ts @@ -0,0 +1,25 @@ +import { useMemo } from "react"; +import { MachinePinged, PingMachine } from "types"; +import { Key, mutationFetcher, useSWRMutation } from "units/swr"; +import { blip } from "utils"; +import { endpoints } from "utils/api"; + +type PingTriggerOptions = { + onSuccess: (response: MachinePinged) => void; + onError?: (error: Error) => void; +}; +const usePingTrigger = (options: PingTriggerOptions) => { + const { onSuccess } = options; + const onError = useMemo(() => options.onError || ((error: Error) => blip.error(error.message)), [options.onError]); + + const { trigger: pingMachineTrigger } = useSWRMutation( + endpoints.network.machine.ping, + mutationFetcher, + { + onError, + onSuccess + } + ); + return { pingMachineTrigger }; +}; +export default usePingTrigger; diff --git a/frontend/src/types/commands.ts b/frontend/src/types/commands.ts index 9f664b4..13945c5 100644 --- a/frontend/src/types/commands.ts +++ b/frontend/src/types/commands.ts @@ -1,3 +1,7 @@ +export type PingMachine = { + machineId: number; +}; + export type ShutdownMachine = { machineId: number; delay?: number; diff --git a/frontend/src/types/events.ts b/frontend/src/types/events.ts index 3996d20..eda3b92 100644 --- a/frontend/src/types/events.ts +++ b/frontend/src/types/events.ts @@ -1,9 +1,10 @@ -export type MachineShutdown = { +export type MachineActionResult = { success: boolean; status: string; }; -export type MachineRestarted = { - success: boolean; - status: string; -}; +export type MachinePinged = MachineActionResult; + +export type MachineShutdown = MachineActionResult; + +export type MachineRestarted = MachineActionResult; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index e71d320..7973e05 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -11,8 +11,6 @@ const securityRoute = `${apiHost}/security`; const routes = { wakeMachine: `${powerActionsRoute}/wake`, - pingMachine: `${powerActionsRoute}/ping`, - network: { machines: `${networkRoute}/machines`, machine: { From 618bfb38e1d62c8fd6d61c3e35dcbc9935e253a8 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:03:47 +0200 Subject: [PATCH 19/30] Refactor WakeComponent: update ping interval and starting time handling, streamline ping logic, and improve dependency management --- .../components/common/WakeComponent.tsx | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/frontend/src/features/machines/components/common/WakeComponent.tsx b/frontend/src/features/machines/components/common/WakeComponent.tsx index c24fdfc..e93d263 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.tsx +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -12,6 +12,9 @@ const initialState = { on: false }; const defaultPingInterval = 1200000; //20 minutes const defaultStartingTime = 300000; //5 minutes +const pingInterval = parseInt(process.env.REACT_APP_MACHINE_PING_INTERVAL ?? "") || defaultPingInterval; +const startingTime = parseInt(process.env.REACT_APP_MACHINE_STARTING_TIME ?? "") || defaultStartingTime; + type Props = { machine: Machine; addLog: (message: string) => void; @@ -24,9 +27,6 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { const { t } = useTranslation(); - const pingInterval = process.env.REACT_APP_MACHINE_PING_INTERVAL || defaultPingInterval; - const startingTime = process.env.REACT_APP_MACHINE_STARTING_TIME || defaultStartingTime; - const getCurrentDateTime = useCallback(() => { const currentDateTime = Date.now(); const result = t("DATE_FORMAT", { @@ -45,11 +45,12 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { setState(prev => ({ ...prev, on: result.success })); log(`[Ping]: Success: ${result.success}. Status: ${result.status}`); - // if (result.success) { - // setTimeout(() => { - // setTrigger(prev => !prev); - // }, pingInterval); - // } + // trigger the next ping + if (result.success) { + setTimeout(() => { + setTrigger(prev => !prev); + }, pingInterval); + } }, onError: () => { // to do: handle error @@ -78,16 +79,16 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { } } ); - }, [log, startingTime, machine.machineId]); + }, [log, machine.machineId]); - const pingInLoop = useCallback(async () => { + const pingMachine = useCallback(async () => { if (disabled) return; pingMachineTrigger({ machineId: machine.machineId }); - }, [machine, log, pingInterval, disabled]); + }, [machine.machineId, pingMachineTrigger, disabled]); useEffect(() => { - pingInLoop(); - }, [trigger, pingInLoop]); + pingMachine(); + }, [trigger, pingMachine]); const handleWakeClick = (event: any) => { wakeMachine(); From 04e80a0ac08eb8925feb1aeb87407d649b17e72c Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:11:09 +0200 Subject: [PATCH 20/30] Refactor WakeComponent: integrate useSWRMutation for waking machines, update API endpoint usage, and enhance type definitions --- .../components/common/WakeComponent.tsx | 48 +++++++++---------- frontend/src/types/commands.ts | 4 ++ frontend/src/types/events.ts | 3 +- frontend/src/utils/api.js | 1 - 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/frontend/src/features/machines/components/common/WakeComponent.tsx b/frontend/src/features/machines/components/common/WakeComponent.tsx index e93d263..d82cdfd 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.tsx +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -4,9 +4,10 @@ import { PowerSettingsNew } from "@mui/icons-material"; import { useTranslation } from "react-i18next"; import { blip } from "../../../../utils"; import { msToMinAndSec } from "../../../../utils/time"; -import { routes, post } from "../../../../utils/api"; -import { Machine } from "types"; +import { endpoints } from "../../../../utils/api"; +import { Machine, MachineWaked, WakeMachine } from "types"; import { usePingTrigger } from "../../hooks"; +import { Key, mutationFetcher, useSWRMutation } from "units/swr"; const initialState = { on: false }; const defaultPingInterval = 1200000; //20 minutes @@ -57,29 +58,28 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { } }); - const wakeMachine = useCallback(async () => { - await post( - routes.wakeMachine, - { machineId: machine.machineId }, - { - onCompleted: (result: any) => { - setState(prev => ({ ...prev, on: result.success })); - log(`[Wake]: Success: ${result.success}. Status: ${result.status}`); - if (result.success) { - blip.success(result.status); + const { trigger: wakeMachineTrigger } = useSWRMutation( + endpoints.network.machine.wake, + mutationFetcher, + { + onError: err => blip.error(err.message), + onSuccess: result => { + setState(prev => ({ ...prev, on: result.success })); + log(`[Wake]: Success: ${result.success}. Status: ${result.status}`); + if (result.success) { + blip.success(result.status); - //retrigger - log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`); - // setTimeout(() => { - // setTrigger(prev => !prev); - // }, startingTime); - } else { - blip.error(result.status); - } + //retrigger + log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`); + // setTimeout(() => { + // setTrigger(prev => !prev); + // }, startingTime); + } else { + blip.error(result.status); } } - ); - }, [log, machine.machineId]); + } + ); const pingMachine = useCallback(async () => { if (disabled) return; @@ -90,8 +90,8 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { pingMachine(); }, [trigger, pingMachine]); - const handleWakeClick = (event: any) => { - wakeMachine(); + const handleWakeClick = (event: React.MouseEvent) => { + wakeMachineTrigger({ machineId: machine.machineId }); event.stopPropagation(); }; diff --git a/frontend/src/types/commands.ts b/frontend/src/types/commands.ts index 13945c5..4ad5b28 100644 --- a/frontend/src/types/commands.ts +++ b/frontend/src/types/commands.ts @@ -1,3 +1,7 @@ +export type WakeMachine = { + machineId: number; +}; + export type PingMachine = { machineId: number; }; diff --git a/frontend/src/types/events.ts b/frontend/src/types/events.ts index eda3b92..64602a3 100644 --- a/frontend/src/types/events.ts +++ b/frontend/src/types/events.ts @@ -3,8 +3,7 @@ export type MachineActionResult = { status: string; }; +export type MachineWaked = MachineActionResult; export type MachinePinged = MachineActionResult; - export type MachineShutdown = MachineActionResult; - export type MachineRestarted = MachineActionResult; diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 7973e05..c450b66 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -10,7 +10,6 @@ const powerActionsRoute = `${apiHost}/resurrector`; const securityRoute = `${apiHost}/security`; const routes = { - wakeMachine: `${powerActionsRoute}/wake`, network: { machines: `${networkRoute}/machines`, machine: { From ad27fa477d8aee56ef55effa48edd75cbfb221f3 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:12:21 +0200 Subject: [PATCH 21/30] Refactor api.js: simplify error handling, remove unused functions, and rename routes to endpoints --- frontend/src/utils/api.js | 53 ++------------------------------------- 1 file changed, 2 insertions(+), 51 deletions(-) diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index c450b66..f7a4d77 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -1,5 +1,3 @@ -import * as axios from "../utils/axios"; -import { toast } from "react-toastify"; import env from "../utils/env"; const apiHost = env.REACT_APP_NETWORK_RESURRECTOR_API_URL; @@ -9,7 +7,7 @@ const systemRoute = `${apiHost}/system`; const powerActionsRoute = `${apiHost}/resurrector`; const securityRoute = `${apiHost}/security`; -const routes = { +const endpoints = { network: { machines: `${networkRoute}/machines`, machine: { @@ -29,51 +27,4 @@ const routes = { } }; -const handleError = err => { - let message; - switch (err?.status) { - case 500: - message = err.title; - break; - - case 404: - message = err.message; - break; - - default: - message = err.title; - } - - toast.error(message); -}; - -const defaultOptions = { - onCompleted: () => null, - onError: handleError -}; - -const call = async (request, options = defaultOptions) => { - const internalOptions = { ...defaultOptions, ...options }; - const { onCompleted, onError } = internalOptions; - - try { - const result = await request(); - onCompleted(result); - } catch (error) { - onError(error); - } -}; - -const get = (route, options) => { - const promise = call(() => axios.get(route), options); - return promise; -}; - -const post = (route, data, options) => { - const promise = call(() => axios.post(route, data), options); - return promise; -}; - -const endpoints = routes; - -export { routes, get, post, endpoints }; +export { endpoints }; From 174f383968095a3542251480563b47af047efff9 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:12:47 +0200 Subject: [PATCH 22/30] Refactor api.js: rename powerActionsRoute to resurrectorRoute for clarity in API endpoint usage --- frontend/src/utils/api.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index f7a4d77..a218992 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -4,17 +4,17 @@ const apiHost = env.REACT_APP_NETWORK_RESURRECTOR_API_URL; const networkRoute = `${apiHost}/network`; const systemRoute = `${apiHost}/system`; -const powerActionsRoute = `${apiHost}/resurrector`; +const resurrectorRoute = `${apiHost}/resurrector`; const securityRoute = `${apiHost}/security`; const endpoints = { network: { machines: `${networkRoute}/machines`, machine: { - wake: `${powerActionsRoute}/wake`, - ping: `${powerActionsRoute}/ping`, - shutdown: `${powerActionsRoute}/shutdown`, - restart: `${powerActionsRoute}/restart` + wake: `${resurrectorRoute}/wake`, + ping: `${resurrectorRoute}/ping`, + shutdown: `${resurrectorRoute}/shutdown`, + restart: `${resurrectorRoute}/restart` } }, system: { From 0a3e4020131d32c73d70741dcaa7bcb531a01a70 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:13:12 +0200 Subject: [PATCH 23/30] Refactor WakeComponent: enable periodic ping retriggering by uncommenting setTimeout logic --- .../features/machines/components/common/WakeComponent.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/features/machines/components/common/WakeComponent.tsx b/frontend/src/features/machines/components/common/WakeComponent.tsx index d82cdfd..3f3843d 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.tsx +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -71,9 +71,9 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { //retrigger log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`); - // setTimeout(() => { - // setTrigger(prev => !prev); - // }, startingTime); + setTimeout(() => { + setTrigger(prev => !prev); + }, startingTime); } else { blip.error(result.status); } From 49ac48e1f84b9be40f0513a5d7fe44c60ce8c51e Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:14:30 +0200 Subject: [PATCH 24/30] Refactor api.js: migrate from JavaScript to TypeScript, enhancing type safety and maintainability --- frontend/src/utils/{api.js => api.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/src/utils/{api.js => api.ts} (100%) diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.ts similarity index 100% rename from frontend/src/utils/api.js rename to frontend/src/utils/api.ts From 52af1ef10bd139386abe72e281332884b013f658 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:17:49 +0200 Subject: [PATCH 25/30] Refactor axios.js: remove unused axios utility functions to streamline codebase --- frontend/src/utils/axios.js | 74 ------------------------------------- 1 file changed, 74 deletions(-) delete mode 100644 frontend/src/utils/axios.js diff --git a/frontend/src/utils/axios.js b/frontend/src/utils/axios.js deleted file mode 100644 index eb73c74..0000000 --- a/frontend/src/utils/axios.js +++ /dev/null @@ -1,74 +0,0 @@ -import axios from "axios"; -import i18next from "i18next"; -import { acquire as fetchTuitioData } from "@flare/tuitio-client"; - -function getHeaders() { - const { token } = fetchTuitioData(); - const language = i18next.language; - - return { - "Content-Type": "application/json", - Authorization: `Tuitio ${token}`, - "Accept-Language": `${language}` - }; -} - -async function internalRequest(url, options) { - try { - const res = await axios.request(url, options); - return res.data; - } catch (error) { - if (error.response && error.response.data) { - throw ( - { - ...error.response.data, - message: error.response.data.detail || error.response.data.title - } || error - ); - } - // The request was made but no response was received - // `error.request` is an instance of XMLHttpRequest in the browser and an instance of - // http.ClientRequest in node.js - throw error; - } -} - -export const request = (url, options) => internalRequest(url, options); - -export function post(url, data) { - const options = { - method: "post", - data: JSON.stringify(data), - headers: getHeaders() - }; - - return internalRequest(url, options); -} - -export function put(url, data) { - const options = { - method: "put", - data: JSON.stringify(data), - headers: getHeaders() - }; - - return internalRequest(url, options); -} - -export function del(url, data) { - const options = { - method: "delete", - data: JSON.stringify(data), - headers: getHeaders() - }; - - return internalRequest(url, options); -} - -export function get(url) { - const options = { - method: "GET", - headers: getHeaders() - }; - return internalRequest(url, options); -} From a664a4ade9e4bedd5449c06d2ae3f7bdfb9d496b Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:36:15 +0200 Subject: [PATCH 26/30] Bump version to 1.3.1 and update release notes with SWR integration and progress bar implementation --- backend/Directory.Build.props | 2 +- backend/ReleaseNotes.xml | 14 +++++++++++++- frontend/package-lock.json | 4 ++-- frontend/package.json | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) diff --git a/backend/Directory.Build.props b/backend/Directory.Build.props index 8228db9..2a424c8 100644 --- a/backend/Directory.Build.props +++ b/backend/Directory.Build.props @@ -1,7 +1,7 @@ - 1.3.0 + 1.3.1 Tudor Stanciu STA NetworkResurrector diff --git a/backend/ReleaseNotes.xml b/backend/ReleaseNotes.xml index 6719574..ffc3f95 100644 --- a/backend/ReleaseNotes.xml +++ b/backend/ReleaseNotes.xml @@ -209,7 +209,19 @@ • Unified repository structure - The backend and frontend codebases have been consolidated into a single repository, improving code management and cross-component consistency. • Frontend Upgrades - The frontend has been upgraded to leverage TypeScript, React 18, and Material UI 5, enhancing code quality, UI consistency, and leveraging the latest features of these technologies. • Build process overhaul - I've switched from react-scripts to react-app-rewired. This gives me more control to tweak the webpack configuration as I need. Plus, it's now set up to handle CommonJS (CJS) modules. - • TypeScript refactoring - Several components have been rewritten in TypeScript, improving type safety and predictability in our codebase. + • TypeScript refactoring - Several components have been rewritten in TypeScript, improving type safety and predictability in the codebase. + + + + 1.3.1 + 2024-11-16 02:28 + + SWR integration and progress bar implementation + • SWR has been integrated into the application to handle data fetching, caching, and revalidation. This will help improve the performance of the application by reducing the number of network requests and ensuring that the data is always up to date. + • In this process, the application has been refactored to use SWR hooks for data fetching and caching, replacing the previous custom methods. + • Also, several components have been updated to use TypeScript to continue the migration process started in the previous release. + • A progress bar has been implemented to provide visual feedback to users when data is being fetched or processed. + • The progress bar is displayed at the top of the page and shows the loading status of the application. \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json index a49a5ba..e7e2ca0 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -1,12 +1,12 @@ { "name": "network-resurrector-frontend", - "version": "1.3.0", + "version": "1.3.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "network-resurrector-frontend", - "version": "1.3.0", + "version": "1.3.1", "dependencies": { "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", diff --git a/frontend/package.json b/frontend/package.json index 2c581ea..982a482 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -1,6 +1,6 @@ { "name": "network-resurrector-frontend", - "version": "1.3.0", + "version": "1.3.1", "description": "Frontend component of Network resurrector system", "author": { "name": "Tudor Stanciu", From 8f9c03f400ae54f537f38f4fd628799a546b2eb6 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 02:47:51 +0200 Subject: [PATCH 27/30] Remove commented-out deleteFetcher function from fetchers.ts to clean up code --- frontend/src/units/swr/fetchers.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/frontend/src/units/swr/fetchers.ts b/frontend/src/units/swr/fetchers.ts index a407355..23f7bd9 100644 --- a/frontend/src/units/swr/fetchers.ts +++ b/frontend/src/units/swr/fetchers.ts @@ -34,11 +34,4 @@ async function mutationFetcher(url: string, { arg }: { arg: Command }) }).then(res => res.json()); } -// async function deleteFetcher(url: string, { arg }: { arg: Command }) { -// return fetch(combine(url, arg as number), { -// method: "DELETE", -// headers: getHeaders() -// }).then(res => res.json()); -// } - export { fetcher, mutationFetcher }; From 82a4750c76fc3e35c8d4e721f3cb48c66ee4c363 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 04:05:16 +0200 Subject: [PATCH 28/30] Refactor SWR mutation handling: replace Error with NetworkError for improved error management and add error handling in fetchers --- .../machines/components/MachineContainer.tsx | 14 +++++++---- .../components/common/WakeComponent.tsx | 4 +-- .../features/machines/hooks/usePingTrigger.ts | 4 +-- .../system/CacheSettingsContainer.tsx | 18 +++++++------ frontend/src/units/swr/errors.ts | 16 ++++++++++++ frontend/src/units/swr/fetchers.ts | 25 ++++++++++++++++--- frontend/src/units/swr/index.ts | 1 + 7 files changed, 63 insertions(+), 19 deletions(-) create mode 100644 frontend/src/units/swr/errors.ts diff --git a/frontend/src/features/machines/components/MachineContainer.tsx b/frontend/src/features/machines/components/MachineContainer.tsx index 3328a46..9eced98 100644 --- a/frontend/src/features/machines/components/MachineContainer.tsx +++ b/frontend/src/features/machines/components/MachineContainer.tsx @@ -14,7 +14,7 @@ import { RestartMachine, ShutdownMachine } from "types"; -import { Key, mutationFetcher, useSWRMutation } from "units/swr"; +import { Key, NetworkError, mutationFetcher, useSWRMutation } from "units/swr"; import { usePingTrigger } from "../hooks"; type Props = { @@ -34,6 +34,7 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { const manageActionResponse = useCallback( (response: MachineActionResult) => { + debugger; addLog(`Success: ${response.success}. Status: ${response.status}`); if (response.success) { blip.success(response.status); @@ -48,20 +49,23 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { onSuccess: manageActionResponse }); - const { trigger: shutdownMachineTrigger } = useSWRMutation( + const { trigger: shutdownMachineTrigger } = useSWRMutation( endpoints.network.machine.shutdown, mutationFetcher, { - onError: err => blip.error(err.message), + onError: err => { + debugger; + blip.error(err.message); + }, onSuccess: manageActionResponse } ); - const { trigger: restartMachineTrigger } = useSWRMutation( + const { trigger: restartMachineTrigger } = useSWRMutation( endpoints.network.machine.restart, mutationFetcher, { - onError: err => blip.error(err.message), + onError: err => blip.error(err.serverError.message || err.message), onSuccess: manageActionResponse } ); diff --git a/frontend/src/features/machines/components/common/WakeComponent.tsx b/frontend/src/features/machines/components/common/WakeComponent.tsx index 3f3843d..3e1aa9e 100644 --- a/frontend/src/features/machines/components/common/WakeComponent.tsx +++ b/frontend/src/features/machines/components/common/WakeComponent.tsx @@ -7,7 +7,7 @@ import { msToMinAndSec } from "../../../../utils/time"; import { endpoints } from "../../../../utils/api"; import { Machine, MachineWaked, WakeMachine } from "types"; import { usePingTrigger } from "../../hooks"; -import { Key, mutationFetcher, useSWRMutation } from "units/swr"; +import { Key, mutationFetcher, NetworkError, useSWRMutation } from "units/swr"; const initialState = { on: false }; const defaultPingInterval = 1200000; //20 minutes @@ -58,7 +58,7 @@ const WakeComponent: React.FC = ({ machine, addLog, disabled }) => { } }); - const { trigger: wakeMachineTrigger } = useSWRMutation( + const { trigger: wakeMachineTrigger } = useSWRMutation( endpoints.network.machine.wake, mutationFetcher, { diff --git a/frontend/src/features/machines/hooks/usePingTrigger.ts b/frontend/src/features/machines/hooks/usePingTrigger.ts index 110c787..c9264ea 100644 --- a/frontend/src/features/machines/hooks/usePingTrigger.ts +++ b/frontend/src/features/machines/hooks/usePingTrigger.ts @@ -1,6 +1,6 @@ import { useMemo } from "react"; import { MachinePinged, PingMachine } from "types"; -import { Key, mutationFetcher, useSWRMutation } from "units/swr"; +import { Key, mutationFetcher, NetworkError, useSWRMutation } from "units/swr"; import { blip } from "utils"; import { endpoints } from "utils/api"; @@ -12,7 +12,7 @@ const usePingTrigger = (options: PingTriggerOptions) => { const { onSuccess } = options; const onError = useMemo(() => options.onError || ((error: Error) => blip.error(error.message)), [options.onError]); - const { trigger: pingMachineTrigger } = useSWRMutation( + const { trigger: pingMachineTrigger } = useSWRMutation( endpoints.network.machine.ping, mutationFetcher, { diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.tsx b/frontend/src/features/settings/system/CacheSettingsContainer.tsx index a423589..2ee1ee1 100644 --- a/frontend/src/features/settings/system/CacheSettingsContainer.tsx +++ b/frontend/src/features/settings/system/CacheSettingsContainer.tsx @@ -3,17 +3,21 @@ import CacheSettingsComponent from "./CacheSettingsComponent"; import { useTranslation } from "react-i18next"; import { endpoints } from "utils/api"; import { blip } from "utils"; -import { useSWRMutation, mutationFetcher, Key } from "units/swr"; +import { useSWRMutation, mutationFetcher, Key, NetworkError } from "units/swr"; const CacheSettingsContainer: React.FC = () => { const { t } = useTranslation(); - const { trigger } = useSWRMutation(endpoints.system.resetCache, mutationFetcher, { - onError: err => { - blip.error(err.message); - }, - onSuccess: () => blip.info(t("Settings.Cache.ResetInfo")) - }); + const { trigger } = useSWRMutation( + endpoints.system.resetCache, + mutationFetcher, + { + onError: err => { + blip.error(err.message); + }, + onSuccess: () => blip.info(t("Settings.Cache.ResetInfo")) + } + ); const handleResetCache = useCallback(() => trigger(), [trigger]); diff --git a/frontend/src/units/swr/errors.ts b/frontend/src/units/swr/errors.ts new file mode 100644 index 0000000..368b2d4 --- /dev/null +++ b/frontend/src/units/swr/errors.ts @@ -0,0 +1,16 @@ +export type ServerError = { + title: string; + status: number; + message: string | null; +}; + +export class NetworkError extends Error { + status: number; + serverError: ServerError; + + constructor(message: string, status: number, serverError: ServerError) { + super(message); + this.status = status; + this.serverError = serverError; + } +} diff --git a/frontend/src/units/swr/fetchers.ts b/frontend/src/units/swr/fetchers.ts index 23f7bd9..bec9af1 100644 --- a/frontend/src/units/swr/fetchers.ts +++ b/frontend/src/units/swr/fetchers.ts @@ -1,5 +1,6 @@ import i18next from "i18next"; import { acquire as fetchTuitioData } from "@flare/tuitio-client"; +import { NetworkError, ServerError } from "./errors"; const getHeaders = (): HeadersInit => { const { token } = fetchTuitioData(); @@ -20,18 +21,36 @@ const getHeaders = (): HeadersInit => { return headers; }; -const fetcher = (url: string) => fetch(url, { method: "GET", headers: getHeaders() }).then(res => res.json()); +const fetcher = async (url: string) => { + const res = await fetch(url, { method: "GET", headers: getHeaders() }); + + if (!res.ok) { + const serverError = (await res.json()) as ServerError; + const error = new NetworkError("An error occurred while fetching the data.", res.status, serverError); + throw error; + } + + return res.json(); +}; async function mutationFetcher(url: string, { arg }: { arg: Command }) { const hasBody = arg !== undefined && arg !== null; const headers = getHeaders(); const body = hasBody ? JSON.stringify(arg) : undefined; - return fetch(url, { + const res = await fetch(url, { method: "POST", headers, body - }).then(res => res.json()); + }); + + if (!res.ok) { + const serverError = (await res.json()) as ServerError; + const error = new NetworkError("An error occurred while mutating the data.", res.status, serverError); + throw error; + } + + return res.json(); } export { fetcher, mutationFetcher }; diff --git a/frontend/src/units/swr/index.ts b/frontend/src/units/swr/index.ts index 80fe9fd..f5d3e5c 100644 --- a/frontend/src/units/swr/index.ts +++ b/frontend/src/units/swr/index.ts @@ -3,6 +3,7 @@ import type { Key } from "swr"; import useSWRMutation from "swr/mutation"; export * from "./fetchers"; +export * from "./errors"; export { useSWR, useSWRMutation }; export type { Key }; From 181c565b3cb705e5f0f59e64addc2764c697e191 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 04:16:10 +0200 Subject: [PATCH 29/30] Remove debugger statements and streamline error handling in MachineContainer --- .../src/features/machines/components/MachineContainer.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/frontend/src/features/machines/components/MachineContainer.tsx b/frontend/src/features/machines/components/MachineContainer.tsx index 9eced98..ba598d3 100644 --- a/frontend/src/features/machines/components/MachineContainer.tsx +++ b/frontend/src/features/machines/components/MachineContainer.tsx @@ -34,7 +34,6 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { const manageActionResponse = useCallback( (response: MachineActionResult) => { - debugger; addLog(`Success: ${response.success}. Status: ${response.status}`); if (response.success) { blip.success(response.status); @@ -53,10 +52,7 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { endpoints.network.machine.shutdown, mutationFetcher, { - onError: err => { - debugger; - blip.error(err.message); - }, + onError: err => blip.error(err.message), onSuccess: manageActionResponse } ); From feb2ab11f9366bdd4b3af179bc3ce1f1f5336ad1 Mon Sep 17 00:00:00 2001 From: Tudor Stanciu Date: Sat, 16 Nov 2024 14:11:32 +0200 Subject: [PATCH 30/30] Implement useGuardedMutation hook for enhanced error handling in MachineContainer and update related components --- .../machines/components/MachineContainer.tsx | 8 ++-- .../features/machines/hooks/usePingTrigger.ts | 3 +- .../system/CacheSettingsContainer.tsx | 7 +--- frontend/src/units/swr/hooks/index.ts | 3 ++ .../src/units/swr/hooks/useGuardedMutation.ts | 39 +++++++++++++++++++ frontend/src/units/swr/index.ts | 1 + 6 files changed, 50 insertions(+), 11 deletions(-) create mode 100644 frontend/src/units/swr/hooks/index.ts create mode 100644 frontend/src/units/swr/hooks/useGuardedMutation.ts diff --git a/frontend/src/features/machines/components/MachineContainer.tsx b/frontend/src/features/machines/components/MachineContainer.tsx index ba598d3..31d02af 100644 --- a/frontend/src/features/machines/components/MachineContainer.tsx +++ b/frontend/src/features/machines/components/MachineContainer.tsx @@ -14,7 +14,7 @@ import { RestartMachine, ShutdownMachine } from "types"; -import { Key, NetworkError, mutationFetcher, useSWRMutation } from "units/swr"; +import { Key, NetworkError, mutationFetcher, useGuardedMutation } from "units/swr"; import { usePingTrigger } from "../hooks"; type Props = { @@ -48,20 +48,18 @@ const MachineContainer: React.FC = ({ machine, viewMode }) => { onSuccess: manageActionResponse }); - const { trigger: shutdownMachineTrigger } = useSWRMutation( + const { trigger: shutdownMachineTrigger } = useGuardedMutation( endpoints.network.machine.shutdown, mutationFetcher, { - onError: err => blip.error(err.message), onSuccess: manageActionResponse } ); - const { trigger: restartMachineTrigger } = useSWRMutation( + const { trigger: restartMachineTrigger } = useGuardedMutation( endpoints.network.machine.restart, mutationFetcher, { - onError: err => blip.error(err.serverError.message || err.message), onSuccess: manageActionResponse } ); diff --git a/frontend/src/features/machines/hooks/usePingTrigger.ts b/frontend/src/features/machines/hooks/usePingTrigger.ts index c9264ea..f725132 100644 --- a/frontend/src/features/machines/hooks/usePingTrigger.ts +++ b/frontend/src/features/machines/hooks/usePingTrigger.ts @@ -17,7 +17,8 @@ const usePingTrigger = (options: PingTriggerOptions) => { mutationFetcher, { onError, - onSuccess + onSuccess, + throwOnError: false } ); return { pingMachineTrigger }; diff --git a/frontend/src/features/settings/system/CacheSettingsContainer.tsx b/frontend/src/features/settings/system/CacheSettingsContainer.tsx index 2ee1ee1..39cec63 100644 --- a/frontend/src/features/settings/system/CacheSettingsContainer.tsx +++ b/frontend/src/features/settings/system/CacheSettingsContainer.tsx @@ -3,18 +3,15 @@ import CacheSettingsComponent from "./CacheSettingsComponent"; import { useTranslation } from "react-i18next"; import { endpoints } from "utils/api"; import { blip } from "utils"; -import { useSWRMutation, mutationFetcher, Key, NetworkError } from "units/swr"; +import { useGuardedMutation, mutationFetcher, Key, NetworkError } from "units/swr"; const CacheSettingsContainer: React.FC = () => { const { t } = useTranslation(); - const { trigger } = useSWRMutation( + const { trigger } = useGuardedMutation( endpoints.system.resetCache, mutationFetcher, { - onError: err => { - blip.error(err.message); - }, onSuccess: () => blip.info(t("Settings.Cache.ResetInfo")) } ); diff --git a/frontend/src/units/swr/hooks/index.ts b/frontend/src/units/swr/hooks/index.ts new file mode 100644 index 0000000..d49e535 --- /dev/null +++ b/frontend/src/units/swr/hooks/index.ts @@ -0,0 +1,3 @@ +import useGuardedMutation from "./useGuardedMutation"; + +export { useGuardedMutation }; diff --git a/frontend/src/units/swr/hooks/useGuardedMutation.ts b/frontend/src/units/swr/hooks/useGuardedMutation.ts new file mode 100644 index 0000000..c2f0897 --- /dev/null +++ b/frontend/src/units/swr/hooks/useGuardedMutation.ts @@ -0,0 +1,39 @@ +import useSWRMutation, { MutationFetcher, SWRMutationConfiguration, SWRMutationResponse } from "swr/mutation"; +import { Key, NetworkError } from "../../swr"; +import { blip } from "utils"; +import { useMemo } from "react"; + +const defaultErrorHandler = (error: Error) => { + const isNetworkError = error instanceof NetworkError; + if (isNetworkError) { + blip.error(error.serverError.message || error.message); + return; + } + blip.error(error.message); +}; + +function useGuardedMutation< + Data = any, + Error = any, + SWRMutationKey extends Key = Key, + ExtraArg = never, + SWRData = Data +>( + key: SWRMutationKey, + fetcher: MutationFetcher, + options?: SWRMutationConfiguration & { throwOnError?: boolean } +): SWRMutationResponse { + const opts = useMemo( + () => + ({ + ...options, + onError: options?.onError ?? defaultErrorHandler, + throwOnError: options?.throwOnError ?? false + } as SWRMutationConfiguration), + [options] + ); + + return useSWRMutation(key, fetcher, opts); +} + +export default useGuardedMutation; diff --git a/frontend/src/units/swr/index.ts b/frontend/src/units/swr/index.ts index f5d3e5c..0480844 100644 --- a/frontend/src/units/swr/index.ts +++ b/frontend/src/units/swr/index.ts @@ -4,6 +4,7 @@ import useSWRMutation from "swr/mutation"; export * from "./fetchers"; export * from "./errors"; +export * from "./hooks"; export { useSWR, useSWRMutation }; export type { Key };