Merged PR 84: SWR integration

https://swr.vercel.app/
master
Tudor Stanciu 2024-11-16 12:14:09 +00:00
commit b597fcffa4
51 changed files with 768 additions and 516 deletions

View File

@ -1,7 +1,7 @@
<Project>
<Import Project="dependencies.props" />
<PropertyGroup>
<Version>1.3.0</Version>
<Version>1.3.1</Version>
<Authors>Tudor Stanciu</Authors>
<Company>STA</Company>
<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.
• 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.
</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>
</Note>
</ReleaseNotes>

View File

@ -44,8 +44,9 @@ namespace NetworkResurrector.Api.Controllers
[HttpPost("reset-cache")]
[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);
return Ok(result);
}

View File

@ -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",
@ -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",

View File

@ -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",
@ -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",

View File

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

View File

@ -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<TopBarProps> = ({ open, onDrawerOpen }) => {
<ProfileButton />
</Box>
</Toolbar>
{/* <ProgressBar /> */}
<ProgressBar />
</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 bullet = <span style={styles.bullet}></span>;
@ -80,7 +80,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => {
size="small"
color="primary"
startIcon={<OpenInNewIcon />}
onClick={handleOpenInNewTab(button.url)}
onClick={onOpenInNewTab(button.url)}
>
{t(button.code)}
</Button>
@ -91,7 +91,7 @@ const AboutSystemComponent = ({ handleOpenInNewTab }) => {
};
AboutSystemComponent.propTypes = {
handleOpenInNewTab: PropTypes.func.isRequired
onOpenInNewTab: PropTypes.func.isRequired
};
export default AboutSystemComponent;

View File

@ -13,15 +13,15 @@ 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 (
<Box sx={styles.page}>
<AboutSystemComponent handleOpenInNewTab={handleOpenInNewTab} />
<AboutSystemComponent onOpenInNewTab={handleOpenInNewTab} />
<Box sx={styles.element}>
<SystemVersionContainer />
</Box>

View File

@ -1,6 +1,5 @@
import React, { useMemo, useEffect, useState } from "react";
import PropTypes from "prop-types";
import { List, ListItem, ListItemText, ListItemAvatar } from "@mui/material";
import React, { useMemo } from "react";
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";
@ -8,47 +7,23 @@ 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 getStyles = 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 SystemVersionComponent = ({ data }) => {
const VersionLabel = styled("span")(({ theme }) => ({
fontSize: "0.9rem",
fontWeight: theme.typography.fontWeightMedium
}));
type Props = {
data: dtos.SystemVersion;
};
const SystemVersionComponent: React.FC<Props> = ({ data }) => {
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 format = "DD-MM-YYYY HH:mm:ss";
@ -78,20 +53,27 @@ const SystemVersionComponent = ({ data }) => {
return (
<Paper variant="outlined">
<List sx={listClass}>
<List
sx={{
display: { xs: "block", md: "flex" },
flexDirection: { md: "row" },
padding: 0,
width: "100%"
}}
>
<ListItem>
<ListItemAvatar>
<Avatar sx={styles.versionAvatar}>
<VersionAvatar>
<DeveloperBoardIcon />
</Avatar>
</VersionAvatar>
</ListItemAvatar>
<ListItemText
primary={
<span style={styles.value}>
<VersionLabel>
{t("About.System.Version.Server", {
version: data.server.version
})}
</span>
</VersionLabel>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.server
@ -100,17 +82,17 @@ const SystemVersionComponent = ({ data }) => {
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar sx={styles.versionAvatar}>
<VersionAvatar>
<SettingsInputSvideoIcon />
</Avatar>
</VersionAvatar>
</ListItemAvatar>
<ListItemText
primary={
<span style={styles.value}>
<VersionLabel>
{t("About.System.Version.Api", {
version: data.api.version
})}
</span>
</VersionLabel>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.api
@ -119,17 +101,17 @@ const SystemVersionComponent = ({ data }) => {
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar sx={styles.versionAvatar}>
<VersionAvatar>
<WebAssetIcon />
</Avatar>
</VersionAvatar>
</ListItemAvatar>
<ListItemText
primary={
<span style={styles.value}>
<VersionLabel>
{t("About.System.Version.Frontend", {
version: process.env.APP_VERSION ?? packageData.version
})}
</span>
</VersionLabel>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.frontend
@ -141,8 +123,4 @@ const SystemVersionComponent = ({ data }) => {
);
};
SystemVersionComponent.propTypes = {
data: PropTypes.object.isRequired
};
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" }
})}
</Typography>
<Typography variant="body2" color="textSecondary">
{t("About.ReleaseNotes.Version")}: {release.version}
</Typography>
</TimelineOppositeContent>
<TimelineSeparator>
<TimelineDot color={release.dot.color} variant={release.dot.variant}>

View File

@ -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 => {

View File

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

View File

@ -1,75 +1,82 @@
import React, { useState, useCallback } from "react";
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";
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 }) => {
const [logs, setLogs] = useState([]);
const { success, error } = useToast();
type Props = {
machine: Machine;
viewMode: string;
};
const MachineContainer: React.FC<Props> = ({ machine, viewMode }) => {
const [logs, setLogs] = useState<string[]>([]);
const { t } = useTranslation();
const addLog = useCallback(
text => {
(text: string) => {
setLogs(prev => [...prev, text]);
},
[setLogs]
);
const manageActionResponse = useCallback(
response => {
(response: MachineActionResult) => {
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 { 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(
async machine => {
await post(
routes.pingMachine,
{ machineId: machine.machineId },
{
onCompleted: manageActionResponse
}
);
},
[manageActionResponse]
async (machine: Machine) => pingMachineTrigger({ machineId: machine.machineId }),
[pingMachineTrigger]
);
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 = [
@ -117,9 +124,4 @@ const MachineContainer = ({ machine, viewMode }) => {
);
};
MachineContainer.propTypes = {
machine: PropTypes.object.isRequired,
viewMode: PropTypes.string.isRequired
};
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 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<Machine[], Error>(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 (
<>

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 { usePermissions } from "../providers/UserPermissionsProvider";
import { useClipboard } from "./useClipboard";
export { useToast, useSensitiveInfo, usePermissions, useClipboard };
export { useSensitiveInfo, usePermissions, useClipboard };

View File

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

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 { 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";

View File

@ -1,6 +1,8 @@
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, fetcher } from "units/swr";
import { blip } from "utils";
const permissionCodes = {
VIEW_DASHBOARD: "VIEW_DASHBOARD",
@ -13,12 +15,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 +56,7 @@ const getPermissionFlags = permissions => {
};
};
const UserPermissionsContext = React.createContext(initialState);
const UserPermissionsContext = React.createContext<UserPermissionsContextPayload>(initialState);
const usePermissions = () => {
const { permissions, loading } = useContext(UserPermissionsContext);
@ -57,20 +64,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 <UserPermissionsContext.Provider value={permissions}>{children}</UserPermissionsContext.Provider>;
type Props = {
children: ReactNode;
};
UserPermissionsProvider.propTypes = {
children: PropTypes.node.isRequired
const UserPermissionsProvider: React.FC<Props> = ({ children }) => {
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 };

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 dtos from "./dtos";
import * as commands from "./commands";
import * as events from "./events";
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 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 };