commit
b597fcffa4
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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}>
|
||||
|
|
|
@ -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 => {
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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;
|
|
@ -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 (
|
||||
<>
|
|
@ -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;
|
|
@ -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;
|
|
@ -0,0 +1,3 @@
|
|||
import usePingTrigger from "./usePingTrigger";
|
||||
|
||||
export { usePingTrigger };
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 };
|
||||
|
|
|
@ -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 };
|
||||
};
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { info, success, warning, error, dark } from "utils/toast";
|
||||
|
||||
export const useToast = () => {
|
||||
return { info, success, warning, error, dark };
|
||||
};
|
|
@ -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";
|
||||
|
|
|
@ -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 };
|
|
@ -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;
|
||||
};
|
|
@ -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[];
|
||||
};
|
|
@ -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;
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
|
@ -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 };
|
|
@ -0,0 +1,4 @@
|
|||
import ProgressBarProvider from "./ProgressBarProvider";
|
||||
import ProgressBar from "./ProgressBar";
|
||||
export { ProgressBarProvider, ProgressBar };
|
||||
export * from "./hooks";
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 };
|
|
@ -0,0 +1,3 @@
|
|||
import useGuardedMutation from "./useGuardedMutation";
|
||||
|
||||
export { useGuardedMutation };
|
|
@ -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;
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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 };
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -1,3 +0,0 @@
|
|||
import { getRandomElement } from "./random";
|
||||
|
||||
export { getRandomElement };
|
|
@ -0,0 +1,4 @@
|
|||
import { getRandomElement } from "./random";
|
||||
import blip from "./blip";
|
||||
|
||||
export { getRandomElement, blip };
|
Loading…
Reference in New Issue