Compare commits

...

14 Commits

Author SHA1 Message Date
Tudor Stanciu 41656b1363 permissions fix 2023-04-18 02:51:52 +03:00
Tudor Stanciu 2b6751335f NotAllowed component 2023-04-18 02:46:21 +03:00
Tudor Stanciu 4dff02b87f MachineCollapsedContent component 2023-04-18 02:22:42 +03:00
Tudor Stanciu feab019df1 disable automatic ping for guest users 2023-04-17 04:05:36 +03:00
Tudor Stanciu 63c7d946e9 removed eslint exception 2023-04-17 03:58:56 +03:00
Tudor Stanciu 27be2a2844 disabled={!canOperateMachines} 2023-04-15 00:59:40 +03:00
Tudor Stanciu 3a211bcb17 refactoring api integration 2023-04-15 00:53:11 +03:00
Tudor Stanciu 0e025e6c68 added user permissions provider 2023-04-15 00:07:15 +03:00
Tudor Stanciu b7affa7677 @flare/tuitio-client-react update 2023-04-10 19:14:22 +03:00
Tudor Stanciu b9726950cc implemented user rights 2023-04-04 18:28:01 +03:00
Tudor Stanciu 6bad079252 bold contact options 2023-04-04 00:40:52 +03:00
Tudor Stanciu 1d58d0f5e7 translations 2023-04-04 00:36:47 +03:00
Tudor Stanciu 1d81b8ec49 added security component in user profile 2023-04-04 00:32:04 +03:00
Tudor Stanciu 4c9491cb74 [1.2.4] 2023-04-02 00:15:08 +03:00
39 changed files with 688 additions and 311 deletions

16
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.2.3", "version": "1.2.4",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1316,21 +1316,21 @@
"integrity": "sha512-6NBXdZgRrHbLXw4EMgyqCIzOVAlUgr1+8QGHjlA+n5Iw2Lp/+dP3FTgAfPW/cHR/PBI3cj7gUDVUf/zD/qTPOQ==" "integrity": "sha512-6NBXdZgRrHbLXw4EMgyqCIzOVAlUgr1+8QGHjlA+n5Iw2Lp/+dP3FTgAfPW/cHR/PBI3cj7gUDVUf/zD/qTPOQ=="
}, },
"@flare/tuitio-client": { "@flare/tuitio-client": {
"version": "1.2.2", "version": "1.2.5",
"resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client/-/tuitio-client-1.2.2.tgz", "resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client/-/tuitio-client-1.2.5.tgz",
"integrity": "sha512-Rqz9seWFY5nUivMHrONphgYEeVriAq3yn8icAUQ+TFgf/ajoOqv0GlVfo2Nc//vqsLBJkZtHexmF92/5WHvvhQ==", "integrity": "sha512-jxTn5n6vbUazRnCGCVWD0D3f41aYU5idjXfgUKVFSt92L+PI59u4Zd/lWcgjLRXRyY21IZGYr47T82k25r3siQ==",
"requires": { "requires": {
"@flare/js-utils": "^1.1.0", "@flare/js-utils": "^1.1.0",
"axios": "^1.3.2" "axios": "^1.3.2"
} }
}, },
"@flare/tuitio-client-react": { "@flare/tuitio-client-react": {
"version": "1.2.2", "version": "1.2.6",
"resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client-react/-/tuitio-client-react-1.2.2.tgz", "resolved": "https://lab.code-rove.com/public-node-registry/@flare/tuitio-client-react/-/tuitio-client-react-1.2.6.tgz",
"integrity": "sha512-mhO1psmLgNXe2bIYwi+2J1WYQM63IDr8162M3s3tjm4V+9pyM0lQs7EGIXJfn344TSmXFde3SZwyxakIyWeIbQ==", "integrity": "sha512-T2uo9r9fQKTsbC25EaQnSifoDKrw3tgwy6fT/O1BdO4RPCUiRDr0YHiIssgPat+sB1G7iiUtV9CS8UNTJkacXw==",
"requires": { "requires": {
"@flare/js-utils": "^1.1.0", "@flare/js-utils": "^1.1.0",
"@flare/tuitio-client": "^1.2.2" "@flare/tuitio-client": "^1.2.5"
} }
}, },
"@gar/promisify": { "@gar/promisify": {

View File

@ -1,6 +1,6 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.2.3", "version": "1.2.4",
"description": "Frontend component of Network resurrector system", "description": "Frontend component of Network resurrector system",
"author": { "author": {
"name": "Tudor Stanciu", "name": "Tudor Stanciu",
@ -14,7 +14,7 @@
"private": true, "private": true,
"dependencies": { "dependencies": {
"@flare/js-utils": "^1.1.0", "@flare/js-utils": "^1.1.0",
"@flare/tuitio-client-react": "^1.2.2", "@flare/tuitio-client-react": "^1.2.6",
"@material-ui/core": "^4.11.2", "@material-ui/core": "^4.11.2",
"@material-ui/icons": "^4.11.2", "@material-ui/icons": "^4.11.2",
"@material-ui/lab": "^4.0.0-alpha.61", "@material-ui/lab": "^4.0.0-alpha.61",

View File

@ -4,3 +4,11 @@ https://v4.mui.com/components/material-icons/
Theming: Theming:
https://v4.mui.com/customization/palette/ (Dark theme) https://v4.mui.com/customization/palette/ (Dark theme)
Add in settings:
- reset cache
- ping interval
- notifications
- test notification mechanism
- permissions
- permissions hierarchy

View File

@ -36,12 +36,34 @@
"Label": "Login", "Label": "Login",
"IncorrectCredentials": "Incorrect credentials." "IncorrectCredentials": "Incorrect credentials."
}, },
"Announcements": {
"NotAllowed": {
"Title": "It seems that you do not have sufficient rights to view this page.",
"Message": "For more details, please contact an administrator."
}
},
"Dashboard": {
"Announcements": {
"Guest": {
"Title": "Hello there! I'm glad you're here!",
"Message": "You are currently browsing my application as a guest. Keep in mind that you cannot perform actions, and most of the data you see is fake. The purpose of the application in this state is for presentation."
},
"User": {
"Title": "Welcome back, {{userName}}",
"Message": "The application is in continuous development, so if you identify a problem, please report it. Thank you!"
}
}
},
"User": { "User": {
"Profile": { "Profile": {
"Label": "Profile", "Label": "Profile",
"Hello": "Hi, {{userName}}", "Hello": "Hi, {{userName}}",
"Description": "{{userName}}, authenticated on {{loginDate}}", "Description": "{{userName}}, authenticated on {{loginDate}}",
"OpenPortfolio": "Open portfolio" "OpenPortfolio": "Open portfolio",
"Security": {
"UserGroups": "User groups",
"UserRoles": "User roles"
}
}, },
"Settings": "Settings", "Settings": "Settings",
"Logout": "Logout" "Logout": "Logout"
@ -51,6 +73,7 @@
"Name": "Machine name", "Name": "Machine name",
"IP": "IP", "IP": "IP",
"MAC": "MAC address", "MAC": "MAC address",
"Description": "Description",
"PoweredOn": "Powered on", "PoweredOn": "Powered on",
"Actions": { "Actions": {
"Wake": "Wake", "Wake": "Wake",

View File

@ -27,12 +27,34 @@
"Label": "Autentificare", "Label": "Autentificare",
"IncorrectCredentials": "Credențiale incorecte." "IncorrectCredentials": "Credențiale incorecte."
}, },
"Announcements": {
"NotAllowed": {
"Title": "Se pare că nu aveți suficiente drepturi pentru a vizualiza această pagină.",
"Message": "Pentru mai multe detalii, vă rugăm să contactați un administrator."
}
},
"Dashboard": {
"Announcements": {
"Guest": {
"Title": "Salutare! Sunt bucuros ca ești aici!",
"Message": "În acest moment, răsfoiești aplicația mea că invitat. Reține că nu poți efectua acțiuni, iar majoritatea datelor pe care le vezi sunt false. Scopul aplicației în această stare este de prezentare."
},
"User": {
"Title": "Bine ai revenit, {{userName}}",
"Message": "Aplicația este în continuă dezvoltare, așa că dacă identifici o problemă, te rog să o raportezi. Mulțumesc!"
}
}
},
"User": { "User": {
"Profile": { "Profile": {
"Label": "Profil", "Label": "Profil",
"Hello": "Salut, {{userName}}", "Hello": "Salut, {{userName}}",
"Description": "{{userName}}, autentificat pe {{loginDate}}", "Description": "{{userName}}, autentificat pe {{loginDate}}",
"OpenPortfolio": "Deschide portofoliu" "OpenPortfolio": "Deschide portofoliu",
"Security": {
"UserGroups": "Grupuri utilizator",
"UserRoles": "Roluri utilizator"
}
}, },
"Settings": "Setări", "Settings": "Setări",
"Logout": "Deconectare" "Logout": "Deconectare"
@ -42,6 +64,7 @@
"Name": "Nume masina", "Name": "Nume masina",
"IP": "IP", "IP": "IP",
"MAC": "Adresa MAC", "MAC": "Adresa MAC",
"Description": "Descriere",
"PoweredOn": "Pornit", "PoweredOn": "Pornit",
"Actions": { "Actions": {
"Wake": "Pornește", "Wake": "Pornește",

View File

@ -1,3 +0,0 @@
import useApi from "./useApi";
export default useApi;

View File

@ -1,123 +0,0 @@
import { useToast } from "../hooks";
import { get, post } from "../utils/axios";
const networkRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/network`;
const systemRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/system`;
const powerActionsRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/resurrector`;
const useApi = () => {
const { error } = useToast();
const handleError = err => {
let message;
switch (err?.status) {
case 500:
message = err.title;
break;
case 404:
message = err.message;
break;
default:
message = err.title;
}
error(message);
};
const defaultOptions = {
onCompleted: () => {},
onError: handleError
};
const call = async (request, options) => {
const internalOptions = { ...defaultOptions, ...options };
const { onCompleted, onError } = internalOptions;
try {
const result = await request();
onCompleted(result);
} catch (error) {
onError(error);
}
};
const getSystemVersion = (options = defaultOptions) => {
const releaseNotesPromise = call(
() => get(`${systemRoute}/version`),
options
);
return releaseNotesPromise;
};
const readReleaseNotes = (options = defaultOptions) => {
const releaseNotesPromise = call(
() => get(`${systemRoute}/release-notes`),
options
);
return releaseNotesPromise;
};
const readMachines = (options = defaultOptions) => {
const machinesPromise = call(
() => get(`${networkRoute}/machines`),
options
);
return machinesPromise;
};
const wakeMachine = (machineId, options = defaultOptions) => {
const promise = call(
() => post(`${powerActionsRoute}/wake`, { machineId }),
options
);
return promise;
};
const pingMachine = (machineId, options = defaultOptions) => {
const promise = call(
() => post(`${powerActionsRoute}/ping`, { machineId }),
options
);
return promise;
};
const shutdownMachine = (
machineId,
delay,
force,
options = defaultOptions
) => {
const promise = call(
() => post(`${powerActionsRoute}/shutdown`, { machineId, delay, force }),
options
);
return promise;
};
const restartMachine = (
machineId,
delay,
force,
options = defaultOptions
) => {
const promise = call(
() => post(`${powerActionsRoute}/restart`, { machineId, delay, force }),
options
);
return promise;
};
return {
getSystemVersion,
readReleaseNotes,
readMachines,
wakeMachine,
pingMachine,
shutdownMachine,
restartMachine
};
};
export default useApi;

View File

@ -1,72 +1,14 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import { UserPermissionsProvider, SensitiveInfoProvider } from "../providers";
import AppLayout from "./layout/AppLayout"; import AppLayout from "./layout/AppLayout";
import { BrowserRouter, Switch, Redirect, Route } from "react-router-dom";
import { useTuitioToken } from "@flare/tuitio-client-react";
import LoginContainer from "../features/login/components/LoginContainer";
const PrivateRoute = ({ component, ...rest }) => {
const { valid } = useTuitioToken();
return (
<Route
{...rest}
render={props =>
valid ? (
React.createElement(component, props)
) : (
<Redirect
to={{
pathname: "/login",
state: {
from: props.location
}
}}
/>
)
}
/>
);
};
PrivateRoute.propTypes = {
component: PropTypes.func.isRequired,
location: PropTypes.object
};
const PublicRoute = ({ component, ...rest }) => {
const { valid } = useTuitioToken();
return (
<Route
{...rest}
render={props =>
valid ? (
<Redirect
to={{
pathname: "/"
}}
/>
) : (
React.createElement(component, props)
)
}
/>
);
};
PublicRoute.propTypes = {
component: PropTypes.func.isRequired
};
const App = () => { const App = () => {
return ( return (
<BrowserRouter basename={process.env.PUBLIC_URL || ""}> <UserPermissionsProvider>
<Switch> <SensitiveInfoProvider>
<Route exact path="/" render={() => <Redirect to="/dashboard" />} /> <AppLayout />
<PublicRoute path="/login" component={LoginContainer} /> </SensitiveInfoProvider>
<PrivateRoute path="/" component={AppLayout} /> </UserPermissionsProvider>
</Switch>
</BrowserRouter>
); );
}; };

View File

@ -0,0 +1,73 @@
import React from "react";
import PropTypes from "prop-types";
import App from "./App";
import { BrowserRouter, Switch, Redirect, Route } from "react-router-dom";
import { useTuitioToken } from "@flare/tuitio-client-react";
import LoginContainer from "../features/login/components/LoginContainer";
const PrivateRoute = ({ component, ...rest }) => {
const { valid } = useTuitioToken();
return (
<Route
{...rest}
render={props =>
valid ? (
React.createElement(component, props)
) : (
<Redirect
to={{
pathname: "/login",
state: {
from: props.location
}
}}
/>
)
}
/>
);
};
PrivateRoute.propTypes = {
component: PropTypes.func.isRequired,
location: PropTypes.object
};
const PublicRoute = ({ component, ...rest }) => {
const { valid } = useTuitioToken();
return (
<Route
{...rest}
render={props =>
valid ? (
<Redirect
to={{
pathname: "/"
}}
/>
) : (
React.createElement(component, props)
)
}
/>
);
};
PublicRoute.propTypes = {
component: PropTypes.func.isRequired
};
const AppRouter = () => {
return (
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
<Switch>
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
<PublicRoute path="/login" component={LoginContainer} />
<PrivateRoute path="/" component={App} />
</Switch>
</BrowserRouter>
);
};
export default AppRouter;

View File

@ -0,0 +1,29 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { Alert, AlertTitle } from "@material-ui/lab";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(theme => ({
alert: {
width: "100%",
"& > * + *": {
marginTop: theme.spacing(1)
}
}
}));
const NotAllowed = () => {
const classes = useStyles();
const { t } = useTranslation();
return (
<div className={classes.alert}>
<Alert variant="outlined" severity="error">
<AlertTitle>{t("Announcements.NotAllowed.Title")}</AlertTitle>
{t("Announcements.NotAllowed.Message")}
</Alert>
</div>
);
};
export default NotAllowed;

View File

@ -4,7 +4,7 @@ import PageNotFound from "./PageNotFound";
import NetworkContainer from "../../features/network/components/NetworkContainer"; import NetworkContainer from "../../features/network/components/NetworkContainer";
import SystemContainer from "../../features/system/SystemContainer"; import SystemContainer from "../../features/system/SystemContainer";
import SettingsContainer from "../../features/settings/SettingsContainer"; import SettingsContainer from "../../features/settings/SettingsContainer";
import DashboardContainer from "../../features/dashboard/components/DashboardContainer"; import DashboardContainer from "../../features/dashboard/DashboardContainer";
import UserProfileContainer from "../../features/user/profile/card/UserProfileContainer"; import UserProfileContainer from "../../features/user/profile/card/UserProfileContainer";
import AboutContainer from "../../features/about/AboutContainer"; import AboutContainer from "../../features/about/AboutContainer";

View File

@ -1,8 +1,8 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import useApi from "../../../api";
import ReleaseNotesList from "./ReleaseNotesList"; import ReleaseNotesList from "./ReleaseNotesList";
import TimelineComponent from "../timeline/TimelineComponent"; import TimelineComponent from "../timeline/TimelineComponent";
import { routes, get } from "../../../utils/api";
const sort = releases => const sort = releases =>
releases.sort((a, b) => new Date(b.date) - new Date(a.date)); releases.sort((a, b) => new Date(b.date) - new Date(a.date));
@ -10,16 +10,12 @@ const sort = releases =>
const ReleaseNotesContainer = ({ view }) => { const ReleaseNotesContainer = ({ view }) => {
const [state, setState] = useState({ data: [], loaded: false }); const [state, setState] = useState({ data: [], loaded: false });
const api = useApi();
useEffect(() => { useEffect(() => {
if (state.loaded) return; if (state.loaded) return;
api.readReleaseNotes({ get(routes.releaseNotes, {
onCompleted: data => { onCompleted: data => setState({ data, loaded: true })
setState({ data, loaded: true });
}
}); });
}, [api, state.loaded]); }, [state.loaded]);
return ( return (
<> <>

View File

@ -1,20 +1,16 @@
import React, { useState, useEffect } from "react"; import React, { useState, useEffect } from "react";
import SystemVersionComponent from "./SystemVersionComponent"; import SystemVersionComponent from "./SystemVersionComponent";
import useApi from "../../../api"; import { routes, get } from "../../../utils/api";
const SystemVersionContainer = () => { const SystemVersionContainer = () => {
const [state, setState] = useState({ data: {}, loaded: false }); const [state, setState] = useState({ data: {}, loaded: false });
const api = useApi();
useEffect(() => { useEffect(() => {
if (state.loaded) return; if (state.loaded) return;
api.getSystemVersion({ get(routes.systemVersion, {
onCompleted: data => { onCompleted: data => setState({ data, loaded: true })
setState({ data, loaded: true });
}
}); });
}, [api, state.loaded]); }, [state.loaded]);
return <>{state.loaded && <SystemVersionComponent data={state.data} />}</>; return <>{state.loaded && <SystemVersionComponent data={state.data} />}</>;
}; };

View File

@ -0,0 +1,10 @@
import React from "react";
import AnnouncementsSection from "./announcements/AnnouncementsSection";
const DashboardContainer = () => (
<>
<AnnouncementsSection />
</>
);
export default DashboardContainer;

View File

@ -0,0 +1,17 @@
import React from "react";
import GuestAnnouncement from "./GuestAnnouncement";
import UserAnnouncement from "./UserAnnouncement";
import { usePermissions } from "../../../hooks";
const AnnouncementsSection = () => {
const { loading, isGuest, isUser } = usePermissions();
if (loading) return "";
return (
<>
{isUser && <UserAnnouncement />}
{isGuest && <GuestAnnouncement />}
</>
);
};
export default AnnouncementsSection;

View File

@ -0,0 +1,21 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { Alert, AlertTitle } from "@material-ui/lab";
import styles from "../styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(styles);
export default function GuestAnnouncement() {
const classes = useStyles();
const { t } = useTranslation();
return (
<div className={classes.alert}>
<Alert variant="outlined" severity="warning">
<AlertTitle>{t("Dashboard.Announcements.Guest.Title")}</AlertTitle>
{t("Dashboard.Announcements.Guest.Message")}
</Alert>
</div>
);
}

View File

@ -0,0 +1,27 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import { Alert, AlertTitle } from "@material-ui/lab";
import styles from "../styles";
import { useTranslation } from "react-i18next";
import { useTuitioUser } from "@flare/tuitio-client-react";
const useStyles = makeStyles(styles);
export default function UserAnnouncement() {
const classes = useStyles();
const { t } = useTranslation();
const { userName } = useTuitioUser();
return (
<div className={classes.alert}>
<Alert variant="outlined" severity="info">
<AlertTitle>
{t("Dashboard.Announcements.User.Title", {
userName
})}
</AlertTitle>
{t("Dashboard.Announcements.User.Message")}
</Alert>
</div>
);
}

View File

@ -1,5 +0,0 @@
import React from "react";
const DashboardContainer = () => <h2>In development...</h2>;
export default DashboardContainer;

View File

@ -0,0 +1,10 @@
const styles = theme => ({
alert: {
width: "100%",
"& > * + *": {
marginTop: theme.spacing(1)
}
}
});
export default styles;

View File

@ -8,7 +8,7 @@ import {
} from "@material-ui/core"; } from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore"; import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import withStyles from "@material-ui/core/styles/withStyles"; import withStyles from "@material-ui/core/styles/withStyles";
import MachineLog from "./common/MachineLog"; import MachineCollapsedContent from "./common/MachineCollapsedContent";
import { DataLabel } from "../../../components/common"; import { DataLabel } from "../../../components/common";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useSensitiveInfo } from "../../../hooks"; import { useSensitiveInfo } from "../../../hooks";
@ -87,7 +87,11 @@ const MachineAccordion = ({ machine, actions, logs, addLog }) => {
</Grid> </Grid>
</IconLeftAccordionSummary> </IconLeftAccordionSummary>
<AccordionDetails> <AccordionDetails>
<MachineLog logs={logs} /> <MachineCollapsedContent
description={machine.description}
logs={logs}
style={{ width: "100%" }}
/>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );

View File

@ -6,7 +6,7 @@ import { ViewModes } from "./ViewModeSelection";
import { useToast } from "../../../hooks"; import { useToast } from "../../../hooks";
import { LastPage, RotateLeft, Launch, Stop } from "@material-ui/icons"; import { LastPage, RotateLeft, Launch, Stop } from "@material-ui/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useApi from "../../../api"; import { routes, post } from "../../../utils/api";
const MachineContainer = ({ machine, viewMode }) => { const MachineContainer = ({ machine, viewMode }) => {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
@ -14,8 +14,6 @@ const MachineContainer = ({ machine, viewMode }) => {
const { success, error } = useToast(); const { success, error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const api = useApi();
const addLog = useCallback( const addLog = useCallback(
text => { text => {
setLogs(prev => [...prev, text]); setLogs(prev => [...prev, text]);
@ -37,29 +35,41 @@ const MachineContainer = ({ machine, viewMode }) => {
const pingMachine = useCallback( const pingMachine = useCallback(
async machine => { async machine => {
await api.pingMachine(machine.machineId, { await post(
routes.pingMachine,
{ machineId: machine.machineId },
{
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); }
);
}, },
[manageActionResponse, api] [manageActionResponse]
); );
const shutdownMachine = useCallback( const shutdownMachine = useCallback(
async machine => { async machine => {
await api.shutdownMachine(machine.machineId, 0, false, { await post(
routes.shutdownMachine,
{ machineId: machine.machineId, delay: 0, force: false },
{
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); }
);
}, },
[manageActionResponse, api] [manageActionResponse]
); );
const restartMachine = useCallback( const restartMachine = useCallback(
async machine => { async machine => {
await api.restartMachine(machine.machineId, 0, false, { await post(
routes.restartMachine,
{ machineId: machine.machineId, delay: 0, force: false },
{
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); }
);
}, },
[manageActionResponse, api] [manageActionResponse]
); );
const actions = [ const actions = [

View File

@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { TableCell, TableRow, IconButton, Collapse } from "@material-ui/core"; import { TableCell, TableRow, IconButton, Collapse } from "@material-ui/core";
import { KeyboardArrowDown, KeyboardArrowUp } from "@material-ui/icons"; import { KeyboardArrowDown, KeyboardArrowUp } from "@material-ui/icons";
import { makeStyles } from "@material-ui/core/styles"; import { makeStyles } from "@material-ui/core/styles";
import MachineLog from "./common/MachineLog"; import MachineCollapsedContent from "./common/MachineCollapsedContent";
import { useSensitiveInfo } from "../../../hooks"; import { useSensitiveInfo } from "../../../hooks";
import ActionsGroup from "./common/ActionsGroup"; import ActionsGroup from "./common/ActionsGroup";
@ -45,7 +45,11 @@ const MachineTableRow = ({ machine, actions, logs, addLog }) => {
<TableRow> <TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}> <TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit> <Collapse in={open} timeout="auto" unmountOnExit>
<MachineLog logs={logs} /> <MachineCollapsedContent
description={machine.description}
logs={logs}
style={{ paddingBottom: "10px" }}
/>
</Collapse> </Collapse>
</TableCell> </TableCell>
</TableRow> </TableRow>

View File

@ -3,11 +3,11 @@ import {
NetworkStateContext, NetworkStateContext,
NetworkDispatchContext NetworkDispatchContext
} from "../../network/state/contexts"; } from "../../network/state/contexts";
import useApi from "../../../api";
import MachinesListComponent from "./MachinesListComponent"; import MachinesListComponent from "./MachinesListComponent";
import PageTitle from "../../../components/common/PageTitle"; import PageTitle from "../../../components/common/PageTitle";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import ViewModeSelection, { ViewModes } from "./ViewModeSelection"; import ViewModeSelection, { ViewModes } from "./ViewModeSelection";
import { routes, get } from "../../../utils/api";
const MachinesContainer = () => { const MachinesContainer = () => {
const [viewMode, setViewMode] = useState(null); const [viewMode, setViewMode] = useState(null);
@ -16,16 +16,14 @@ const MachinesContainer = () => {
const dispatchActions = useContext(NetworkDispatchContext); const dispatchActions = useContext(NetworkDispatchContext);
const { t } = useTranslation(); const { t } = useTranslation();
const api = useApi();
const handleReadMachines = useCallback(async () => { const handleReadMachines = useCallback(async () => {
await api.readMachines({ await get(routes.machines, {
onCompleted: machines => { onCompleted: machines => {
const data = Object.assign(machines, { loaded: true }); const data = Object.assign(machines, { loaded: true });
dispatchActions.onNetworkChange("machines", data); dispatchActions.onNetworkChange("machines", data);
} }
}); });
}, [dispatchActions, api]); }, [dispatchActions]);
useEffect(() => { useEffect(() => {
if (!state.network.machines.loaded) { if (!state.network.machines.loaded) {

View File

@ -3,7 +3,7 @@ import PropTypes from "prop-types";
import { IconButton, Tooltip } from "@material-ui/core"; import { IconButton, Tooltip } from "@material-ui/core";
const ActionButton = React.forwardRef((props, _ref) => { const ActionButton = React.forwardRef((props, _ref) => {
const { action, machine, callback } = props; const { action, machine, callback, disabled } = props;
const id = `machine-item-${machine.machineId}-${action.code}`; const id = `machine-item-${machine.machineId}-${action.code}`;
const handleActionClick = event => { const handleActionClick = event => {
action.effect(machine, event); action.effect(machine, event);
@ -22,6 +22,7 @@ const ActionButton = React.forwardRef((props, _ref) => {
size={"small"} size={"small"}
onFocus={event => event.stopPropagation()} onFocus={event => event.stopPropagation()}
onClick={handleActionClick} onClick={handleActionClick}
disabled={disabled}
> >
<action.icon /> <action.icon />
</IconButton> </IconButton>
@ -39,7 +40,8 @@ ActionButton.propTypes = {
tooltip: PropTypes.string.isRequired, tooltip: PropTypes.string.isRequired,
effect: PropTypes.func.isRequired effect: PropTypes.func.isRequired
}).isRequired, }).isRequired,
callback: PropTypes.func callback: PropTypes.func,
disabled: PropTypes.bool
}; };
export default ActionButton; export default ActionButton;

View File

@ -5,11 +5,13 @@ import ActionButton from "./ActionButton";
import { Menu } from "@material-ui/core"; import { Menu } from "@material-ui/core";
import { MoreHoriz } from "@material-ui/icons"; import { MoreHoriz } from "@material-ui/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { usePermissions } from "../../../../hooks";
const ActionsGroup = ({ machine, actions, addLog }) => { const ActionsGroup = ({ machine, actions, addLog }) => {
const [menuAnchor, setMenuAnchor] = useState(null); const [menuAnchor, setMenuAnchor] = useState(null);
const { t } = useTranslation(); const { t } = useTranslation();
const { operateMachines: canOperateMachines } = usePermissions();
const mainActions = useMemo( const mainActions = useMemo(
() => actions.filter(a => a.main === true), () => actions.filter(a => a.main === true),
@ -31,12 +33,17 @@ const ActionsGroup = ({ machine, actions, addLog }) => {
return ( return (
<> <>
<WakeComponent machine={machine} addLog={addLog} /> <WakeComponent
machine={machine}
addLog={addLog}
disabled={!canOperateMachines}
/>
{mainActions.map(action => ( {mainActions.map(action => (
<ActionButton <ActionButton
key={`machine-item-${machine.machineId}-${action.code}`} key={`machine-item-${machine.machineId}-${action.code}`}
action={action} action={action}
machine={machine} machine={machine}
disabled={!canOperateMachines}
/> />
))} ))}
<ActionButton <ActionButton
@ -62,6 +69,7 @@ const ActionsGroup = ({ machine, actions, addLog }) => {
action={action} action={action}
machine={machine} machine={machine}
callback={handleMenuClose} callback={handleMenuClose}
disabled={!canOperateMachines}
/> />
))} ))}
</Menu> </Menu>

View File

@ -0,0 +1,56 @@
import React from "react";
import PropTypes from "prop-types";
import MachineLog from "./MachineLog";
import Typography from "@material-ui/core/Typography";
import { useSensitiveInfo } from "../../../../hooks";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(_theme => ({
panel: {
display: "flex"
},
label: {
marginRight: "4px"
}
}));
const MachineDescription = ({ description }) => {
const classes = useStyles();
return (
<div className={classes.panel}>
<Typography
variant="body2"
className={classes.label}
color="textSecondary"
>
{"Description:"}
</Typography>
<Typography variant="body2" color="textSecondary">
{description}
</Typography>
</div>
);
};
MachineDescription.propTypes = {
description: PropTypes.string
};
const MachineCollapsedContent = ({ description, logs, style }) => {
const { mask } = useSensitiveInfo();
return (
<div style={style}>
<MachineLog logs={logs} />
{description && <MachineDescription description={mask(description)} />}
</div>
);
};
MachineCollapsedContent.propTypes = {
logs: PropTypes.array.isRequired,
description: PropTypes.string,
style: PropTypes.object
};
export default MachineCollapsedContent;

View File

@ -13,7 +13,7 @@ const MachineLog = ({ logs }) => {
); );
return ( return (
<Box width="100%"> <Box>
<div style={{ height: 200 }}> <div style={{ height: 200 }}>
<ScrollFollow <ScrollFollow
startFollowing={true} startFollowing={true}

View File

@ -5,19 +5,18 @@ import { PowerSettingsNew } from "@material-ui/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useToast } from "../../../../hooks"; import { useToast } from "../../../../hooks";
import { msToMinAndSec } from "../../../../utils/time"; import { msToMinAndSec } from "../../../../utils/time";
import useApi from "../../../../api"; import { routes, post } from "../../../../utils/api";
const initialState = { on: false }; const initialState = { on: false };
const defaultPingInterval = 1200000; //20 minutes const defaultPingInterval = 1200000; //20 minutes
const defaultStartingTime = 300000; //5 minutes const defaultStartingTime = 300000; //5 minutes
const WakeComponent = ({ machine, addLog }) => { const WakeComponent = ({ machine, addLog, disabled }) => {
const [state, setState] = useState(initialState); const [state, setState] = useState(initialState);
const [trigger, setTrigger] = useState(false); const [trigger, setTrigger] = useState(false);
const { t } = useTranslation(); const { t } = useTranslation();
const { success, error } = useToast(); const { success, error } = useToast();
const api = useApi();
const pingInterval = const pingInterval =
process.env.REACT_APP_MACHINE_PING_INTERVAL || defaultPingInterval; process.env.REACT_APP_MACHINE_PING_INTERVAL || defaultPingInterval;
@ -38,7 +37,10 @@ const WakeComponent = ({ machine, addLog }) => {
); );
const wakeMachine = useCallback(async () => { const wakeMachine = useCallback(async () => {
await api.wakeMachine(machine.machineId, { await post(
routes.wakeMachine,
{ machineId: machine.machineId },
{
onCompleted: result => { onCompleted: result => {
setState(prev => ({ ...prev, on: result.success })); setState(prev => ({ ...prev, on: result.success }));
log(`[Wake]: Success: ${result.success}. Status: ${result.status}`); log(`[Wake]: Success: ${result.success}. Status: ${result.status}`);
@ -58,11 +60,16 @@ const WakeComponent = ({ machine, addLog }) => {
error(result.status); error(result.status);
} }
} }
}); }
}, [log, success, error, startingTime, machine.machineId, api]); );
}, [log, success, error, startingTime, machine.machineId]);
const pingInLoop = useCallback(async () => { const pingInLoop = useCallback(async () => {
await api.pingMachine(machine.machineId, { if (disabled) return;
await post(
routes.pingMachine,
{ machineId: machine.machineId },
{
onCompleted: result => { onCompleted: result => {
setState(prev => ({ ...prev, on: result.success })); setState(prev => ({ ...prev, on: result.success }));
log(`[Ping]: Success: ${result.success}. Status: ${result.status}`); log(`[Ping]: Success: ${result.success}. Status: ${result.status}`);
@ -74,9 +81,9 @@ const WakeComponent = ({ machine, addLog }) => {
} }
}, },
onError: () => {} onError: () => {}
}); }
}, [machine, log, pingInterval]); // eslint-disable-line react-hooks/exhaustive-deps );
// if "api" is added in pingInLoop dependencies, the useEffect is triggered in loop }, [machine, log, pingInterval, disabled]);
useEffect(pingInLoop, [trigger, pingInLoop]); useEffect(pingInLoop, [trigger, pingInLoop]);
@ -91,9 +98,9 @@ const WakeComponent = ({ machine, addLog }) => {
<IconButton <IconButton
id={`machine-${machine.machineId}-wake`} id={`machine-${machine.machineId}-wake`}
size={"small"} size={"small"}
disabled={state.on} disabled={disabled || state.on}
onClick={handleWakeClick} onClick={handleWakeClick}
style={state.on ? { color: "#33cc33" } : {}} style={state.on ? { color: "#33cc33" } : undefined}
onFocus={event => event.stopPropagation()} onFocus={event => event.stopPropagation()}
> >
<PowerSettingsNew /> <PowerSettingsNew />
@ -105,7 +112,8 @@ const WakeComponent = ({ machine, addLog }) => {
WakeComponent.propTypes = { WakeComponent.propTypes = {
machine: PropTypes.object.isRequired, machine: PropTypes.object.isRequired,
addLog: PropTypes.func.isRequired addLog: PropTypes.func.isRequired,
disabled: PropTypes.bool
}; };
export default WakeComponent; export default WakeComponent;

View File

@ -1,8 +1,15 @@
import React from "react"; import React from "react";
import MachinesContainer from "../../machines/components/MachinesContainer"; import MachinesContainer from "../../machines/components/MachinesContainer";
import NetworkStateProvider from "../state/NetworkStateProvider"; import NetworkStateProvider from "../state/NetworkStateProvider";
import { usePermissions } from "../../../hooks";
import NotAllowed from "../../../components/common/NotAllowed";
const NetworkContainer = () => { const NetworkContainer = () => {
const { loading, viewMachines } = usePermissions();
if (loading) return "";
if (!viewMachines) return <NotAllowed />;
return ( return (
<NetworkStateProvider> <NetworkStateProvider>
<MachinesContainer /> <MachinesContainer />

View File

@ -4,9 +4,15 @@ import { useTranslation } from "react-i18next";
import { Card, CardHeader, CardContent } from "@material-ui/core"; import { Card, CardHeader, CardContent } from "@material-ui/core";
import PageTitle from "../../../../components/common/PageTitle"; import PageTitle from "../../../../components/common/PageTitle";
import UserProfileCardContent from "./UserProfileCardContent"; import UserProfileCardContent from "./UserProfileCardContent";
import SecurityComponent from "../security/SecurityComponent";
import styles from "../styles";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(styles);
const UserProfileComponent = ({ userData }) => { const UserProfileComponent = ({ userData }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const classes = useStyles();
const userLoginDate = useMemo( const userLoginDate = useMemo(
() => () =>
@ -32,6 +38,13 @@ const UserProfileComponent = ({ userData }) => {
<UserProfileCardContent userData={userData} /> <UserProfileCardContent userData={userData} />
</CardContent> </CardContent>
</Card> </Card>
<div className={classes.section}>
<SecurityComponent
userGroups={userData.userGroups}
userRoles={userData.userRoles}
/>
</div>
</> </>
); );
}; };

View File

@ -36,11 +36,11 @@ const ContactOption = ({ tooltip, label, link, onClick, ...props }) => {
primary={ primary={
<Tooltip title={tooltip}> <Tooltip title={tooltip}>
{onClick ? ( {onClick ? (
<Link href="#" onClick={onClick}> <Link href="#" onClick={onClick} style={{ fontWeight: "bold" }}>
{linkLabel} {linkLabel}
</Link> </Link>
) : ( ) : (
<Link href={link} target="_blank"> <Link href={link} target="_blank" style={{ fontWeight: "bold" }}>
{linkLabel} {linkLabel}
</Link> </Link>
)} )}

View File

@ -0,0 +1,55 @@
import React from "react";
import PropTypes from "prop-types";
import { Paper, Grid, Chip, Typography } from "@material-ui/core";
import styles from "../styles";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(styles);
const SecurityComponent = ({ userGroups, userRoles }) => {
const { t } = useTranslation();
const classes = useStyles();
return (
<Paper>
<Grid container>
<Grid item xs={12} md={6}>
<div className={classes.paper}>
<Typography gutterBottom variant="body1">
{t("User.Profile.Security.UserGroups")}
</Typography>
<div>
{userGroups.map(g => (
<Chip key={g.code} className={classes.chip} label={g.name} />
))}
</div>
</div>
</Grid>
<Grid item xs={12} md={6}>
<div className={classes.paper}>
<Typography gutterBottom variant="body1">
{t("User.Profile.Security.UserRoles")}
</Typography>
<div>
{userRoles.map(r => (
<Chip
key={r.code}
className={classes.chip}
color="primary"
label={r.name}
/>
))}
</div>
</div>
</Grid>
</Grid>
</Paper>
);
};
SecurityComponent.propTypes = {
userGroups: PropTypes.array.isRequired,
userRoles: PropTypes.array.isRequired
};
export default SecurityComponent;

View File

@ -1,5 +1,8 @@
const style = theme => { const style = theme => {
return { return {
section: {
marginTop: theme.spacing(2)
},
panel: { panel: {
display: "flex", display: "flex",
flexDirection: "row", flexDirection: "row",
@ -12,6 +15,12 @@ const style = theme => {
display: "block", display: "block",
width: theme.spacing(25), width: theme.spacing(25),
height: theme.spacing(25) height: theme.spacing(25)
},
paper: {
margin: theme.spacing(2)
},
chip: {
margin: theme.spacing(0.5)
} }
}; };
}; };

View File

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

View File

@ -1,11 +1,11 @@
import { toast } from "react-toastify"; import { toast } from "react-toastify";
export const useToast = () => {
const info = message => toast.info(message); const info = message => toast.info(message);
const success = message => toast.success(message); const success = message => toast.success(message);
const warning = message => toast.warning(message); const warning = message => toast.warning(message);
const error = message => toast.error(message); const error = message => toast.error(message);
const dark = message => toast.dark(message); const dark = message => toast.dark(message);
export const useToast = () => {
return { info, success, warning, error, dark }; return { info, success, warning, error, dark };
}; };

View File

@ -2,23 +2,20 @@ import React, { Suspense } from "react";
import ReactDOM from "react-dom"; import ReactDOM from "react-dom";
import ThemeProvider from "./providers/ThemeProvider"; import ThemeProvider from "./providers/ThemeProvider";
import CssBaseline from "@material-ui/core/CssBaseline"; import CssBaseline from "@material-ui/core/CssBaseline";
import App from "./components/App"; import AppRouter from "./components/AppRouter";
import { TuitioProvider } from "@flare/tuitio-client-react"; import { TuitioProvider } from "@flare/tuitio-client-react";
import ToastProvider from "./providers/ToastProvider"; import { ToastProvider } from "./providers";
import SensitiveInfoProvider from "./providers/SensitiveInfoProvider";
import "./utils/i18n"; import "./utils/i18n";
ReactDOM.render( ReactDOM.render(
<TuitioProvider tuitioUrl={process.env.REACT_APP_TUITIO_URL}> <TuitioProvider tuitioUrl={process.env.REACT_APP_TUITIO_URL}>
<ThemeProvider> <ThemeProvider>
<CssBaseline /> <CssBaseline />
<SensitiveInfoProvider>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<ToastProvider> <ToastProvider>
<App /> <AppRouter />
</ToastProvider> </ToastProvider>
</Suspense> </Suspense>
</SensitiveInfoProvider>
</ThemeProvider> </ThemeProvider>
</TuitioProvider>, </TuitioProvider>,
document.getElementById("root") document.getElementById("root")

View File

@ -0,0 +1,85 @@
import React, { useState, useEffect, useContext, useMemo } from "react";
import PropTypes from "prop-types";
import { routes, get } from "../utils/api";
const permissionCodes = {
VIEW_DASHBOARD: "VIEW_DASHBOARD",
MANAGE_USERS: "MANAGE_USERS",
MANAGE_SETTINGS: "MANAGE_SETTINGS",
VIEW_MACHINES: "VIEW_MACHINES",
MANAGE_MACHINES: "MANAGE_MACHINES",
OPERATE_MACHINES: "OPERATE_MACHINES",
GUEST_ACCESS: "GUEST_ACCESS"
};
const initialState = {
permissions: [],
loading: true
};
const getPermissionFlags = permissions => {
const viewDashboard =
permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false;
const manageUsers =
permissions.includes(permissionCodes.MANAGE_USERS) ?? false;
const manageSettings =
permissions.includes(permissionCodes.MANAGE_SETTINGS) ?? false;
const viewMachines =
permissions.includes(permissionCodes.VIEW_MACHINES) ?? false;
const manageMachines =
permissions.includes(permissionCodes.MANAGE_MACHINES) ?? false;
const operateMachines =
permissions.includes(permissionCodes.OPERATE_MACHINES) ?? false;
const guestAccess =
permissions.includes(permissionCodes.GUEST_ACCESS) ?? false;
const flags = {
viewDashboard,
manageUsers,
manageSettings,
viewMachines,
manageMachines,
operateMachines,
guestAccess
};
const isGuest = guestAccess === true;
const isUser = Object.values(flags).includes(true) && !flags.guestAccess;
return {
...flags,
isGuest,
isUser
};
};
const UserPermissionsContext = React.createContext(initialState);
const usePermissions = () => {
const { permissions, loading } = useContext(UserPermissionsContext);
const flags = useMemo(() => getPermissionFlags(permissions), [permissions]);
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>
);
};
UserPermissionsProvider.propTypes = {
children: PropTypes.node.isRequired
};
export { UserPermissionsProvider, usePermissions };
export default UserPermissionsProvider;

11
src/providers/index.js Normal file
View File

@ -0,0 +1,11 @@
import ThemeProvider from "./ThemeProvider";
import ToastProvider from "./ToastProvider";
import SensitiveInfoProvider from "./SensitiveInfoProvider";
import UserPermissionsProvider from "./UserPermissionsProvider";
export {
ThemeProvider,
ToastProvider,
SensitiveInfoProvider,
UserPermissionsProvider
};

65
src/utils/api.js Normal file
View File

@ -0,0 +1,65 @@
import * as axios from "../utils/axios";
import { toast } from "react-toastify";
const networkRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/network`;
const systemRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/system`;
const powerActionsRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/resurrector`;
const securityRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/security`;
const routes = {
permissions: `${securityRoute}/permissions`,
systemVersion: `${systemRoute}/version`,
releaseNotes: `${systemRoute}/release-notes`,
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: () => {},
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 };