Compare commits

...

33 Commits

Author SHA1 Message Date
Tudor Stanciu b597fcffa4 Merged PR 84: SWR integration
https://swr.vercel.app/
2024-11-16 12:14:09 +00:00
Tudor Stanciu feb2ab11f9 Implement useGuardedMutation hook for enhanced error handling in MachineContainer and update related components 2024-11-16 14:11:32 +02:00
Tudor Stanciu 181c565b3c Remove debugger statements and streamline error handling in MachineContainer 2024-11-16 04:16:10 +02:00
Tudor Stanciu 82a4750c76 Refactor SWR mutation handling: replace Error with NetworkError for improved error management and add error handling in fetchers 2024-11-16 04:05:16 +02:00
Tudor Stanciu 8f9c03f400 Remove commented-out deleteFetcher function from fetchers.ts to clean up code 2024-11-16 02:47:51 +02:00
Tudor Stanciu a664a4ade9 Bump version to 1.3.1 and update release notes with SWR integration and progress bar implementation 2024-11-16 02:36:15 +02:00
Tudor Stanciu 52af1ef10b Refactor axios.js: remove unused axios utility functions to streamline codebase 2024-11-16 02:17:49 +02:00
Tudor Stanciu 49ac48e1f8 Refactor api.js: migrate from JavaScript to TypeScript, enhancing type safety and maintainability 2024-11-16 02:14:30 +02:00
Tudor Stanciu 0a3e402013 Refactor WakeComponent: enable periodic ping retriggering by uncommenting setTimeout logic 2024-11-16 02:13:12 +02:00
Tudor Stanciu 174f383968 Refactor api.js: rename powerActionsRoute to resurrectorRoute for clarity in API endpoint usage 2024-11-16 02:12:47 +02:00
Tudor Stanciu ad27fa477d Refactor api.js: simplify error handling, remove unused functions, and rename routes to endpoints 2024-11-16 02:12:21 +02:00
Tudor Stanciu 04e80a0ac0 Refactor WakeComponent: integrate useSWRMutation for waking machines, update API endpoint usage, and enhance type definitions 2024-11-16 02:11:09 +02:00
Tudor Stanciu 618bfb38e1 Refactor WakeComponent: update ping interval and starting time handling, streamline ping logic, and improve dependency management 2024-11-16 02:03:47 +02:00
Tudor Stanciu 09e447f4b3 Refactor MachineContainer and WakeComponent: implement usePingTrigger for pinging machines, update action result types, and clean up API calls 2024-11-16 01:52:47 +02:00
Tudor Stanciu 7234b857a6 Refactor MachineContainer: convert to TypeScript, implement SWR for data fetching, and update action handling 2024-11-16 01:29:18 +02:00
Tudor Stanciu 253ee1953c Refactor WakeComponent: convert to TypeScript, update API routes, and enhance logging functionality 2024-11-11 02:13:03 +02:00
Tudor Stanciu 8148da132d Refactor MachinesContainer: convert to TypeScript, implement SWR for data fetching, and update API routes 2024-11-11 02:04:02 +02:00
Tudor Stanciu 338aa17fe8 Refactor SWR imports: consolidate fetcher and mutationFetcher into units/swr, update component imports accordingly 2024-11-11 01:54:01 +02:00
Tudor Stanciu 0468af03be Refactor UserPermissionsProvider to TypeScript, remove unused permissions API routes, and enhance cache reset functionality 2024-11-11 01:48:15 +02:00
Tudor Stanciu ca388cb639 Refactor cache reset functionality: replace old CacheSettingsContainer with TypeScript version, update SystemController to handle reset cache, and enhance SWR mutation fetcher. 2024-11-11 01:29:07 +02:00
Tudor Stanciu 8af7b64a60 Add version display to TimelineComponent in release notes 2024-11-10 23:25:14 +02:00
Tudor Stanciu 19415f312a Refactor ReleaseNotesContainer to TypeScript, implement SWR for data fetching, and update release note structure 2024-11-10 23:21:37 +02:00
Tudor Stanciu 9389058b1f Refactor SystemVersionComponent to update styling and add dtos to types 2024-11-10 02:27:30 +02:00
Tudor Stanciu 5f234938b8 Refactor SystemVersionComponent to remove unused imports and update styling 2024-11-10 02:19:57 +02:00
Tudor Stanciu 97b22c7047 Refactor SystemVersionComponent and add dtos to types 2024-11-10 02:16:25 +02:00
Tudor Stanciu 74a176b9aa Refactor SystemVersionContainer and add dtos to types 2024-11-10 02:05:32 +02:00
Tudor Stanciu c5ba810605 blip toast proxy 2024-11-10 01:52:39 +02:00
Tudor Stanciu f8d0d7c486 Refactor handleOpenInNewTab function in AboutSystemContainer.js 2024-11-10 01:12:01 +02:00
Tudor Stanciu 1ba19eb96c Refactor handleOpenInNewTab function in AboutSystemContainer.js 2024-11-10 01:11:02 +02:00
Tudor Stanciu 0a3257dca3 move themes in units 2024-08-16 00:58:40 +03:00
Tudor Stanciu b1fa0e1c6a refactor: Update progress bar component and provider 2024-08-16 00:52:54 +03:00
Tudor Stanciu 8a95bd9886 dockerfile fix 2024-07-14 03:06:01 +03:00
Tudor Stanciu 958fa44344 Upgrade Node.js version in frontend Dockerfile 2024-07-14 02:55:05 +03:00
52 changed files with 770 additions and 518 deletions

View File

@ -1,7 +1,7 @@
<Project> <Project>
<Import Project="dependencies.props" /> <Import Project="dependencies.props" />
<PropertyGroup> <PropertyGroup>
<Version>1.3.0</Version> <Version>1.3.1</Version>
<Authors>Tudor Stanciu</Authors> <Authors>Tudor Stanciu</Authors>
<Company>STA</Company> <Company>STA</Company>
<PackageTags>NetworkResurrector</PackageTags> <PackageTags>NetworkResurrector</PackageTags>

View File

@ -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. • 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. • 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. • 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.
</Content>
</Note>
<Note>
<Version>1.3.1</Version>
<Date>2024-11-16 02:28</Date>
<Content>
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.
</Content> </Content>
</Note> </Note>
</ReleaseNotes> </ReleaseNotes>

View File

@ -44,8 +44,9 @@ namespace NetworkResurrector.Api.Controllers
[HttpPost("reset-cache")] [HttpPost("reset-cache")]
[Authorize(Policy = Policies.SystemAdministration)] [Authorize(Policy = Policies.SystemAdministration)]
public async Task<IActionResult> WakeMachine([FromBody] ResetCache resetCache) public async Task<IActionResult> ResetCache()
{ {
var resetCache = new ResetCache();
var result = await _mediator.Send(resetCache); var result = await _mediator.Send(resetCache);
return Ok(result); return Ok(result);
} }

View File

@ -1,5 +1,5 @@
# BUILD ENVIRONMENT # BUILD ENVIRONMENT
FROM node:14-slim as builder FROM node:16-slim AS builder
WORKDIR /app WORKDIR /app
ARG APP_SUBFOLDER="" ARG APP_SUBFOLDER=""
@ -15,7 +15,7 @@ COPY . ./
RUN if [ -z "$APP_SUBFOLDER" ]; then npm run build; else PUBLIC_URL=/${APP_SUBFOLDER}/ npm run build; fi RUN if [ -z "$APP_SUBFOLDER" ]; then npm run build; else PUBLIC_URL=/${APP_SUBFOLDER}/ npm run build; fi
# PRODUCTION ENVIRONMENT # PRODUCTION ENVIRONMENT
FROM node:14-slim FROM node:16-slim
ARG APP_SUBFOLDER="" ARG APP_SUBFOLDER=""

View File

@ -1,12 +1,12 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.3.0", "version": "1.3.1",
"lockfileVersion": 2, "lockfileVersion": 2,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.3.0", "version": "1.3.1",
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.1", "@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
@ -19,6 +19,7 @@
"i18next": "^22.4.15", "i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0", "i18next-http-backend": "^2.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -26,10 +27,12 @@
"react-lazylog": "^4.5.3", "react-lazylog": "^4.5.3",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"react-world-flags": "^1.6.0" "react-world-flags": "^1.6.0",
"swr": "^2.2.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/lodash": "^4.17.7",
"@types/react": "^18.2.33", "@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.14",
"@types/react-world-flags": "^1.4.5", "@types/react-world-flags": "^1.4.5",
@ -4890,6 +4893,12 @@
"dev": true, "dev": true,
"peer": 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": { "node_modules/@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz",
@ -6973,6 +6982,11 @@
"node": ">=0.10.0" "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": { "node_modules/cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
@ -19483,6 +19497,18 @@
"boolbase": "~1.0.0" "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": { "node_modules/symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -20209,6 +20235,14 @@
"requires-port": "^1.0.0" "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": { "node_modules/util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
@ -24773,6 +24807,12 @@
"dev": true, "dev": true,
"peer": 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": { "@types/mime": {
"version": "1.3.5", "version": "1.3.5",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", "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": { "cliui": {
"version": "7.0.4", "version": "7.0.4",
"resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", "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": { "symbol-tree": {
"version": "3.2.4", "version": "3.2.4",
"resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz",
@ -36186,6 +36240,12 @@
"requires-port": "^1.0.0" "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": { "util-deprecate": {
"version": "1.0.2", "version": "1.0.2",
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.3.0", "version": "1.3.1",
"description": "Frontend component of Network resurrector system", "description": "Frontend component of Network resurrector system",
"author": { "author": {
"name": "Tudor Stanciu", "name": "Tudor Stanciu",
@ -24,6 +24,7 @@
"i18next": "^22.4.15", "i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1", "i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0", "i18next-http-backend": "^2.2.0",
"lodash": "^4.17.21",
"moment": "^2.29.4", "moment": "^2.29.4",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
@ -31,10 +32,12 @@
"react-lazylog": "^4.5.3", "react-lazylog": "^4.5.3",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-toastify": "^9.1.3", "react-toastify": "^9.1.3",
"react-world-flags": "^1.6.0" "react-world-flags": "^1.6.0",
"swr": "^2.2.5"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/lodash": "^4.17.7",
"@types/react": "^18.2.33", "@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14", "@types/react-dom": "^18.2.14",
"@types/react-world-flags": "^1.4.5", "@types/react-world-flags": "^1.4.5",

View File

@ -6,17 +6,16 @@ import AccountBoxIcon from "@mui/icons-material/AccountBox";
import SettingsIcon from "@mui/icons-material/Settings"; import SettingsIcon from "@mui/icons-material/Settings";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { useTuitioClient } from "@flare/tuitio-client-react"; import { useTuitioClient } from "@flare/tuitio-client-react";
import { useToast } from "../../hooks"; import { blip } from "../../utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
const ProfileButton = () => { const ProfileButton = () => {
const navigate = useNavigate(); const navigate = useNavigate();
const { error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { logout } = useTuitioClient({ const { logout } = useTuitioClient({
onLogoutFailed: errorMessage => error(errorMessage), onLogoutFailed: errorMessage => blip.error(errorMessage),
onLogoutError: err => error(err.message) onLogoutError: err => blip.error(err.message)
}); });
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);

View File

@ -7,6 +7,7 @@ import LightDarkToggle from "./LightDarkToggle";
import SensitiveInfoToggle from "./SensitiveInfoToggle"; import SensitiveInfoToggle from "./SensitiveInfoToggle";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { drawerWidth } from "./constants"; import { drawerWidth } from "./constants";
import { ProgressBar } from "units/progress";
interface AppBarProps extends MuiAppBarProps { interface AppBarProps extends MuiAppBarProps {
open?: boolean; open?: boolean;
@ -64,7 +65,7 @@ const TopBar: React.FC<TopBarProps> = ({ open, onDrawerOpen }) => {
<ProfileButton /> <ProfileButton />
</Box> </Box>
</Toolbar> </Toolbar>
{/* <ProgressBar /> */} <ProgressBar />
</AppBar> </AppBar>
); );
}; };

View File

@ -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" ? (
<TimelineComponent releases={sort(state.data)} />
) : (
<ReleaseNotesList releases={sort(state.data)} />
))}
</>
);
};
ReleaseNotesContainer.propTypes = {
view: PropTypes.string
};
export default ReleaseNotesContainer;

View File

@ -0,0 +1,31 @@
import React from "react";
import ReleaseNotesList from "./ReleaseNotesList";
import TimelineComponent from "../timeline/TimelineComponent";
import { endpoints } from "../../../utils/api";
import { useSWR, fetcher } from "units/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<Props> = ({ view }) => {
const { data, isLoading } = useSWR<dtos.ReleaseNote[], Error>(endpoints.system.releaseNotes, fetcher, {
revalidateOnFocus: false,
onError: err => blip.error(err.message)
});
if (isLoading || !data) return null;
return (
<>
{view === "timeline" ? <TimelineComponent releases={sort(data)} /> : <ReleaseNotesList releases={sort(data)} />}
</>
);
};
export default ReleaseNotesContainer;

View File

@ -38,7 +38,7 @@ const buttons = [
} }
]; ];
const AboutSystemComponent = ({ handleOpenInNewTab }) => { const AboutSystemComponent = ({ onOpenInNewTab }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const bullet = <span style={styles.bullet}></span>; const bullet = <span style={styles.bullet}></span>;
@ -80,7 +80,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => {
size="small" size="small"
color="primary" color="primary"
startIcon={<OpenInNewIcon />} startIcon={<OpenInNewIcon />}
onClick={handleOpenInNewTab(button.url)} onClick={onOpenInNewTab(button.url)}
> >
{t(button.code)} {t(button.code)}
</Button> </Button>
@ -91,7 +91,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => {
}; };
AboutSystemComponent.propTypes = { AboutSystemComponent.propTypes = {
handleOpenInNewTab: PropTypes.func.isRequired onOpenInNewTab: PropTypes.func.isRequired
}; };
export default AboutSystemComponent; export default AboutSystemComponent;

View File

@ -13,15 +13,15 @@ const styles = {
} }
}; };
const AboutSystemContainer = () => { const handleOpenInNewTab = url => event => {
const handleOpenInNewTab = url => event => {
window.open(url, "_blank"); window.open(url, "_blank");
event.preventDefault(); event.preventDefault();
}; };
const AboutSystemContainer = () => {
return ( return (
<Box sx={styles.page}> <Box sx={styles.page}>
<AboutSystemComponent handleOpenInNewTab={handleOpenInNewTab} /> <AboutSystemComponent onOpenInNewTab={handleOpenInNewTab} />
<Box sx={styles.element}> <Box sx={styles.element}>
<SystemVersionContainer /> <SystemVersionContainer />
</Box> </Box>

View File

@ -1,6 +1,5 @@
import React, { useMemo, useEffect, useState } from "react"; import React, { useMemo } from "react";
import PropTypes from "prop-types"; import { List, ListItem, ListItemText, ListItemAvatar, styled } from "@mui/material";
import { List, ListItem, ListItemText, ListItemAvatar } from "@mui/material";
import Avatar from "@mui/material/Avatar"; import Avatar from "@mui/material/Avatar";
import WebAssetIcon from "@mui/icons-material/WebAsset"; import WebAssetIcon from "@mui/icons-material/WebAsset";
import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard"; import DeveloperBoardIcon from "@mui/icons-material/DeveloperBoard";
@ -8,47 +7,23 @@ import SettingsInputSvideoIcon from "@mui/icons-material/SettingsInputSvideo";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import packageData from "../../../../package.json"; import packageData from "../../../../package.json";
import Paper from "@mui/material/Paper"; import Paper from "@mui/material/Paper";
import { useTheme } from "@mui/material/styles"; import { dtos } from "types";
const getStyles = theme => ({ const VersionAvatar = styled(Avatar)(({ theme }) => ({
horizontally: { backgroundColor: theme.palette.secondary.main
display: "flex", }));
flexDirection: "row",
padding: 0 const VersionLabel = styled("span")(({ theme }) => ({
},
vertical: {
width: "100%"
},
value: {
fontSize: "0.9rem", fontSize: "0.9rem",
fontWeight: theme.typography.fontWeightMedium fontWeight: theme.typography.fontWeightMedium
}, }));
versionAvatar: {
backgroundColor: theme.palette.secondary.main
}
});
const SystemVersionComponent = ({ data }) => { type Props = {
data: dtos.SystemVersion;
};
const SystemVersionComponent: React.FC<Props> = ({ data }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const theme = useTheme();
const styles = getStyles(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;
setListClass(cssClass);
}
handleMatches(mediaQuery);
mediaQuery.addListener(handleMatches);
return () => {
mediaQuery.removeListener(handleMatches);
};
}, [styles.horizontally, styles.vertical]);
const lastReleaseDate = useMemo(() => { const lastReleaseDate = useMemo(() => {
const format = "DD-MM-YYYY HH:mm:ss"; const format = "DD-MM-YYYY HH:mm:ss";
@ -78,20 +53,27 @@ const SystemVersionComponent = ({ data }) => {
return ( return (
<Paper variant="outlined"> <Paper variant="outlined">
<List sx={listClass}> <List
sx={{
display: { xs: "block", md: "flex" },
flexDirection: { md: "row" },
padding: 0,
width: "100%"
}}
>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={styles.versionAvatar}> <VersionAvatar>
<DeveloperBoardIcon /> <DeveloperBoardIcon />
</Avatar> </VersionAvatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={
<span style={styles.value}> <VersionLabel>
{t("About.System.Version.Server", { {t("About.System.Version.Server", {
version: data.server.version version: data.server.version
})} })}
</span> </VersionLabel>
} }
secondary={t("About.System.Version.LastReleaseDate", { secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.server date: lastReleaseDate.server
@ -100,17 +82,17 @@ const SystemVersionComponent = ({ data }) => {
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={styles.versionAvatar}> <VersionAvatar>
<SettingsInputSvideoIcon /> <SettingsInputSvideoIcon />
</Avatar> </VersionAvatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={
<span style={styles.value}> <VersionLabel>
{t("About.System.Version.Api", { {t("About.System.Version.Api", {
version: data.api.version version: data.api.version
})} })}
</span> </VersionLabel>
} }
secondary={t("About.System.Version.LastReleaseDate", { secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.api date: lastReleaseDate.api
@ -119,17 +101,17 @@ const SystemVersionComponent = ({ data }) => {
</ListItem> </ListItem>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<Avatar sx={styles.versionAvatar}> <VersionAvatar>
<WebAssetIcon /> <WebAssetIcon />
</Avatar> </VersionAvatar>
</ListItemAvatar> </ListItemAvatar>
<ListItemText <ListItemText
primary={ primary={
<span style={styles.value}> <VersionLabel>
{t("About.System.Version.Frontend", { {t("About.System.Version.Frontend", {
version: process.env.APP_VERSION ?? packageData.version version: process.env.APP_VERSION ?? packageData.version
})} })}
</span> </VersionLabel>
} }
secondary={t("About.System.Version.LastReleaseDate", { secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.frontend date: lastReleaseDate.frontend
@ -141,8 +123,4 @@ const SystemVersionComponent = ({ data }) => {
); );
}; };
SystemVersionComponent.propTypes = {
data: PropTypes.object.isRequired
};
export default SystemVersionComponent; export default SystemVersionComponent;

View File

@ -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 && <SystemVersionComponent data={state.data} />}</>;
};
export default SystemVersionContainer;

View File

@ -0,0 +1,18 @@
import React from "react";
import SystemVersionComponent from "./SystemVersionComponent";
import { endpoints } from "../../../utils/api";
import { useSWR, fetcher } from "units/swr";
import { blip } from "utils";
import { dtos } from "types";
const SystemVersionContainer: React.FC = () => {
const { data, isLoading } = useSWR<dtos.SystemVersion, Error>(endpoints.system.version, fetcher, {
revalidateOnFocus: false,
onError: err => blip.error(err.message)
});
if (isLoading || !data) return null;
return <SystemVersionComponent data={data} />;
};
export default SystemVersionContainer;

View File

@ -74,6 +74,9 @@ const TimelineComponent = ({ releases }) => {
date: { value: release.date, format: "DD-MM-YYYY HH:mm" } date: { value: release.date, format: "DD-MM-YYYY HH:mm" }
})} })}
</Typography> </Typography>
<Typography variant="body2" color="textSecondary">
{t("About.ReleaseNotes.Version")}: {release.version}
</Typography>
</TimelineOppositeContent> </TimelineOppositeContent>
<TimelineSeparator> <TimelineSeparator>
<TimelineDot color={release.dot.color} variant={release.dot.variant}> <TimelineDot color={release.dot.color} variant={release.dot.variant}>

View File

@ -1,6 +1,6 @@
import React, { useState } from "react"; import React, { useState } from "react";
import LoginCard from "./LoginCard"; import LoginCard from "./LoginCard";
import { useToast } from "../../../hooks"; import { blip } from "../../../utils";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useTuitioClient } from "@flare/tuitio-client-react"; import { useTuitioClient } from "@flare/tuitio-client-react";
@ -10,11 +10,10 @@ const LoginContainer = () => {
password: "" password: ""
}); });
const { error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { login } = useTuitioClient({ const { login } = useTuitioClient({
onLoginFailed: () => error(t("Login.IncorrectCredentials")), onLoginFailed: () => blip.error(t("Login.IncorrectCredentials")),
onLoginError: err => error(err.message) onLoginError: err => blip.error(err.message)
}); });
const handleChange = prop => event => { const handleChange = prop => event => {

View File

@ -66,9 +66,9 @@ const GridCell: React.FC<GridCellProps> = ({ label, value }) => {
type Props = { type Props = {
machine: models.Machine; machine: models.Machine;
actions: Array<any>; // Replace any with the actual type of the actions actions: Array<any>;
logs: Array<any>; // Replace any with the actual type of the logs logs: Array<any>;
addLog: () => void; // Replace with the actual function signature addLog: (message: string) => void;
}; };
const MachineAccordion: React.FC<Props> = ({ machine, actions, logs, addLog }) => { const MachineAccordion: React.FC<Props> = ({ machine, actions, logs, addLog }) => {

View File

@ -1,75 +1,82 @@
import React, { useState, useCallback } from "react"; import React, { useState, useCallback } from "react";
import PropTypes from "prop-types";
import MachineTableRow from "./MachineTableRow"; import MachineTableRow from "./MachineTableRow";
import MachineAccordion from "./MachineAccordion"; import MachineAccordion from "./MachineAccordion";
import { ViewModes } from "./ViewModeSelection"; import { ViewModes } from "./ViewModeSelection";
import { useToast } from "../../../hooks"; import { blip } from "../../../utils";
import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material"; import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { routes, post } from "../../../utils/api"; import { endpoints } from "../../../utils/api";
import {
Machine,
MachineActionResult,
MachineRestarted,
MachineShutdown,
RestartMachine,
ShutdownMachine
} from "types";
import { Key, NetworkError, mutationFetcher, useGuardedMutation } from "units/swr";
import { usePingTrigger } from "../hooks";
const MachineContainer = ({ machine, viewMode }) => { type Props = {
const [logs, setLogs] = useState([]); machine: Machine;
viewMode: string;
const { success, error } = useToast(); };
const MachineContainer: React.FC<Props> = ({ machine, viewMode }) => {
const [logs, setLogs] = useState<string[]>([]);
const { t } = useTranslation(); const { t } = useTranslation();
const addLog = useCallback( const addLog = useCallback(
text => { (text: string) => {
setLogs(prev => [...prev, text]); setLogs(prev => [...prev, text]);
}, },
[setLogs] [setLogs]
); );
const manageActionResponse = useCallback( const manageActionResponse = useCallback(
response => { (response: MachineActionResult) => {
addLog(`Success: ${response.success}. Status: ${response.status}`); addLog(`Success: ${response.success}. Status: ${response.status}`);
if (response.success) { if (response.success) {
success(response.status); blip.success(response.status);
} else { } else {
error(response.status); blip.error(response.status);
} }
}, },
[error, success, addLog] [addLog]
);
const { pingMachineTrigger } = usePingTrigger({
onSuccess: manageActionResponse
});
const { trigger: shutdownMachineTrigger } = useGuardedMutation<MachineShutdown, NetworkError, Key, ShutdownMachine>(
endpoints.network.machine.shutdown,
mutationFetcher<ShutdownMachine>,
{
onSuccess: manageActionResponse
}
);
const { trigger: restartMachineTrigger } = useGuardedMutation<MachineRestarted, NetworkError, Key, RestartMachine>(
endpoints.network.machine.restart,
mutationFetcher<RestartMachine>,
{
onSuccess: manageActionResponse
}
); );
const pingMachine = useCallback( const pingMachine = useCallback(
async machine => { async (machine: Machine) => pingMachineTrigger({ machineId: machine.machineId }),
await post( [pingMachineTrigger]
routes.pingMachine,
{ machineId: machine.machineId },
{
onCompleted: manageActionResponse
}
);
},
[manageActionResponse]
); );
const shutdownMachine = useCallback( const shutdownMachine = useCallback(
async machine => { async (machine: Machine) => shutdownMachineTrigger({ machineId: machine.machineId, delay: 0, force: false }),
await post( [shutdownMachineTrigger]
routes.shutdownMachine,
{ machineId: machine.machineId, delay: 0, force: false },
{
onCompleted: manageActionResponse
}
);
},
[manageActionResponse]
); );
const restartMachine = useCallback( const restartMachine = useCallback(
async machine => { async (machine: Machine) => restartMachineTrigger({ machineId: machine.machineId, delay: 0, force: false }),
await post( [restartMachineTrigger]
routes.restartMachine,
{ machineId: machine.machineId, delay: 0, force: false },
{
onCompleted: manageActionResponse
}
);
},
[manageActionResponse]
); );
const actions = [ const actions = [
@ -117,9 +124,4 @@ const MachineContainer = ({ machine, viewMode }) => {
); );
}; };
MachineContainer.propTypes = {
machine: PropTypes.object.isRequired,
viewMode: PropTypes.string.isRequired
};
export default MachineContainer; export default MachineContainer;

View File

@ -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 { NetworkStateContext, NetworkDispatchContext } from "../../network/state/contexts";
import MachinesListComponent from "./MachinesListComponent"; import MachinesListComponent from "./MachinesListComponent";
import PageTitle from "../../../components/common/PageTitle"; import PageTitle from "../../../components/common/PageTitle";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ViewModeSelection, { ViewModes } from "./ViewModeSelection"; 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 [viewMode, setViewMode] = useState(null);
const state = useContext(NetworkStateContext); const state = useContext(NetworkStateContext);
const dispatchActions = useContext(NetworkDispatchContext); const dispatchActions = useContext(NetworkDispatchContext);
const { t } = useTranslation(); const { t } = useTranslation();
const handleReadMachines = useCallback(async () => { const url = state.network.machines.loaded ? null : endpoints.network.machines;
await get(routes.machines, { useSWR<Machine[], Error>(url, fetcher, {
onCompleted: machines => { revalidateOnFocus: false,
onError: err => blip.error(err.message),
onSuccess: machines => {
const data = Object.assign(machines, { loaded: true }); const data = Object.assign(machines, { loaded: true });
dispatchActions.onNetworkChange("machines", data); dispatchActions.onNetworkChange("machines", data);
} }
}); });
}, [dispatchActions]);
useEffect(() => {
if (!state.network.machines.loaded) {
handleReadMachines();
}
}, [handleReadMachines, state.network.machines.loaded]);
return ( return (
<> <>

View File

@ -1,115 +0,0 @@
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 { useToast } from "../../../../hooks";
import { msToMinAndSec } from "../../../../utils/time";
import { routes, post } from "../../../../utils/api";
const initialState = { on: false };
const defaultPingInterval = 1200000; //20 minutes
const defaultStartingTime = 300000; //5 minutes
const WakeComponent = ({ machine, addLog, disabled }) => {
const [state, setState] = useState(initialState);
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;
const getCurrentDateTime = useCallback(() => {
const currentDateTime = Date.now();
const result = t("DATE_FORMAT", {
date: { value: currentDateTime, format: "DD-MM-YYYY HH:mm:ss" }
});
return result;
}, [t]);
const log = useCallback(message => addLog(`[${getCurrentDateTime()}] ${message}`), [addLog, getCurrentDateTime]);
const wakeMachine = useCallback(async () => {
await post(
routes.wakeMachine,
{ machineId: machine.machineId },
{
onCompleted: result => {
setState(prev => ({ ...prev, on: result.success }));
log(`[Wake]: Success: ${result.success}. Status: ${result.status}`);
if (result.success) {
success(result.status);
//retrigger
log(`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(startingTime)}]`);
setTimeout(() => {
setTrigger(prev => !prev);
}, startingTime);
} else {
error(result.status);
}
}
}
);
}, [log, success, error, startingTime, machine.machineId]);
const pingInLoop = useCallback(async () => {
if (disabled) return;
await post(
routes.pingMachine,
{ machineId: machine.machineId },
{
onCompleted: 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
}
}
);
}, [machine, log, pingInterval, disabled]);
useEffect(() => {
pingInLoop();
}, [trigger, pingInLoop]);
const handleWakeClick = event => {
wakeMachine();
event.stopPropagation();
};
return (
<Tooltip title={t(state.on ? "Machine.PoweredOn" : "Machine.Actions.Wake")}>
<span>
<IconButton
id={`machine-${machine.machineId}-wake`}
size={"small"}
disabled={disabled || state.on}
onClick={handleWakeClick}
sx={{ padding: "0.2rem" }}
style={state.on ? { color: "#33cc33" } : undefined}
onFocus={event => event.stopPropagation()}
>
<PowerSettingsNew />
</IconButton>
</span>
</Tooltip>
);
};
WakeComponent.propTypes = {
machine: PropTypes.object.isRequired,
addLog: PropTypes.func.isRequired,
disabled: PropTypes.bool
};
export default WakeComponent;

View File

@ -0,0 +1,117 @@
import React, { useState, useEffect, useCallback } from "react";
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 { endpoints } from "../../../../utils/api";
import { Machine, MachineWaked, WakeMachine } from "types";
import { usePingTrigger } from "../../hooks";
import { Key, mutationFetcher, NetworkError, useSWRMutation } from "units/swr";
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;
disabled: boolean;
};
const WakeComponent: React.FC<Props> = ({ machine, addLog, disabled }) => {
const [state, setState] = useState(initialState);
const [trigger, setTrigger] = useState(false);
const { t } = useTranslation();
const getCurrentDateTime = useCallback(() => {
const currentDateTime = Date.now();
const result = t("DATE_FORMAT", {
date: { value: currentDateTime, format: "DD-MM-YYYY HH:mm:ss" }
});
return result;
}, [t]);
const log = useCallback(
(message: string) => addLog(`[${getCurrentDateTime()}] ${message}`),
[addLog, getCurrentDateTime]
);
const { pingMachineTrigger } = usePingTrigger({
onSuccess: result => {
setState(prev => ({ ...prev, on: result.success }));
log(`[Ping]: Success: ${result.success}. Status: ${result.status}`);
// trigger the next ping
if (result.success) {
setTimeout(() => {
setTrigger(prev => !prev);
}, pingInterval);
}
},
onError: () => {
// to do: handle error
}
});
const { trigger: wakeMachineTrigger } = useSWRMutation<MachineWaked, NetworkError, Key, WakeMachine>(
endpoints.network.machine.wake,
mutationFetcher<WakeMachine>,
{
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);
}
}
}
);
const pingMachine = useCallback(async () => {
if (disabled) return;
pingMachineTrigger({ machineId: machine.machineId });
}, [machine.machineId, pingMachineTrigger, disabled]);
useEffect(() => {
pingMachine();
}, [trigger, pingMachine]);
const handleWakeClick = (event: React.MouseEvent<HTMLButtonElement>) => {
wakeMachineTrigger({ machineId: machine.machineId });
event.stopPropagation();
};
return (
<Tooltip title={t(state.on ? "Machine.PoweredOn" : "Machine.Actions.Wake")}>
<span>
<IconButton
id={`machine-${machine.machineId}-wake`}
size={"small"}
disabled={disabled || state.on}
onClick={handleWakeClick}
sx={{ padding: "0.2rem" }}
style={state.on ? { color: "#33cc33" } : undefined}
onFocus={event => event.stopPropagation()}
>
<PowerSettingsNew />
</IconButton>
</span>
</Tooltip>
);
};
export default WakeComponent;

View File

@ -0,0 +1,3 @@
import usePingTrigger from "./usePingTrigger";
export { usePingTrigger };

View File

@ -0,0 +1,26 @@
import { useMemo } from "react";
import { MachinePinged, PingMachine } from "types";
import { Key, mutationFetcher, NetworkError, 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<MachinePinged, NetworkError, Key, PingMachine>(
endpoints.network.machine.ping,
mutationFetcher<PingMachine>,
{
onError,
onSuccess,
throwOnError: false
}
);
return { pingMachineTrigger };
};
export default usePingTrigger;

View File

@ -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 { info } from "utils/toast";
const CacheSettingsContainer = () => {
const { t } = useTranslation();
const handleResetCache = useCallback(async () => {
await post(
routes.resetCache,
{},
{
onCompleted: () => info(t("Settings.Cache.ResetInfo"))
}
);
}, [t]);
return <CacheSettingsComponent onResetCache={handleResetCache} />;
};
export default CacheSettingsContainer;

View File

@ -0,0 +1,24 @@
import React, { useCallback } from "react";
import CacheSettingsComponent from "./CacheSettingsComponent";
import { useTranslation } from "react-i18next";
import { endpoints } from "utils/api";
import { blip } from "utils";
import { useGuardedMutation, mutationFetcher, Key, NetworkError } from "units/swr";
const CacheSettingsContainer: React.FC = () => {
const { t } = useTranslation();
const { trigger } = useGuardedMutation<void, NetworkError, Key, void>(
endpoints.system.resetCache,
mutationFetcher<void>,
{
onSuccess: () => blip.info(t("Settings.Cache.ResetInfo"))
}
);
const handleResetCache = useCallback(() => trigger(), [trigger]);
return <CacheSettingsComponent onResetCache={handleResetCache} />;
};
export default CacheSettingsContainer;

View File

@ -1,6 +1,5 @@
import { useToast } from "./useToast";
import { useSensitiveInfo } from "../providers/SensitiveInfoProvider"; import { useSensitiveInfo } from "../providers/SensitiveInfoProvider";
import { usePermissions } from "../providers/UserPermissionsProvider"; import { usePermissions } from "../providers/UserPermissionsProvider";
import { useClipboard } from "./useClipboard"; import { useClipboard } from "./useClipboard";
export { useToast, useSensitiveInfo, usePermissions, useClipboard }; export { useSensitiveInfo, usePermissions, useClipboard };

View File

@ -1,16 +1,15 @@
import { useCallback } from "react"; import { useCallback } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useToast } from "./useToast"; import { blip } from "../utils";
const useClipboard = () => { const useClipboard = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { info } = useToast();
const copy = useCallback( const copy = useCallback(
url => () => { url => () => {
navigator.clipboard.writeText(url); navigator.clipboard.writeText(url);
info(t("Generic.CopiedToClipboard")); blip.info(t("Generic.CopiedToClipboard"));
}, },
[info, t] [t]
); );
return { copy }; return { copy };
}; };

View File

@ -1,5 +0,0 @@
import { info, success, warning, error, dark } from "utils/toast";
export const useToast = () => {
return { info, success, warning, error, dark };
};

View File

@ -2,7 +2,7 @@ import React, { useReducer, useMemo, useContext } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles"; import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
import { localStorage } from "@flare/js-utils"; import { localStorage } from "@flare/js-utils";
import { getThemes } from "../themes"; import { getThemes } from "../units/themes";
const ApplicationThemeContext = React.createContext(); const ApplicationThemeContext = React.createContext();
const LOCAL_STORAGE_COLOR_SCHEME_KEY = "network-resurrector-color-scheme"; const LOCAL_STORAGE_COLOR_SCHEME_KEY = "network-resurrector-color-scheme";

View File

@ -1,6 +1,8 @@
import React, { useState, useEffect, useContext, useMemo } from "react"; import React, { useState, useContext, useMemo, ReactNode } from "react";
import PropTypes from "prop-types"; import { endpoints } from "../utils/api";
import { routes, get } from "../utils/api"; import { PermissionsDto } from "types/dtos";
import { useSWR, fetcher } from "units/swr";
import { blip } from "utils";
const permissionCodes = { const permissionCodes = {
VIEW_DASHBOARD: "VIEW_DASHBOARD", VIEW_DASHBOARD: "VIEW_DASHBOARD",
@ -13,12 +15,17 @@ const permissionCodes = {
SYSTEM_ADMINISTRATION: "SYSTEM_ADMINISTRATION" SYSTEM_ADMINISTRATION: "SYSTEM_ADMINISTRATION"
}; };
const initialState = { type UserPermissionsContextPayload = {
permissions: string[];
loading: boolean;
};
const initialState: UserPermissionsContextPayload = {
permissions: [], permissions: [],
loading: true loading: true
}; };
const getPermissionFlags = permissions => { const getPermissionFlags = (permissions: string[]) => {
const viewDashboard = permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false; const viewDashboard = permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false;
const manageUsers = permissions.includes(permissionCodes.MANAGE_USERS) ?? false; const manageUsers = permissions.includes(permissionCodes.MANAGE_USERS) ?? false;
const manageSettings = permissions.includes(permissionCodes.MANAGE_SETTINGS) ?? false; const manageSettings = permissions.includes(permissionCodes.MANAGE_SETTINGS) ?? false;
@ -49,7 +56,7 @@ const getPermissionFlags = permissions => {
}; };
}; };
const UserPermissionsContext = React.createContext(initialState); const UserPermissionsContext = React.createContext<UserPermissionsContextPayload>(initialState);
const usePermissions = () => { const usePermissions = () => {
const { permissions, loading } = useContext(UserPermissionsContext); const { permissions, loading } = useContext(UserPermissionsContext);
@ -57,20 +64,21 @@ const usePermissions = () => {
return { loading, ...flags }; return { loading, ...flags };
}; };
const UserPermissionsProvider = ({ children }) => { type Props = {
const [permissions, setPermissions] = useState(initialState); children: ReactNode;
useEffect(() => {
get(routes.permissions, {
onCompleted: data => setPermissions({ ...data, loading: false })
});
}, []);
return <UserPermissionsContext.Provider value={permissions}>{children}</UserPermissionsContext.Provider>;
}; };
UserPermissionsProvider.propTypes = { const UserPermissionsProvider: React.FC<Props> = ({ children }) => {
children: PropTypes.node.isRequired const [state, setState] = useState<UserPermissionsContextPayload>(initialState);
const url = useMemo(() => (state.permissions?.length ? null : endpoints.security.permissions), [state.permissions]);
useSWR<PermissionsDto, Error>(url, fetcher, {
revalidateOnFocus: false,
onError: err => blip.error(err.message),
onSuccess: data => setState({ ...data, loading: false })
});
return <UserPermissionsContext.Provider value={state}>{children}</UserPermissionsContext.Provider>;
}; };
export { UserPermissionsProvider, usePermissions }; export { UserPermissionsProvider, usePermissions };

View File

@ -0,0 +1,19 @@
export type WakeMachine = {
machineId: number;
};
export type PingMachine = {
machineId: number;
};
export type ShutdownMachine = {
machineId: number;
delay?: number;
force?: boolean;
};
export type RestartMachine = {
machineId: number;
delay?: number;
force?: boolean;
};

View File

@ -0,0 +1,19 @@
export type SystemVersionElement = {
version: string;
lastReleaseDate: string;
};
export type SystemVersion = {
api: SystemVersionElement;
server: SystemVersionElement;
};
export type ReleaseNote = {
version: string;
date: string;
notes: string[];
};
export type PermissionsDto = {
permissions: string[];
};

View File

@ -0,0 +1,9 @@
export type MachineActionResult = {
success: boolean;
status: string;
};
export type MachineWaked = MachineActionResult;
export type MachinePinged = MachineActionResult;
export type MachineShutdown = MachineActionResult;
export type MachineRestarted = MachineActionResult;

View File

@ -1,5 +1,10 @@
import * as models from "./models"; 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 "./models";
export * from "./commands";
export * from "./events";
export { models }; export { models, dtos, commands, events };

View File

@ -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 (
<Box sx={{ width: "100%" }}>
<LinearProgress color="secondary" />
</Box>
);
};
export default ProgressBar;

View File

@ -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<ProgressBarContextPayload>(initialContext);
type Props = { children: React.ReactNode };
const ProgressBarProvider: React.FC<Props> = ({ children }) => {
const [network, setNetwork] = useState<string[]>([]);
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 <ProgressBarContext.Provider value={payload}>{children}</ProgressBarContext.Provider>;
};
export { ProgressBarContext };
export default ProgressBarProvider;

View File

@ -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 };

View File

@ -0,0 +1,4 @@
import ProgressBarProvider from "./ProgressBarProvider";
import ProgressBar from "./ProgressBar";
export { ProgressBarProvider, ProgressBar };
export * from "./hooks";

View File

@ -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;
}
}

View File

@ -0,0 +1,56 @@
import i18next from "i18next";
import { acquire as fetchTuitioData } from "@flare/tuitio-client";
import { NetworkError, ServerError } from "./errors";
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 = 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<Command>(url: string, { arg }: { arg: Command }) {
const hasBody = arg !== undefined && arg !== null;
const headers = getHeaders();
const body = hasBody ? JSON.stringify(arg) : undefined;
const res = await fetch(url, {
method: "POST",
headers,
body
});
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 };

View File

@ -0,0 +1,3 @@
import useGuardedMutation from "./useGuardedMutation";
export { useGuardedMutation };

View File

@ -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<Data, SWRMutationKey, ExtraArg>,
options?: SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg, SWRData> & { throwOnError?: boolean }
): SWRMutationResponse<Data, Error, SWRMutationKey, ExtraArg> {
const opts = useMemo(
() =>
({
...options,
onError: options?.onError ?? defaultErrorHandler,
throwOnError: options?.throwOnError ?? false
} as SWRMutationConfiguration<Data, Error, SWRMutationKey, ExtraArg, SWRData>),
[options]
);
return useSWRMutation<Data, Error, SWRMutationKey, ExtraArg, SWRData>(key, fetcher, opts);
}
export default useGuardedMutation;

View File

@ -0,0 +1,10 @@
import useSWR from "swr";
import type { Key } from "swr";
import useSWRMutation from "swr/mutation";
export * from "./fetchers";
export * from "./errors";
export * from "./hooks";
export { useSWR, useSWRMutation };
export type { Key };

View File

@ -1,67 +0,0 @@
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 routes = {
permissions: `${securityRoute}/permissions`,
systemVersion: `${systemRoute}/version`,
releaseNotes: `${systemRoute}/release-notes`,
resetCache: `${systemRoute}/reset-cache`,
machines: `${networkRoute}/machines`,
wakeMachine: `${powerActionsRoute}/wake`,
pingMachine: `${powerActionsRoute}/ping`,
shutdownMachine: `${powerActionsRoute}/shutdown`,
restartMachine: `${powerActionsRoute}/restart`
};
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;
};
export { routes, get, post };

30
frontend/src/utils/api.ts Normal file
View File

@ -0,0 +1,30 @@
import env from "../utils/env";
const apiHost = env.REACT_APP_NETWORK_RESURRECTOR_API_URL;
const networkRoute = `${apiHost}/network`;
const systemRoute = `${apiHost}/system`;
const resurrectorRoute = `${apiHost}/resurrector`;
const securityRoute = `${apiHost}/security`;
const endpoints = {
network: {
machines: `${networkRoute}/machines`,
machine: {
wake: `${resurrectorRoute}/wake`,
ping: `${resurrectorRoute}/ping`,
shutdown: `${resurrectorRoute}/shutdown`,
restart: `${resurrectorRoute}/restart`
}
},
system: {
version: `${systemRoute}/version`,
releaseNotes: `${systemRoute}/release-notes`,
resetCache: `${systemRoute}/reset-cache`
},
security: {
permissions: `${securityRoute}/permissions`
}
};
export { endpoints };

View File

@ -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);
}

View File

@ -6,4 +6,6 @@ const warning = message => toast.warning(message);
const error = message => toast.error(message); const error = message => toast.error(message);
const dark = message => toast.dark(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;

View File

@ -1,3 +0,0 @@
import { getRandomElement } from "./random";
export { getRandomElement };

View File

@ -0,0 +1,4 @@
import { getRandomElement } from "./random";
import blip from "./blip";
export { getRandomElement, blip };