Compare commits

...

27 Commits

Author SHA1 Message Date
52cd8f53f5 [1.2.3] 2023-03-27 02:15:22 +03:00
Tudor Stanciu
f08ba0221b system page 2023-03-27 02:06:32 +03:00
Tudor Stanciu
58e5f61a97 Settings: AppearanceComponent 2023-03-27 01:44:45 +03:00
Tudor Stanciu
46581b33c9 settings page changes 2023-03-27 01:08:55 +03:00
Tudor Stanciu
d8bad156dd language component refactoring 2023-03-27 00:27:47 +03:00
Tudor Stanciu
eec6d9ceb2 secondary color update 2023-03-26 22:53:31 +03:00
Tudor Stanciu
e6d115a21e button colors 2023-03-26 22:44:31 +03:00
Tudor Stanciu
99b4929b2a Timeline component 2023-03-26 22:36:26 +03:00
Tudor Stanciu
853cc9c9ad moved AboutContainer 2023-03-26 05:02:50 +03:00
Tudor Stanciu
5ffed1c995 system description update 2023-03-26 04:56:12 +03:00
Tudor Stanciu
d3f359c6dd system description 2023-03-26 04:52:36 +03:00
Tudor Stanciu
c61df6b59b system version component update 2023-03-26 02:51:46 +02:00
Tudor Stanciu
484b3d0ab2 SystemVersionContainer 2023-03-26 02:43:28 +02:00
Tudor Stanciu
6d1c2df40b release notes page 2023-03-25 12:57:51 +02:00
Tudor Stanciu
78f7b2db2a about page update 2023-03-25 01:16:11 +02:00
Tudor Stanciu
a166e1c9e3 visual components update 2023-03-25 00:28:48 +02:00
Tudor Stanciu
1ad78b3ef1 ViewModeSelection double selection fix 2023-03-24 01:33:37 +02:00
Tudor Stanciu
4195456227 removed secondaryActionsMenuProps 2023-03-24 01:30:49 +02:00
Tudor Stanciu
64684674ba machine view modes update 2023-03-24 00:43:28 +02:00
Tudor Stanciu
6ad7abf3d1 machines accordion view update 2023-03-22 18:20:14 +02:00
Tudor Stanciu
7115649f12 machines view modes update 2023-03-21 19:32:35 +02:00
Tudor Stanciu
94f2138bc7 changed components structure 2023-03-21 18:31:58 +02:00
Tudor Stanciu
3cc5d8f6f3 machines view mode option 2023-03-21 18:14:32 +02:00
Tudor Stanciu
234778cc43 added about menu item 2023-03-20 19:04:29 +02:00
Tudor Stanciu
6af31bdcca removed old stepper component 2023-03-20 18:57:41 +02:00
Tudor Stanciu
a49a289b2c Merge branch 'master' of https://dev.azure.com/tstanciu94/NetworkResurrector/_git/NetworkResurrector_Frontend 2023-03-20 18:49:52 +02:00
Tudor Stanciu
b617d59b69 User profile page 2023-03-20 18:49:35 +02:00
77 changed files with 2085 additions and 851 deletions

8
.env
View File

@ -1,8 +1,8 @@
#REACT_APP_TUITIO_URL=http://localhost:5063/identity/authenticate?UserName={username}&Password={password} #REACT_APP_TUITIO_URL=http://localhost:5063
REACT_APP_TUITIO_URL=https://lab.code-rove.com/tuitio REACT_APP_TUITIO_URL=https://lab.code-rove.com/tuitio
#REACT_APP_NETWORK_RESURRECTOR_API_URL=http://localhost:5064
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api REACT_APP_NETWORK_RESURRECTOR_API_URL=http://localhost:5064
#REACT_APP_NETWORK_RESURRECTOR_API_URL=https://lab.code-rove.com/network-resurrector-api
#600000 milliseconds = 10 minutes #600000 milliseconds = 10 minutes
REACT_APP_MACHINE_PING_INTERVAL=600000 REACT_APP_MACHINE_PING_INTERVAL=600000

View File

@ -27,6 +27,9 @@ ENV AUTHOR="Tudor Stanciu"
ARG APP_VERSION=0.0.0 ARG APP_VERSION=0.0.0
ENV APP_VERSION=${APP_VERSION} ENV APP_VERSION=${APP_VERSION}
ARG APP_DATE="-"
ENV APP_DATE=${APP_DATE}
#set workdir to root #set workdir to root
WORKDIR / WORKDIR /

14
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.2.2", "version": "1.2.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -1980,6 +1980,18 @@
"@babel/runtime": "^7.4.4" "@babel/runtime": "^7.4.4"
} }
}, },
"@material-ui/lab": {
"version": "4.0.0-alpha.61",
"resolved": "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.61.tgz",
"integrity": "sha512-rSzm+XKiNUjKegj8bzt5+pygZeckNLOr+IjykH8sYdVk7dE9y2ZuUSofiMV2bJk3qU+JHwexmw+q0RyNZB9ugg==",
"requires": {
"@babel/runtime": "^7.4.4",
"@material-ui/utils": "^4.11.3",
"clsx": "^1.0.4",
"prop-types": "^15.7.2",
"react-is": "^16.8.0 || ^17.0.0"
}
},
"@material-ui/styles": { "@material-ui/styles": {
"version": "4.11.5", "version": "4.11.5",
"resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz", "resolved": "https://registry.npmjs.org/@material-ui/styles/-/styles-4.11.5.tgz",

View File

@ -1,6 +1,6 @@
{ {
"name": "network-resurrector-frontend", "name": "network-resurrector-frontend",
"version": "1.2.2", "version": "1.2.3",
"description": "Frontend component of Network resurrector system", "description": "Frontend component of Network resurrector system",
"author": { "author": {
"name": "Tudor Stanciu", "name": "Tudor Stanciu",
@ -17,6 +17,7 @@
"@flare/tuitio-client-react": "^1.1.1", "@flare/tuitio-client-react": "^1.1.1",
"@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",
"axios": "^1.3.4", "axios": "^1.3.4",
"i18next": "^19.4.4", "i18next": "^19.4.4",
"i18next-browser-languagedetector": "^4.1.1", "i18next-browser-languagedetector": "^4.1.1",

View File

@ -12,22 +12,39 @@
"English": "English", "English": "English",
"Romanian": "Romanian" "Romanian": "Romanian"
}, },
"Generic": {
"Copy": "Copy",
"OpenInNewTab": "Open in new tab",
"CopiedToClipboard": "Copied to clipboard",
"SendEmail": "Send email"
},
"Menu": { "Menu": {
"Dashboard": "Dashboard", "Dashboard": "Dashboard",
"Machines": "Machines", "Machines": "Machines",
"Trash": "Trash", "System": "System",
"Settings": "Settings" "Administration": "Administration",
"Settings": "Settings",
"About": "About"
},
"ViewModes": {
"Table": "Table",
"List": "List"
}, },
"Login": { "Login": {
"Username": "Username", "Username": "Username",
"Password": "Password", "Password": "Password",
"Label": "Login", "Label": "Login",
"ChangeUser": "Change user", "IncorrectCredentials": "Incorrect credentials."
"UserChanged": "User changed", },
"Logout": "Logout", "User": {
"IncorrectCredentials": "Incorrect credentials.", "Profile": {
"Hello": "Hi, {{username}}", "Label": "Profile",
"AuthenticationDate": "Authentication date" "Hello": "Hi, {{userName}}",
"Description": "{{userName}}, authenticated on {{loginDate}}",
"OpenPortfolio": "Open portfolio"
},
"Settings": "Settings",
"Logout": "Logout"
}, },
"Machine": { "Machine": {
"FullName": "Full machine name", "FullName": "Full machine name",
@ -43,5 +60,52 @@
"Restart": "Restart", "Restart": "Restart",
"Advanced": "Advanced" "Advanced": "Advanced"
} }
},
"System": {
"Navigation": {
"MainServices": "Main services",
"Agents": "Agents"
}
},
"Settings": {
"Navigation": {
"Appearance": "Appearance",
"Notifications": "Notifications"
}
},
"About": {
"Navigation": {
"System": "System",
"ReleaseNotes": "Release notes",
"Timeline": "Timeline"
},
"ReleaseNotes": {
"Version": "Version",
"Date": "Date"
},
"System": {
"Description": {
"Title": "Network resurrector system",
"FirstPhrase": "Everything must be able to be managed remotely. Even the powered off servers. That's how Network resurrector appeared, the tool I wrote specifically to be able to wake up my machines that I don't need to be powered on all the time.",
"SecondPhrase": "Network Resurrector is a system that comprises of five essential services which allow for the execution of its core functionality. To enable various additional features, such as the notification mechanism, supplementary components may be added to the system as an option.",
"Frontend": "Frontend: The frontend component is a web application written in React JS that has the role of providing the user with a friendly visual interface through which to interact with the system.",
"Api": "API: The API component is a .NET 6 REST API that has the role of mediating the exchange of data and commands between the frontend component and the database or server component.",
"Server": "Server: The server component is a .NET 6 service specialized in executing 'WakeOnLAN', 'Ping' and 'Shutdown' actions for the machines in its network.",
"Agent": "Agent: The agent is a .NET 6 service specialized in executing 'Shutdown', 'Restart', 'Sleep', 'Logout' and 'Lock' actions on the host machine on which it is installed. Each action can be executed at the time of launch or with a certain delay. If an action is requested with a delay and later the user changes his mind, he can cancel the action by executing the separate 'Cancel' type action.",
"Tuitio": "Tuitio: Tuitio is my personal identity server. It manages user authentication within the application and authorizes requests made by it. Further information about Tuitio can be found on its dedicated page."
},
"Services": {
"Frontend": "Frontend",
"Api": "API",
"Server": "Server",
"Tuitio": "Tuitio"
},
"Version": {
"Server": "Server: {{version}}",
"Api": "API: {{version}}",
"Frontend": "UI: {{version}}",
"LastReleaseDate": "Last update date: {{date}}"
}
}
} }
} }

View File

@ -3,22 +3,39 @@
"English": "Engleză", "English": "Engleză",
"Romanian": "Română" "Romanian": "Română"
}, },
"Generic": {
"Copy": "Copiază",
"OpenInNewTab": "Deschide într-un tab nou",
"CopiedToClipboard": "Copiat în clipboard",
"SendEmail": "Trimite email"
},
"Menu": { "Menu": {
"Dashboard": "Bord", "Dashboard": "Bord",
"Machines": "Mașini", "Machines": "Mașini",
"Trash": "Gunoi", "System": "Sistem",
"Settings": "Setări" "Administration": "Administrare",
"Settings": "Setări",
"About": "Despre"
},
"ViewModes": {
"Table": "Tabel",
"List": "Lista"
}, },
"Login": { "Login": {
"Username": "Utilizator", "Username": "Utilizator",
"Password": "Parolă", "Password": "Parolă",
"Label": "Autentificare", "Label": "Autentificare",
"ChangeUser": "Schimbă utilizatorul", "IncorrectCredentials": "Credențiale incorecte."
"UserChanged": "Utilizator schimbat", },
"Logout": "Deconectare", "User": {
"IncorrectCredentials": "Credențiale incorecte.", "Profile": {
"Hello": "Salut, {{username}}", "Label": "Profil",
"AuthenticationDate": "Momentul autentificării" "Hello": "Salut, {{userName}}",
"Description": "{{userName}}, autentificat pe {{loginDate}}",
"OpenPortfolio": "Deschide portofoliu"
},
"Settings": "Setări",
"Logout": "Deconectare"
}, },
"Machine": { "Machine": {
"FullName": "Nume intreg masina", "FullName": "Nume intreg masina",
@ -34,5 +51,52 @@
"Restart": "Repornește", "Restart": "Repornește",
"Advanced": "Avansat" "Advanced": "Avansat"
} }
},
"System": {
"Navigation": {
"MainServices": "Servicii principale",
"Agents": "Agenți"
}
},
"Settings": {
"Navigation": {
"Appearance": "Aspect",
"Notifications": "Notificări"
}
},
"About": {
"Navigation": {
"System": "Sistem",
"ReleaseNotes": "Note de lansare",
"Timeline": "Cronologie"
},
"ReleaseNotes": {
"Version": "Versiune",
"Date": "Dată"
},
"System": {
"Description": {
"Title": "Network resurrector system",
"FirstPhrase": "Totul trebuie să poată fi gestionat de la distanță. Chiar și serverele oprite. Așa a apărut Network resurrector, instrumentul pe care l-am scris special pentru a-mi putea porni mașinile de care nu am nevoie să fie pornite tot timpul.",
"SecondPhrase": "Network Resurrector este un sistem care cuprinde cinci servicii esențiale care permit executarea funcționalității sale de bază. Pentru a activa diverse funcții suplimentare, cum ar fi mecanismul de notificare, componente suplimentare pot fi adăugate la sistem ca opțiune.",
"Frontend": "Frontend: Componenta frontend este o aplicație web scrisă în React JS care are rolul de a oferi utilizatorului o interfață vizuală prietenoasă prin care să interacționeze cu sistemul.",
"Api": "API: Componenta API este un .NET 6 REST API care are rolul de a media schimbul de date și comenzi între componenta frontend și baza de date sau componenta server.",
"Server": "Server: Componenta server este un serviciu .NET 6 specializat în executarea acțiunilor 'WakeOnLAN', 'Ping' și 'Shutdown' pentru mașinile din rețeaua sa.",
"Agent": "Agent: Agentul este un serviciu .NET 6 specializat în executarea acțiunilor 'Shutdown', 'Restart', 'Sleep', 'Logout' și 'Lock' pe mașina gazdă pe care este instalat. Fiecare acțiune poate fi executată în momentul lansării sau cu o anumită întârziere. Dacă o acțiune este solicitată cu întârziere și ulterior utilizatorul se răzgândește, el poate anula acțiunea executând comanda separată de tip 'Cancel'.",
"Tuitio": "Tuitio: Tuitio este serverul meu de identitate personală. Gestionează autentificarea utilizatorilor în cadrul aplicației și autorizează solicitările făcute de aceasta. Mai multe informații despre Tuitio pot fi găsite pe pagina sa dedicată."
},
"Services": {
"Frontend": "Frontend",
"Api": "API",
"Server": "Server",
"Tuitio": "Tuitio"
},
"Version": {
"Server": "Server: {{version}}",
"Api": "API: {{version}}",
"Frontend": "UI: {{version}}",
"LastReleaseDate": "Data ultimei actualizări: {{date}}"
}
}
} }
} }

View File

@ -2,6 +2,7 @@ import { useToast } from "../hooks";
import { get, post } from "../utils/axios"; import { get, post } from "../utils/axios";
const networkRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/network`; 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 powerActionsRoute = `${process.env.REACT_APP_NETWORK_RESURRECTOR_API_URL}/resurrector`;
const useApi = () => { const useApi = () => {
@ -42,6 +43,22 @@ const useApi = () => {
} }
}; };
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 readMachines = (options = defaultOptions) => {
const machinesPromise = call( const machinesPromise = call(
() => get(`${networkRoute}/machines`), () => get(`${networkRoute}/machines`),
@ -93,6 +110,8 @@ const useApi = () => {
}; };
return { return {
getSystemVersion,
readReleaseNotes,
readMachines, readMachines,
wakeMachine, wakeMachine,
pingMachine, pingMachine,

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@ -0,0 +1,42 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
panel: {
display: "flex"
},
label: {
marginRight: "4px"
},
data: {
fontWeight: theme.typography.fontWeightMedium
}
}));
const DataLabel = ({ label, data }) => {
const classes = useStyles();
const lbl = useMemo(
() => (label.endsWith(":") ? label : `${label}:`),
[label]
);
return (
<div className={classes.panel}>
<Typography variant="body2" className={classes.label}>
{lbl}
</Typography>
<Typography variant="body2" className={classes.data}>
{data}
</Typography>
</div>
);
};
DataLabel.propTypes = {
label: PropTypes.string.isRequired,
data: PropTypes.string
};
export default DataLabel;

View File

@ -0,0 +1,45 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import ToggleButton from "@material-ui/lab/ToggleButton";
import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup";
import { Tooltip } from "@material-ui/core";
const NavigationButtons = ({ tabs, onTabChange }) => {
const [selected, setSelected] = useState(tabs[0].code);
const handleTabSelection = (_event, tabCode) => {
setSelected(tabCode);
onTabChange && onTabChange(tabCode);
};
return (
<ToggleButtonGroup
size="small"
value={selected}
exclusive
onChange={handleTabSelection}
>
{tabs.map(tab => (
<ToggleButton
key={tab.code}
value={tab.code}
aria-label="navigation buttons"
disabled={selected === tab.code}
>
<Tooltip title={tab.tooltip}>
<tab.icon color="primary" />
</Tooltip>
</ToggleButton>
))}
</ToggleButtonGroup>
);
};
NavigationButtons.propTypes = {
tabs: PropTypes.arrayOf(
PropTypes.shape({ code: PropTypes.string.isRequired })
).isRequired,
onTabChange: PropTypes.func
};
export default NavigationButtons;

View File

@ -7,26 +7,38 @@ const useStyles = makeStyles(theme => ({
box: { box: {
display: "flex", display: "flex",
justifyContent: "space-between", justifyContent: "space-between",
marginBottom: theme.spacing(2), marginBottom: theme.spacing(1),
marginTop: theme.spacing(0) marginTop: theme.spacing(0)
}, },
title: { textTransform: "uppercase" } title: {
display: "flex",
justifyContent: "center",
flexDirection: "column",
minHeight: "40px"
},
titleText: { textTransform: "uppercase" }
})); }));
const PageTitle = ({ text }) => { const PageTitle = ({ text, toolBar, navigation }) => {
const classes = useStyles(); const classes = useStyles();
return ( return (
<div className={classes.box}> <div className={classes.box}>
<Typography className={classes.title} variant="h3" size="sm"> {navigation && navigation}
<div className={classes.title}>
<Typography className={classes.titleText} variant="h3" size="sm">
{text} {text}
</Typography> </Typography>
</div> </div>
{toolBar && toolBar}
</div>
); );
}; };
PageTitle.propTypes = { PageTitle.propTypes = {
text: PropTypes.string.isRequired text: PropTypes.string.isRequired,
toolBar: PropTypes.node,
navigation: PropTypes.node
}; };
export default PageTitle; export default PageTitle;

View File

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

View File

@ -1,75 +0,0 @@
import React, { useState, useEffect } from "react";
import steps from "./steps";
import {
Card,
Stepper,
Step,
StepButton,
StepLabel,
makeStyles
} from "@material-ui/core";
import { useLocation, useHistory } from "react-router-dom";
import CustomStepConnector from "./CustomStepConnector";
import StepIcon from "./StepIcon";
import { useTranslation } from "react-i18next";
const styles = () => ({
stepperCard: {
margin: "15px"
},
stepper: {
padding: "10px"
}
});
const useStyles = makeStyles(styles);
const ApplicationStepper = () => {
const firstStep = steps[0];
const [activeStep, setActiveStep] = useState(firstStep);
const classes = useStyles();
const location = useLocation();
const history = useHistory();
const { t } = useTranslation();
useEffect(() => {
const step = steps.find(z => z.route === location.pathname);
if (step) {
setActiveStep(step);
}
}, [location.pathname]);
const handleStepClick = step => {
if (!step) return;
setActiveStep(step);
if (location.pathname === step.route) return;
history.push(step.route);
};
return (
<Card className={classes.stepperCard}>
<Stepper
className={classes.stepper}
activeStep={activeStep.id}
orientation="horizontal"
connector={<CustomStepConnector />}
>
{steps.map(step => (
<Step key={step.id}>
<StepButton
id={"appStepper_" + step.title}
disabled={step.disabled}
onClick={() => handleStepClick(step)}
>
<StepLabel StepIconComponent={StepIcon}>
{t(step.title)}
</StepLabel>
</StepButton>
</Step>
))}
</Stepper>
</Card>
);
};
export default ApplicationStepper;

View File

@ -1,28 +0,0 @@
import { withStyles } from "@material-ui/core/styles";
import StepConnector from "@material-ui/core/StepConnector";
const CustomStepConnector = withStyles({
alternativeLabel: {
top: 22
},
active: {
"& $line": {
backgroundImage:
"linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)"
}
},
completed: {
"& $line": {
backgroundImage:
"linear-gradient( 95deg,rgb(242,113,33) 0%,rgb(233,64,87) 50%,rgb(138,35,135) 100%)"
}
},
line: {
height: 3,
border: 0,
backgroundColor: "#eaeaf0",
borderRadius: 1
}
})(StepConnector);
export default CustomStepConnector;

View File

@ -1,66 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import clsx from "clsx";
import { makeStyles } from "@material-ui/core/styles";
import steps from "./steps";
const useStepIconStyles = makeStyles({
root: {
backgroundColor: "#ccc",
zIndex: 1,
color: "#fff",
width: 50,
height: 50,
display: "flex",
borderRadius: "50%",
justifyContent: "center",
alignItems: "center"
},
active: {
backgroundImage:
"linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)",
boxShadow: "0 4px 10px 0 rgba(0,0,0,.25)"
},
completed: {
backgroundImage:
"linear-gradient( 136deg, rgb(242,113,33) 0%, rgb(233,64,87) 50%, rgb(138,35,135) 100%)"
}
});
function StepIcon(props) {
const classes = useStepIconStyles();
const { active, completed } = props;
const getIcon = icon => {
const step = steps.find(z => z.id === icon - 1);
return step.icon;
};
return (
<div
className={clsx(classes.root, {
[classes.active]: active,
[classes.completed]: completed
})}
>
{getIcon(props.icon)}
</div>
);
}
StepIcon.propTypes = {
/**
* Whether this step is active.
*/
active: PropTypes.bool,
/**
* Mark the step as completed. Is passed to child components.
*/
completed: PropTypes.bool,
/**
* The label displayed in the step icon.
*/
icon: PropTypes.node
};
export default StepIcon;

View File

@ -1,27 +0,0 @@
import { Router, VpnKey, Settings } from "@material-ui/icons";
const steps = [
{
id: 0,
title: "Steps.Login",
route: "/",
disabled: false,
icon: <VpnKey />
},
{
id: 1,
title: "Steps.Network",
route: "/machines",
disabled: false,
icon: <Router />
},
{
id: 2,
title: "Steps.Settings",
route: "/settings",
disabled: false,
icon: <Settings />
}
];
export default steps;

View File

@ -1,18 +1,22 @@
import React from "react"; import React from "react";
import { Route, Switch } from "react-router-dom"; import { Route, Switch } from "react-router-dom";
import PageNotFound from "./PageNotFound"; import PageNotFound from "./PageNotFound";
import LoginContainer from "../../features/login/components/LoginContainer";
import NetworkContainer from "../../features/network/components/NetworkContainer"; import NetworkContainer from "../../features/network/components/NetworkContainer";
import SettingsContainer from "../../features/settings/components/SettingsContainer"; import SystemContainer from "../../features/system/SystemContainer";
import SettingsContainer from "../../features/settings/SettingsContainer";
import DashboardContainer from "../../features/dashboard/components/DashboardContainer"; import DashboardContainer from "../../features/dashboard/components/DashboardContainer";
import UserProfileContainer from "../../features/user/profile/components/UserProfileContainer";
import AboutContainer from "../../features/about/AboutContainer";
const AppRoutes = () => { const AppRoutes = () => {
return ( return (
<Switch> <Switch>
<Route exact path="/dashboard" component={DashboardContainer} /> <Route exact path="/dashboard" component={DashboardContainer} />
<Route exact path="/user-profile" component={LoginContainer} /> <Route exact path="/user-profile" component={UserProfileContainer} />
<Route exact path="/machines" component={NetworkContainer} /> <Route exact path="/machines" component={NetworkContainer} />
<Route exact path="/system" component={SystemContainer} />
<Route exact path="/settings" component={SettingsContainer} /> <Route exact path="/settings" component={SettingsContainer} />
<Route exact path="/about" component={AboutContainer} />
<Route component={PageNotFound} /> <Route component={PageNotFound} />
</Switch> </Switch>
); );

View File

@ -1,13 +1,29 @@
import React, { useState } from "react"; import React, { useState } from "react";
import { IconButton, Menu, MenuItem } from "@material-ui/core"; import {
IconButton,
Menu,
MenuItem,
Typography,
ListItemIcon
} from "@material-ui/core";
import AccountCircle from "@material-ui/icons/AccountCircle"; import AccountCircle from "@material-ui/icons/AccountCircle";
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
import AccountBoxIcon from "@material-ui/icons/AccountBox";
import SettingsIcon from "@material-ui/icons/Settings";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useTuitioClient } from "@flare/tuitio-client-react"; import { useTuitioClient } from "@flare/tuitio-client-react";
import { useToast } from "../../hooks"; import { useToast } from "../../hooks";
import styles from "./styles";
import { makeStyles } from "@material-ui/core/styles";
import { useTranslation } from "react-i18next";
const useStyles = makeStyles(styles);
const ProfileButton = () => { const ProfileButton = () => {
const history = useHistory(); const history = useHistory();
const { error } = useToast(); const { error } = useToast();
const classes = useStyles();
const { t } = useTranslation();
const { logout } = useTuitioClient({ const { logout } = useTuitioClient({
onLogoutFailed: errorMessage => error(errorMessage), onLogoutFailed: errorMessage => error(errorMessage),
@ -57,9 +73,28 @@ const ProfileButton = () => {
handleClose(); handleClose();
}} }}
> >
Profile <ListItemIcon className={classes.menuItemIcon}>
<AccountBoxIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">{t("User.Profile.Label")}</Typography>
</MenuItem>
<MenuItem
onClick={() => {
history.push("/settings");
handleClose();
}}
>
<ListItemIcon className={classes.menuItemIcon}>
<SettingsIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">{t("User.Settings")}</Typography>
</MenuItem>
<MenuItem onClick={logout}>
<ListItemIcon className={classes.menuItemIcon}>
<ExitToAppIcon fontSize="small" />
</ListItemIcon>
<Typography variant="inherit">{t("User.Logout")}</Typography>
</MenuItem> </MenuItem>
<MenuItem onClick={logout}>Logout</MenuItem>
</Menu> </Menu>
</div> </div>
); );

View File

@ -13,10 +13,12 @@ import {
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft"; import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
import ChevronRightIcon from "@material-ui/icons/ChevronRight"; import ChevronRightIcon from "@material-ui/icons/ChevronRight";
import ListItem from "@material-ui/core/ListItem"; import ListItem from "@material-ui/core/ListItem";
import DeleteIcon from "@material-ui/icons/Delete"; import BuildIcon from "@material-ui/icons/Build";
import DnsIcon from "@material-ui/icons/Dns"; import DnsIcon from "@material-ui/icons/Dns";
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
import SettingsIcon from "@material-ui/icons/Settings"; import SettingsIcon from "@material-ui/icons/Settings";
import DashboardIcon from "@material-ui/icons/Dashboard"; import DashboardIcon from "@material-ui/icons/Dashboard";
import FeaturedPlayListIcon from "@material-ui/icons/FeaturedPlayList";
import { useHistory } from "react-router-dom"; import { useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import styles from "./styles"; import styles from "./styles";
@ -74,14 +76,24 @@ const Sidebar = ({ open, handleDrawerClose }) => {
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("Menu.Machines")} /> <ListItemText primary={t("Menu.Machines")} />
</ListItem> </ListItem>
<ListItem button key="system" onClick={() => history.push("/system")}>
<ListItemIcon>
<DeviceHubIcon />
</ListItemIcon>
<ListItemText primary={t("Menu.System")} />
</ListItem>
</List> </List>
<Divider /> <Divider />
<List> <List>
<ListItem button key="trash" onClick={() => history.push("/trash")}> <ListItem
button
key="administration"
onClick={() => history.push("/administration")}
>
<ListItemIcon> <ListItemIcon>
<DeleteIcon /> <BuildIcon />
</ListItemIcon> </ListItemIcon>
<ListItemText primary={t("Menu.Trash")} /> <ListItemText primary={t("Menu.Administration")} />
</ListItem> </ListItem>
<ListItem <ListItem
button button
@ -94,6 +106,15 @@ const Sidebar = ({ open, handleDrawerClose }) => {
<ListItemText primary={t("Menu.Settings")} /> <ListItemText primary={t("Menu.Settings")} />
</ListItem> </ListItem>
</List> </List>
<Divider />
<List>
<ListItem button key="about" onClick={() => history.push("/about")}>
<ListItemIcon>
<FeaturedPlayListIcon />
</ListItemIcon>
<ListItemText primary={t("Menu.About")} />
</ListItem>
</List>
</Drawer> </Drawer>
); );
}; };

View File

@ -62,6 +62,9 @@ const styles = theme => ({
content: { content: {
flexGrow: 1, flexGrow: 1,
padding: theme.spacing(2) padding: theme.spacing(2)
},
menuItemIcon: {
minWidth: "26px"
} }
}); });

View File

@ -0,0 +1,58 @@
import React, { useState, useMemo } from "react";
import PageTitle from "../../components/common/PageTitle";
import BubbleChartIcon from "@material-ui/icons/BubbleChart";
import NotesIcon from "@material-ui/icons/Notes";
import TimelineIcon from "@material-ui/icons/Timeline";
import { useTranslation } from "react-i18next";
import AboutSystemContainer from "./system/AboutSystemContainer";
import ReleaseNotesContainer from "./releaseNotes/ReleaseNotesContainer";
import NavigationButtons from "../../components/common/NavigationButtons";
const NavigationTabs = {
SYSTEM: "About.Navigation.System",
RELEASE_NOTES: "About.Navigation.ReleaseNotes",
TIMELINE: "About.Navigation.Timeline"
};
const tabs = [
{
code: NavigationTabs.SYSTEM,
icon: BubbleChartIcon
},
{
code: NavigationTabs.RELEASE_NOTES,
icon: NotesIcon
},
{
code: NavigationTabs.TIMELINE,
icon: TimelineIcon
}
];
const AboutContainer = () => {
const [tab, setTab] = useState(NavigationTabs.SYSTEM);
const { t } = useTranslation();
const navigationTabs = useMemo(
() => tabs.map(z => ({ ...z, tooltip: t(z.code) })),
[t]
);
return (
<>
<PageTitle
text={t(tab)}
navigation={
<NavigationButtons tabs={navigationTabs} onTabChange={setTab} />
}
/>
{tab === NavigationTabs.SYSTEM && <AboutSystemContainer />}
{tab === NavigationTabs.RELEASE_NOTES && <ReleaseNotesContainer />}
{tab === NavigationTabs.TIMELINE && (
<ReleaseNotesContainer view="timeline" />
)}
</>
);
};
export default AboutContainer;

View File

@ -1,7 +0,0 @@
import React from "react";
const AboutContainer = () => {
return <div>TEST</div>;
};
export default AboutContainer;

View File

@ -0,0 +1,27 @@
import React from "react";
import PropTypes from "prop-types";
import Typography from "@material-ui/core/Typography";
const ReleaseNote = ({ releaseNote }) => {
return (
<div>
{releaseNote.notes.map(note => {
return (
<Typography
key={releaseNote.notes.indexOf(note)}
variant="body2"
gutterBottom
>
{note}
</Typography>
);
})}
</div>
);
};
ReleaseNote.propTypes = {
releaseNote: PropTypes.object.isRequired
};
export default ReleaseNote;

View File

@ -0,0 +1,37 @@
import React from "react";
import PropTypes from "prop-types";
import { Grid, Typography } from "@material-ui/core";
import { useTranslation } from "react-i18next";
const ReleaseNoteSummary = ({ releaseNote, collapsed }) => {
const { t } = useTranslation();
return (
<Grid container>
<Grid item xs={6} sm={2} md={2}>
<Typography variant={collapsed ? "subtitle2" : "h6"}>
{`${t("About.ReleaseNotes.Version")}: ${releaseNote.version}`}
</Typography>
</Grid>
<Grid item xs={6} sm={2} md={collapsed ? 2 : 4}>
<Typography variant={collapsed ? "subtitle2" : "h6"}>
{`${t("About.ReleaseNotes.Date")}: ${t("DATE_FORMAT", {
date: { value: releaseNote.date, format: "DD-MM-YYYY HH:mm" }
})}`}
</Typography>
</Grid>
{collapsed && (
<Grid item xs={12} sm={8} md={8}>
<Typography variant="body2">{releaseNote.notes[0]}</Typography>
</Grid>
)}
</Grid>
);
};
ReleaseNoteSummary.propTypes = {
releaseNote: PropTypes.object.isRequired,
collapsed: PropTypes.bool.isRequired
};
export default ReleaseNoteSummary;

View File

@ -0,0 +1,40 @@
import React, { useState, useEffect } from "react";
import PropTypes from "prop-types";
import useApi from "../../../api";
import ReleaseNotesList from "./ReleaseNotesList";
import TimelineComponent from "../timeline/TimelineComponent";
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 });
const api = useApi();
useEffect(() => {
if (state.loaded) return;
api.readReleaseNotes({
onCompleted: data => {
setState({ data, loaded: true });
}
});
}, [api, state.loaded]);
return (
<>
{state.loaded &&
(view === "timeline" ? (
<TimelineComponent releases={sort(state.data)} />
) : (
<ReleaseNotesList releases={sort(state.data)} />
))}
</>
);
};
ReleaseNotesContainer.propTypes = {
view: PropTypes.string
};
export default ReleaseNotesContainer;

View File

@ -0,0 +1,64 @@
import React, { useState } from "react";
import PropTypes from "prop-types";
import {
Accordion,
AccordionSummary,
AccordionDetails
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import ReleaseNoteSummary from "./ReleaseNoteSummary";
import ReleaseNote from "./ReleaseNote";
const ReleaseNotesList = ({ releases }) => {
const [flags, setFlags] = useState({});
const handleToggle = key => (_, expanded) => {
setFlags(prev => ({
...prev,
[key]: expanded
}));
};
const isCollapsed = key => {
const expanded = flags[key];
const collapsed = !expanded || expanded === false;
return collapsed;
};
console.log(
"sortedReleases",
JSON.stringify(releases.map(z => ({ version: z.version, date: z.date })))
);
return (
<>
{releases.map(release => {
return (
<Accordion
key={release.version}
onChange={handleToggle(release.version)}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
id={`panel-${release.version}-header`}
>
<ReleaseNoteSummary
releaseNote={release}
collapsed={isCollapsed(release.version)}
/>
</AccordionSummary>
<AccordionDetails>
<ReleaseNote releaseNote={release} />
</AccordionDetails>
</Accordion>
);
})}
</>
);
};
ReleaseNotesList.propTypes = {
releases: PropTypes.array.isRequired
};
export default ReleaseNotesList;

View File

@ -0,0 +1,103 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Card from "@material-ui/core/Card";
import CardActions from "@material-ui/core/CardActions";
import CardContent from "@material-ui/core/CardContent";
import Button from "@material-ui/core/Button";
import Typography from "@material-ui/core/Typography";
import { useTranslation } from "react-i18next";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
const useStyles = makeStyles(theme => ({
bullet: {
display: "inline-block",
margin: "0 4px",
transform: "scale(1.5)"
},
service: {
marginTop: theme.spacing(1)
}
}));
const buttons = [
{
code: "About.System.Services.Frontend",
url: "https://lab.code-rove.com/gitea/tudor.stanciu/network-resurrector-frontend"
},
{
code: "About.System.Services.Api",
url: "https://lab.code-rove.com/gitea/tudor.stanciu/network-resurrector"
},
{
code: "About.System.Services.Server",
url: "https://lab.code-rove.com/gitea/tudor.stanciu/network-resurrector"
},
{
code: "About.System.Services.Tuitio",
url: "https://lab.code-rove.com/gitea/tudor.stanciu/tuitio"
}
];
const AboutSystemComponent = ({ handleOpenInNewTab }) => {
const classes = useStyles();
const { t } = useTranslation();
const bullet = <span className={classes.bullet}></span>;
return (
<Card variant="outlined">
<CardContent>
<Typography variant="h5" gutterBottom>
{t("About.System.Description.Title")}
</Typography>
<Typography color="textSecondary">
{t("About.System.Description.FirstPhrase")}
</Typography>
<Typography color="textSecondary">
{t("About.System.Description.SecondPhrase")}
</Typography>
<Typography className={classes.service} color="textSecondary">
{bullet}
{t("About.System.Description.Frontend")}
</Typography>
<Typography className={classes.service} color="textSecondary">
{bullet}
{t("About.System.Description.Api")}
</Typography>
<Typography className={classes.service} color="textSecondary">
{bullet}
{t("About.System.Description.Server")}
</Typography>
<Typography className={classes.service} color="textSecondary">
{bullet}
{t("About.System.Description.Agent")}
</Typography>
<Typography className={classes.service} color="textSecondary">
{bullet}
{t("About.System.Description.Tuitio")}
</Typography>
</CardContent>
<CardActions>
{buttons.map(button => (
<Button
key={button.code}
size="small"
color="primary"
startIcon={<OpenInNewIcon />}
onClick={handleOpenInNewTab(button.url)}
>
{t(button.code)}
</Button>
))}
</CardActions>
</Card>
);
};
AboutSystemComponent.propTypes = {
handleOpenInNewTab: PropTypes.func.isRequired
};
export default AboutSystemComponent;

View File

@ -0,0 +1,35 @@
import React from "react";
import { makeStyles } from "@material-ui/core/styles";
import AboutSystemComponent from "./AboutSystemComponent";
import SystemVersionContainer from "./SystemVersionContainer";
const useStyles = makeStyles(theme => {
return {
page: {
display: "flex",
flexDirection: "column"
},
element: {
marginTop: theme.spacing(1)
}
};
});
const AboutSystemContainer = () => {
const classes = useStyles();
const handleOpenInNewTab = url => event => {
window.open(url, "_blank");
event.preventDefault();
};
return (
<div className={classes.page}>
<AboutSystemComponent handleOpenInNewTab={handleOpenInNewTab} />
<div className={classes.element}>
<SystemVersionContainer />
</div>
</div>
);
};
export default AboutSystemContainer;

View File

@ -0,0 +1,154 @@
import React, { useMemo, useEffect, useState } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
List,
ListItem,
ListItemText,
ListItemAvatar
} from "@material-ui/core";
import Avatar from "@material-ui/core/Avatar";
import WebAssetIcon from "@material-ui/icons/WebAsset";
import DeveloperBoardIcon from "@material-ui/icons/DeveloperBoard";
import SettingsInputSvideoIcon from "@material-ui/icons/SettingsInputSvideo";
import { useTranslation } from "react-i18next";
import packageData from "../../../../package.json";
import Paper from "@material-ui/core/Paper";
const useStyles = makeStyles(theme => {
return {
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 SystemVersionComponent = ({ data }) => {
const classes = useStyles();
const [listClass, setListClass] = useState(classes.horizontally);
const { t } = useTranslation();
useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 800px)");
function handleMatches(event) {
const cssClass = event.matches ? classes.vertical : classes.horizontally;
setListClass(cssClass);
}
handleMatches(mediaQuery);
mediaQuery.addListener(handleMatches);
return () => {
mediaQuery.removeListener(handleMatches);
};
}, [classes.horizontally, classes.vertical]);
const lastReleaseDate = useMemo(() => {
const format = "DD-MM-YYYY HH:mm:ss";
const server = t("DATE_FORMAT", {
date: {
value: data.server.lastReleaseDate,
format
}
});
const api = t("DATE_FORMAT", {
date: {
value: data.api.lastReleaseDate,
format
}
});
const frontend = t("DATE_FORMAT", {
date: {
value: process.env.APP_DATE ?? new Date(),
format
}
});
return { server, api, frontend };
}, [data, t]);
return (
<Paper variant="outlined">
<List className={listClass}>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.versionAvatar}>
<DeveloperBoardIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("About.System.Version.Server", {
version: data.server.version
})}
</span>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.server
})}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.versionAvatar}>
<SettingsInputSvideoIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("About.System.Version.Api", {
version: data.api.version
})}
</span>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.api
})}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar className={classes.versionAvatar}>
<WebAssetIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary={
<span className={classes.value}>
{t("About.System.Version.Frontend", {
version: process.env.APP_VERSION ?? packageData.version
})}
</span>
}
secondary={t("About.System.Version.LastReleaseDate", {
date: lastReleaseDate.frontend
})}
/>
</ListItem>
</List>
</Paper>
);
};
SystemVersionComponent.propTypes = {
data: PropTypes.object.isRequired
};
export default SystemVersionComponent;

View File

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

View File

@ -0,0 +1,117 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import Timeline from "@material-ui/lab/Timeline";
import TimelineItem from "@material-ui/lab/TimelineItem";
import TimelineSeparator from "@material-ui/lab/TimelineSeparator";
import TimelineConnector from "@material-ui/lab/TimelineConnector";
import TimelineContent from "@material-ui/lab/TimelineContent";
import TimelineOppositeContent from "@material-ui/lab/TimelineOppositeContent";
import TimelineDot from "@material-ui/lab/TimelineDot";
import Paper from "@material-ui/core/Paper";
import Typography from "@material-ui/core/Typography";
import { useTranslation } from "react-i18next";
import { getRandomElement } from "../../../utils";
import {
Announcement,
AmpStories,
Apps,
BugReport,
DeviceHub,
Equalizer,
FilterTiltShift,
Grain,
Layers,
LocalOffer,
Memory,
NetworkCheck,
OfflineBolt,
Star,
Whatshot,
Widgets
} from "@material-ui/icons";
const timelineIcons = [
Announcement,
AmpStories,
Apps,
BugReport,
DeviceHub,
Equalizer,
FilterTiltShift,
Grain,
Layers,
LocalOffer,
Memory,
NetworkCheck,
OfflineBolt,
Star,
Whatshot,
Widgets
];
const timelineDotVariants = [
{ color: "primary", variant: "outlined" },
{ color: "secondary", variant: "outlined" }
];
const useStyles = makeStyles(() => ({
paper: {
padding: "6px 16px"
}
}));
const TimelineComponent = ({ releases }) => {
const classes = useStyles();
const { t } = useTranslation();
const _releases = releases.map((release, index) => {
const isLast = index === releases.length - 1;
const icon = getRandomElement(timelineIcons);
const dot = getRandomElement(timelineDotVariants);
return { ...release, isLast, icon, dot };
});
return (
<Timeline align="alternate">
{_releases.map(release => (
<TimelineItem key={release.version}>
<TimelineOppositeContent>
<Typography variant="body2" color="textSecondary">
{t("DATE_FORMAT", {
date: { value: release.date, format: "DD-MM-YYYY HH:mm" }
})}
</Typography>
</TimelineOppositeContent>
<TimelineSeparator>
<TimelineDot
color={release.dot.color}
variant={release.dot.variant}
>
<release.icon />
</TimelineDot>
{!release.isLast && <TimelineConnector />}
</TimelineSeparator>
<TimelineContent>
<Paper elevation={3} className={classes.paper}>
<Typography variant="h6" component="h1">
{release.notes[0]}
</Typography>
{release.notes.slice(1).map((note, index) => (
<Typography key={index} variant="body2">
{note}
</Typography>
))}
</Paper>
</TimelineContent>
</TimelineItem>
))}
</Timeline>
);
};
TimelineComponent.propTypes = {
releases: PropTypes.array.isRequired
};
export default TimelineComponent;

View File

@ -1,111 +0,0 @@
import React, { useState, useMemo } from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import {
Avatar,
Collapse,
Tooltip,
Divider,
Typography,
Card,
CardContent,
CardActions,
CardHeader,
IconButton
} from "@material-ui/core";
import { AccountBox, RotateLeft, ExitToApp } from "@material-ui/icons";
import LoginComponent from "./LoginComponent";
import { useTranslation } from "react-i18next";
import { useToast } from "../../../hooks";
import styles from "../styles";
import { useTuitioUser } from "@flare/tuitio-client-react";
const useStyles = makeStyles(styles);
const LoggedInComponent = ({ credentials, onChange, onLogin, onLogout }) => {
const classes = useStyles();
const { t } = useTranslation();
const [expanded, setExpanded] = useState(false);
const { info } = useToast();
const { userName } = useTuitioUser();
const handleExpandLogin = () => {
setExpanded(!expanded);
};
const loginDate = useMemo(() => {
const valueForDisplay = t("LONG_DATE", { date: new Date() });
return valueForDisplay;
}, [t]);
const handleLogin = async () => {
const result = await onLogin();
if (result) {
setExpanded(false);
info(t("Login.UserChanged"));
}
};
return (
<div className={classes.loggedInContent}>
<Card className={classes.loggedInCard}>
<CardHeader
avatar={
<Avatar aria-label="user" className={classes.avatar}>
<AccountBox />
</Avatar>
}
title={<strong>{t("Login.Hello", { username: userName })}</strong>}
subheader={
<Tooltip title={t("Login.AuthenticationDate")}>
<Typography variant="caption" display="block">
{loginDate}
</Typography>
</Tooltip>
}
/>
<CardActions disableSpacing>
<Tooltip title={t("Login.ChangeUser")}>
<IconButton
size="small"
className={classes.onRight}
onClick={handleExpandLogin}
aria-expanded={expanded}
aria-label="show login component"
>
<RotateLeft />
</IconButton>
</Tooltip>
<Tooltip title={t("Login.Logout")}>
<IconButton size="small" onClick={onLogout}>
<ExitToApp />
</IconButton>
</Tooltip>
</CardActions>
<Divider />
<Collapse in={expanded} timeout="auto" unmountOnExit>
<CardContent
className={classes.collapseContent}
style={{ paddingBottom: "5px" }}
>
<LoginComponent
credentials={credentials}
onChange={onChange}
onLogin={handleLogin}
/>
</CardContent>
</Collapse>
</Card>
</div>
);
};
LoggedInComponent.propTypes = {
credentials: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
onLogin: PropTypes.func.isRequired,
onLogout: PropTypes.func.isRequired
};
export default LoggedInComponent;

View File

@ -2,8 +2,7 @@ import React, { useState } from "react";
import LoginCard from "./LoginCard"; import LoginCard from "./LoginCard";
import { useToast } from "../../../hooks"; import { useToast } from "../../../hooks";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LoggedInComponent from "./LoggedInComponent"; import { useTuitioClient } from "@flare/tuitio-client-react";
import { useTuitioClient, useTuitioToken } from "@flare/tuitio-client-react";
const LoginContainer = () => { const LoginContainer = () => {
const [credentials, setCredentials] = useState({ const [credentials, setCredentials] = useState({
@ -13,11 +12,10 @@ const LoginContainer = () => {
const { error } = useToast(); const { error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
const { login, logout } = useTuitioClient({ const { login } = useTuitioClient({
onLoginFailed: () => error(t("Login.IncorrectCredentials")), onLoginFailed: () => error(t("Login.IncorrectCredentials")),
onLoginError: err => error(err.message) onLoginError: err => error(err.message)
}); });
const { valid: tokenIsValid } = useTuitioToken();
const handleChange = prop => event => { const handleChange = prop => event => {
setCredentials(prev => ({ ...prev, [prop]: event.target.value })); setCredentials(prev => ({ ...prev, [prop]: event.target.value }));
@ -28,27 +26,12 @@ const LoginContainer = () => {
return login(userName, password); return login(userName, password);
}; };
const handleLogout = () => {
logout();
};
return ( return (
<>
{tokenIsValid ? (
<LoggedInComponent
credentials={credentials}
onChange={handleChange}
onLogin={handleLogin}
onLogout={handleLogout}
/>
) : (
<LoginCard <LoginCard
credentials={credentials} credentials={credentials}
onChange={handleChange} onChange={handleChange}
onLogin={handleLogin} onLogin={handleLogin}
/> />
)}
</>
); );
}; };

View File

@ -1,23 +1,7 @@
const styles = theme => ({ const styles = theme => ({
loggedInCard: {
minWidth: 350
},
onRight: { onRight: {
marginLeft: "auto" marginLeft: "auto"
}, },
avatar: {
backgroundColor: theme.palette.primary.main
},
collapseContent: {
padding: 0
},
loggedInContent: {
minHeight: "80vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
fontSize: "calc(10px + 2vmin)"
},
appLogin: { appLogin: {
minHeight: "100vh", minHeight: "100vh",
display: "flex", display: "flex",

View File

@ -1,148 +0,0 @@
import React, { useMemo } from "react";
import PropTypes from "prop-types";
import {
TableCell,
TableRow,
IconButton,
Collapse,
Tooltip,
Menu
} from "@material-ui/core";
import { KeyboardArrowDown, KeyboardArrowUp } from "@material-ui/icons";
import { makeStyles } from "@material-ui/core/styles";
import MachineLog from "./MachineLog";
import WakeComponent from "./WakeComponent";
import { useSensitiveInfo } from "../../../hooks";
const useRowStyles = makeStyles({
root: {
"& > *": {
borderBottom: "unset"
}
}
});
const ActionButton = React.forwardRef((props, _ref) => {
const { action, machine } = props;
return (
<Tooltip
id={`machine-item-${machine.machineId}-${action.code}-tooltip`}
title={action.tooltip}
>
<span>
<IconButton
id={`machine-item-${machine.machineId}-${action.code}`}
size={"small"}
onClick={action.system ? action.effect : action.effect(machine)}
>
<action.icon />
</IconButton>
</span>
</Tooltip>
);
});
ActionButton.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired
}).isRequired,
action: PropTypes.shape({
code: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
system: PropTypes.bool,
effect: PropTypes.func.isRequired
}).isRequired
};
const Machine = ({
machine,
actions,
logs,
addLog,
secondaryActionsMenuProps
}) => {
const [open, setOpen] = React.useState(false);
const classes = useRowStyles();
const { mask, maskElements } = useSensitiveInfo();
const topActions = useMemo(
() => actions.filter(a => a.top === true),
[actions]
);
const secondaryActions = useMemo(
() => actions.filter(a => a.top === false),
[actions]
);
return (
<React.Fragment>
<TableRow className={classes.root}>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton>
</TableCell>
<TableCell component="th" scope="row">
{mask(machine.fullMachineName)}
</TableCell>
<TableCell>{mask(machine.machineName)}</TableCell>
<TableCell>{mask(machine.iPv4Address)}</TableCell>
<TableCell>{mask(machine.macAddress)}</TableCell>
<TableCell align="right">
<WakeComponent machine={machine} addLog={addLog} />
{topActions.map(action => (
<ActionButton
key={`machine-item-${machine.machineId}-${action.code}`}
action={action}
machine={machine}
/>
))}
<Menu
id="secondary-actions-menu"
anchorEl={secondaryActionsMenuProps.anchor}
keepMounted
open={Boolean(secondaryActionsMenuProps.anchor)}
onClose={secondaryActionsMenuProps.onCloseSecondaryActions}
>
{secondaryActions.map(action => (
<ActionButton
key={`machine-item-${machine.machineId}-${action.code}`}
action={action}
machine={machine}
/>
))}
</Menu>
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<MachineLog logs={maskElements(logs)} />
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
};
Machine.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired,
machineName: PropTypes.string.isRequired,
fullMachineName: PropTypes.string.isRequired,
macAddress: PropTypes.string.isRequired,
iPv4Address: PropTypes.string,
description: PropTypes.string
}).isRequired,
actions: PropTypes.array.isRequired,
logs: PropTypes.array.isRequired,
addLog: PropTypes.func.isRequired,
secondaryActionsMenuProps: PropTypes.object.isRequired
};
export default Machine;

View File

@ -0,0 +1,110 @@
import React from "react";
import PropTypes from "prop-types";
import {
Accordion,
AccordionSummary,
AccordionDetails,
Grid
} from "@material-ui/core";
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
import withStyles from "@material-ui/core/styles/withStyles";
import MachineLog from "./common/MachineLog";
import { DataLabel } from "../../../components/common";
import { useTranslation } from "react-i18next";
import { useSensitiveInfo } from "../../../hooks";
import ActionsGroup from "./common/ActionsGroup";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(() => ({
panel: {
justifyContent: "center",
alignItems: "center"
}
}));
const IconLeftAccordionSummary = withStyles(theme => ({
root: {
minHeight: "20px",
height: "42px",
[theme.breakpoints.down("md")]: {
height: "62px"
},
[theme.breakpoints.down("sm")]: {
height: "102px"
}
},
expandIcon: {
order: -1
}
}))(AccordionSummary);
const GridCell = ({ label, value }) => {
const { mask } = useSensitiveInfo();
return (
<Grid item xs={12} md={6} lg={3}>
<DataLabel label={label} data={mask(value)} />
</Grid>
);
};
GridCell.propTypes = {
label: PropTypes.string.isRequired,
value: PropTypes.string.isRequired
};
const MachineAccordion = ({ machine, actions, logs, addLog }) => {
const { t } = useTranslation();
const classes = useStyles();
return (
<Accordion>
<IconLeftAccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-label="Expand"
aria-controls="additional-actions1-content"
id="additional-actions1-header"
IconButtonProps={{ edge: "start" }}
>
<Grid container className={classes.panel}>
<Grid item xs={11}>
<Grid container>
<GridCell
label={t("Machine.FullName")}
value={machine.fullMachineName}
/>
<GridCell label={t("Machine.Name")} value={machine.machineName} />
<GridCell label={t("Machine.IP")} value={machine.iPv4Address} />
<GridCell label={t("Machine.MAC")} value={machine.macAddress} />
</Grid>
</Grid>
<Grid item xs={1} style={{ textAlign: "right" }}>
<ActionsGroup
className={classes.actions}
machine={machine}
actions={actions}
addLog={addLog}
/>
</Grid>
</Grid>
</IconLeftAccordionSummary>
<AccordionDetails>
<MachineLog logs={logs} />
</AccordionDetails>
</Accordion>
);
};
MachineAccordion.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired,
machineName: PropTypes.string.isRequired,
fullMachineName: PropTypes.string.isRequired,
macAddress: PropTypes.string.isRequired,
iPv4Address: PropTypes.string,
description: PropTypes.string
}).isRequired,
actions: PropTypes.array.isRequired,
logs: PropTypes.array.isRequired,
addLog: PropTypes.func.isRequired
};
export default MachineAccordion;

View File

@ -1,21 +1,15 @@
import React, { useState, useCallback } from "react"; import React, { useState, useCallback } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import Machine from "./Machine"; import MachineTableRow from "./MachineTableRow";
import MachineAccordion from "./MachineAccordion";
import { ViewModes } from "./ViewModeSelection";
import { useToast } from "../../../hooks"; import { useToast } from "../../../hooks";
import { import { LastPage, RotateLeft, Launch, Stop } from "@material-ui/icons";
LastPage,
MoreHoriz,
RotateLeft,
Launch,
Stop
} from "@material-ui/icons";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import useApi from "../../../api"; import useApi from "../../../api";
const MachineContainer = ({ machine }) => { const MachineContainer = ({ machine, viewMode }) => {
const [logs, setLogs] = useState([]); const [logs, setLogs] = useState([]);
const [secondaryActionsAnchor, setSecondaryActionsAnchor] =
React.useState(null);
const { success, error } = useToast(); const { success, error } = useToast();
const { t } = useTranslation(); const { t } = useTranslation();
@ -42,7 +36,7 @@ const MachineContainer = ({ machine }) => {
); );
const pingMachine = useCallback( const pingMachine = useCallback(
machine => async () => { async machine => {
await api.pingMachine(machine.machineId, { await api.pingMachine(machine.machineId, {
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); });
@ -50,35 +44,20 @@ const MachineContainer = ({ machine }) => {
[manageActionResponse, api] [manageActionResponse, api]
); );
const handleOpenSecondaryActions = event => {
setSecondaryActionsAnchor(event.currentTarget);
};
const handleCloseSecondaryActions = () => {
setSecondaryActionsAnchor(null);
};
const secondaryActionsMenuProps = {
anchor: secondaryActionsAnchor,
onCloseSecondaryActions: handleCloseSecondaryActions
};
const shutdownMachine = useCallback( const shutdownMachine = useCallback(
machine => async () => { async machine => {
await api.shutdownMachine(machine.machineId, 0, false, { await api.shutdownMachine(machine.machineId, 0, false, {
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); });
handleCloseSecondaryActions();
}, },
[manageActionResponse, api] [manageActionResponse, api]
); );
const restartMachine = useCallback( const restartMachine = useCallback(
machine => async () => { async machine => {
await api.restartMachine(machine.machineId, 0, false, { await api.restartMachine(machine.machineId, 0, false, {
onCompleted: manageActionResponse onCompleted: manageActionResponse
}); });
handleCloseSecondaryActions();
}, },
[manageActionResponse, api] [manageActionResponse, api]
); );
@ -89,52 +68,56 @@ const MachineContainer = ({ machine }) => {
effect: pingMachine, effect: pingMachine,
icon: LastPage, icon: LastPage,
tooltip: t("Machine.Actions.Ping"), tooltip: t("Machine.Actions.Ping"),
top: true main: true
},
{
code: "more",
effect: handleOpenSecondaryActions,
icon: MoreHoriz,
tooltip: t("Machine.Actions.More"),
top: true,
system: true
}, },
{ {
code: "shutdown", code: "shutdown",
effect: shutdownMachine, effect: shutdownMachine,
icon: Stop, icon: Stop,
tooltip: t("Machine.Actions.Shutdown"), tooltip: t("Machine.Actions.Shutdown"),
top: false main: false
}, },
{ {
code: "restart", code: "restart",
effect: restartMachine, effect: restartMachine,
icon: RotateLeft, icon: RotateLeft,
tooltip: t("Machine.Actions.Restart"), tooltip: t("Machine.Actions.Restart"),
top: false main: false
}, },
{ {
code: "advanced", code: "advanced",
effect: () => {}, effect: () => {},
icon: Launch, icon: Launch,
tooltip: t("Machine.Actions.Advanced"), tooltip: t("Machine.Actions.Advanced"),
top: false main: false
} }
]; ];
return ( return (
<Machine <>
{viewMode === ViewModes.TABLE && (
<MachineTableRow
machine={machine} machine={machine}
actions={actions} actions={actions}
logs={logs} logs={logs}
addLog={addLog} addLog={addLog}
secondaryActionsMenuProps={secondaryActionsMenuProps}
/> />
)}
{viewMode === ViewModes.ACCORDION && (
<MachineAccordion
machine={machine}
actions={actions}
logs={logs}
addLog={addLog}
/>
)}
</>
); );
}; };
MachineContainer.propTypes = { MachineContainer.propTypes = {
machine: PropTypes.object.isRequired machine: PropTypes.object.isRequired,
viewMode: PropTypes.string.isRequired
}; };
export default MachineContainer; export default MachineContainer;

View File

@ -0,0 +1,70 @@
import React from "react";
import PropTypes from "prop-types";
import { TableCell, TableRow, IconButton, Collapse } from "@material-ui/core";
import { KeyboardArrowDown, KeyboardArrowUp } from "@material-ui/icons";
import { makeStyles } from "@material-ui/core/styles";
import MachineLog from "./common/MachineLog";
import { useSensitiveInfo } from "../../../hooks";
import ActionsGroup from "./common/ActionsGroup";
const useRowStyles = makeStyles({
root: {
"& > *": {
borderBottom: "unset"
}
}
});
const MachineTableRow = ({ machine, actions, logs, addLog }) => {
const [open, setOpen] = React.useState(false);
const classes = useRowStyles();
const { mask } = useSensitiveInfo();
return (
<React.Fragment>
<TableRow className={classes.root}>
<TableCell>
<IconButton
aria-label="expand row"
size="small"
onClick={() => setOpen(!open)}
>
{open ? <KeyboardArrowUp /> : <KeyboardArrowDown />}
</IconButton>
</TableCell>
<TableCell component="th" scope="row">
{mask(machine.fullMachineName)}
</TableCell>
<TableCell>{mask(machine.machineName)}</TableCell>
<TableCell>{mask(machine.iPv4Address)}</TableCell>
<TableCell>{mask(machine.macAddress)}</TableCell>
<TableCell align="right">
<ActionsGroup machine={machine} actions={actions} addLog={addLog} />
</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<MachineLog logs={logs} />
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
);
};
MachineTableRow.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired,
machineName: PropTypes.string.isRequired,
fullMachineName: PropTypes.string.isRequired,
macAddress: PropTypes.string.isRequired,
iPv4Address: PropTypes.string,
description: PropTypes.string
}).isRequired,
actions: PropTypes.array.isRequired,
logs: PropTypes.array.isRequired,
addLog: PropTypes.func.isRequired
};
export default MachineTableRow;

View File

@ -1,16 +1,19 @@
import React, { useContext, useEffect, useCallback } from "react"; import React, { useContext, useEffect, useCallback, useState } from "react";
import { import {
ApplicationStateContext, NetworkStateContext,
ApplicationDispatchContext NetworkDispatchContext
} from "../../../state/contexts"; } from "../../network/state/contexts";
import useApi from "../../../api"; import useApi from "../../../api";
import MachinesList from "./MachinesList"; 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";
const MachinesContainer = () => { const MachinesContainer = () => {
const state = useContext(ApplicationStateContext); const [viewMode, setViewMode] = useState(null);
const dispatchActions = useContext(ApplicationDispatchContext);
const state = useContext(NetworkStateContext);
const dispatchActions = useContext(NetworkDispatchContext);
const { t } = useTranslation(); const { t } = useTranslation();
const api = useApi(); const api = useApi();
@ -32,8 +35,21 @@ const MachinesContainer = () => {
return ( return (
<> <>
<PageTitle text={t("Menu.Machines")} /> <PageTitle
<MachinesList dense={true} machines={state.network.machines} /> text={t("Menu.Machines")}
toolBar={
<ViewModeSelection
callback={setViewMode}
initialMode={ViewModes.TABLE}
/>
}
/>
{viewMode && (
<MachinesListComponent
machines={state.network.machines}
viewMode={viewMode}
/>
)}
</> </>
); );
}; };

View File

@ -1,48 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow
} from "@material-ui/core";
import Paper from "@material-ui/core/Paper";
import MachineContainer from "./MachineContainer";
import { useTranslation } from "react-i18next";
const MachinesList = ({ dense, machines }) => {
const { t } = useTranslation();
return (
<TableContainer component={Paper}>
<Table aria-label="collapsible table" size={dense ? "small" : "medium"}>
<TableHead>
<TableRow>
<TableCell />
<TableCell>{t("Machine.FullName")}</TableCell>
<TableCell>{t("Machine.Name")}</TableCell>
<TableCell>{t("Machine.IP")}</TableCell>
<TableCell>{t("Machine.MAC")}</TableCell>
<TableCell align="right" />
</TableRow>
</TableHead>
<TableBody>
{machines.map(machine => (
<MachineContainer
key={`machine-${machine.machineId}`}
machine={machine}
/>
))}
</TableBody>
</Table>
</TableContainer>
);
};
MachinesList.propTypes = {
dense: PropTypes.bool.isRequired,
machines: PropTypes.array.isRequired
};
export default MachinesList;

View File

@ -0,0 +1,80 @@
import React from "react";
import PropTypes from "prop-types";
import {
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow
} from "@material-ui/core";
import Paper from "@material-ui/core/Paper";
import MachineContainer from "./MachineContainer";
import { useTranslation } from "react-i18next";
import { ViewModes } from "./ViewModeSelection";
const MachinesList = ({ machines, viewMode }) => {
return (
<>
{machines.map(machine => (
<MachineContainer
key={`machine-${machine.machineId}`}
machine={machine}
viewMode={viewMode}
/>
))}
</>
);
};
MachinesList.propTypes = {
machines: PropTypes.array.isRequired,
viewMode: PropTypes.string.isRequired
};
const MachinesTableList = ({ machines, viewMode }) => {
const { t } = useTranslation();
return (
<TableContainer component={Paper}>
<Table aria-label="collapsible table" size="small">
<TableHead>
<TableRow>
<TableCell />
<TableCell>{t("Machine.FullName")}</TableCell>
<TableCell>{t("Machine.Name")}</TableCell>
<TableCell>{t("Machine.IP")}</TableCell>
<TableCell>{t("Machine.MAC")}</TableCell>
<TableCell align="right" />
</TableRow>
</TableHead>
<TableBody>
<MachinesList machines={machines} viewMode={viewMode} />
</TableBody>
</Table>
</TableContainer>
);
};
MachinesTableList.propTypes = {
machines: PropTypes.array.isRequired,
viewMode: PropTypes.string.isRequired
};
const MachinesListComponent = ({ machines, viewMode }) => {
return (
<>
{viewMode === ViewModes.TABLE ? (
<MachinesTableList machines={machines} viewMode={viewMode} />
) : (
<MachinesList machines={machines} viewMode={viewMode} />
)}
</>
);
};
MachinesListComponent.propTypes = {
machines: PropTypes.array.isRequired,
viewMode: PropTypes.string.isRequired
};
export default MachinesListComponent;

View File

@ -0,0 +1,80 @@
import React, { useState, useEffect, useCallback } from "react";
import PropTypes from "prop-types";
import TableChartIcon from "@material-ui/icons/TableChart";
import ViewListIcon from "@material-ui/icons/ViewList";
import ToggleButton from "@material-ui/lab/ToggleButton";
import ToggleButtonGroup from "@material-ui/lab/ToggleButtonGroup";
import { Tooltip } from "@material-ui/core";
import { useTranslation } from "react-i18next";
export const ViewModes = {
TABLE: "table",
ACCORDION: "accordion"
};
const ViewModeSelection = ({ initialMode, callback }) => {
const [state, setState] = useState({
mode: initialMode,
manual: false
});
const { t } = useTranslation();
const handleViewModeSelection = useCallback((event, mode) => {
setState({ mode, manual: true });
}, []);
useEffect(() => {
const mediaQuery = window.matchMedia("(max-width: 1100px)");
function handleMatches(event) {
if (state.manual === true) return;
const mode = event.matches ? ViewModes.ACCORDION : ViewModes.TABLE;
setState({ mode, manual: false });
}
handleMatches(mediaQuery);
mediaQuery.addListener(handleMatches);
return () => {
mediaQuery.removeListener(handleMatches);
};
}, [state.manual]);
useEffect(() => callback && callback(state.mode), [callback, state.mode]);
return (
<ToggleButtonGroup
size="small"
value={state.mode}
exclusive
onChange={handleViewModeSelection}
>
<ToggleButton
value={ViewModes.TABLE}
aria-label="table view mode"
disabled={state.mode === ViewModes.TABLE}
>
<Tooltip title={t("ViewModes.Table")}>
<TableChartIcon />
</Tooltip>
</ToggleButton>
<ToggleButton
value={ViewModes.ACCORDION}
aria-label="accordion view mode"
disabled={state.mode === ViewModes.ACCORDION}
>
<Tooltip title={t("ViewModes.List")}>
<ViewListIcon />
</Tooltip>
</ToggleButton>
</ToggleButtonGroup>
);
};
ViewModeSelection.propTypes = {
initialMode: PropTypes.oneOf([ViewModes.TABLE, ViewModes.ACCORDION]),
callback: PropTypes.func
};
export default ViewModeSelection;

View File

@ -0,0 +1,45 @@
import React from "react";
import PropTypes from "prop-types";
import { IconButton, Tooltip } from "@material-ui/core";
const ActionButton = React.forwardRef((props, _ref) => {
const { action, machine, callback } = props;
const id = `machine-item-${machine.machineId}-${action.code}`;
const handleActionClick = event => {
action.effect(machine, event);
callback && callback(machine);
event.stopPropagation();
};
return (
<Tooltip
id={`machine-item-${machine.machineId}-${action.code}-tooltip`}
title={action.tooltip}
>
<span>
<IconButton
id={id}
size={"small"}
onFocus={event => event.stopPropagation()}
onClick={handleActionClick}
>
<action.icon />
</IconButton>
</span>
</Tooltip>
);
});
ActionButton.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired
}).isRequired,
action: PropTypes.shape({
code: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
effect: PropTypes.func.isRequired
}).isRequired,
callback: PropTypes.func
};
export default ActionButton;

View File

@ -0,0 +1,85 @@
import React, { useMemo, useState } from "react";
import PropTypes from "prop-types";
import WakeComponent from "./WakeComponent";
import ActionButton from "./ActionButton";
import { Menu } from "@material-ui/core";
import { MoreHoriz } from "@material-ui/icons";
import { useTranslation } from "react-i18next";
const ActionsGroup = ({ machine, actions, addLog }) => {
const [menuAnchor, setMenuAnchor] = useState(null);
const { t } = useTranslation();
const mainActions = useMemo(
() => actions.filter(a => a.main === true),
[actions]
);
const secondaryActions = useMemo(
() => actions.filter(a => a.main === false),
[actions]
);
const handleMenuOpen = (_, event) => {
setMenuAnchor(event.currentTarget);
};
const handleMenuClose = () => {
setMenuAnchor(null);
};
return (
<>
<WakeComponent machine={machine} addLog={addLog} />
{mainActions.map(action => (
<ActionButton
key={`machine-item-${machine.machineId}-${action.code}`}
action={action}
machine={machine}
/>
))}
<ActionButton
action={{
code: "more",
effect: handleMenuOpen,
icon: MoreHoriz,
tooltip: t("Machine.Actions.More")
}}
machine={machine}
/>
<Menu
id="secondary-actions-menu"
anchorEl={menuAnchor}
keepMounted
open={Boolean(menuAnchor)}
onClose={handleMenuClose}
>
{secondaryActions.map(action => (
<ActionButton
key={`machine-item-${machine.machineId}-${action.code}`}
action={action}
machine={machine}
callback={handleMenuClose}
/>
))}
</Menu>
</>
);
};
ActionsGroup.propTypes = {
machine: PropTypes.shape({
machineId: PropTypes.number.isRequired,
machineName: PropTypes.string.isRequired,
fullMachineName: PropTypes.string.isRequired,
macAddress: PropTypes.string.isRequired,
iPv4Address: PropTypes.string,
description: PropTypes.string
}).isRequired,
actions: PropTypes.array.isRequired,
addLog: PropTypes.func.isRequired
};
export default ActionsGroup;

View File

@ -1,15 +1,19 @@
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { Box } from "@material-ui/core"; import { Box } from "@material-ui/core";
import { useSensitiveInfo } from "../../../../hooks";
import { LazyLog, ScrollFollow } from "react-lazylog"; import { LazyLog, ScrollFollow } from "react-lazylog";
const MachineLog = ({ logs }) => { const MachineLog = ({ logs }) => {
const { maskElements } = useSensitiveInfo();
const displayLogs = useMemo( const displayLogs = useMemo(
() => (logs.length > 0 ? logs.join("\n") : "..."), () => (logs.length > 0 ? maskElements(logs).join("\n") : "..."),
[logs] [logs, maskElements]
); );
return ( return (
<Box margin={1}> <Box width="100%">
<div style={{ height: 200 }}> <div style={{ height: 200 }}>
<ScrollFollow <ScrollFollow
startFollowing={true} startFollowing={true}

View File

@ -3,9 +3,9 @@ import PropTypes from "prop-types";
import { IconButton, Tooltip } from "@material-ui/core"; import { IconButton, Tooltip } from "@material-ui/core";
import { PowerSettingsNew } from "@material-ui/icons"; 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 useApi from "../../../../api";
const initialState = { on: false }; const initialState = { on: false };
const defaultPingInterval = 1200000; //20 minutes const defaultPingInterval = 1200000; //20 minutes
@ -80,6 +80,11 @@ const WakeComponent = ({ machine, addLog }) => {
useEffect(pingInLoop, [trigger, pingInLoop]); useEffect(pingInLoop, [trigger, pingInLoop]);
const handleWakeClick = event => {
wakeMachine();
event.stopPropagation();
};
return ( return (
<Tooltip title={t(state.on ? "Machine.PoweredOn" : "Machine.Actions.Wake")}> <Tooltip title={t(state.on ? "Machine.PoweredOn" : "Machine.Actions.Wake")}>
<span> <span>
@ -87,8 +92,9 @@ const WakeComponent = ({ machine, addLog }) => {
id={`machine-${machine.machineId}-wake`} id={`machine-${machine.machineId}-wake`}
size={"small"} size={"small"}
disabled={state.on} disabled={state.on}
onClick={wakeMachine} onClick={handleWakeClick}
style={state.on ? { color: "#33cc33" } : {}} style={state.on ? { color: "#33cc33" } : {}}
onFocus={event => event.stopPropagation()}
> >
<PowerSettingsNew /> <PowerSettingsNew />
</IconButton> </IconButton>

View File

@ -1,19 +1,12 @@
import React from "react"; import React from "react";
import MachinesContainer from "../../machines/components/MachinesContainer"; import MachinesContainer from "../../machines/components/MachinesContainer";
//import NotesContainer from "../../notes/components/NotesContainer"; import NetworkStateProvider from "../state/NetworkStateProvider";
import { makeStyles } from "@material-ui/core/styles";
import styles from "../styles";
const useStyles = makeStyles(styles);
const NetworkContainer = () => { const NetworkContainer = () => {
const classes = useStyles();
return ( return (
<div className={classes.root}> <NetworkStateProvider>
<MachinesContainer /> <MachinesContainer />
{/* <NotesContainer /> */} </NetworkStateProvider>
</div>
); );
}; };

View File

@ -0,0 +1,27 @@
import React, { useReducer, useMemo } from "react";
import PropTypes from "prop-types";
import { NetworkStateContext, NetworkDispatchContext } from "./contexts";
import { reducer, dispatchActions as reducerDispatchActions } from "./reducer";
import { initialState } from "./initialState";
const NetworkStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchActions = useMemo(
() => reducerDispatchActions(dispatch),
[dispatch]
);
return (
<NetworkStateContext.Provider value={state}>
<NetworkDispatchContext.Provider value={dispatchActions}>
{children}
</NetworkDispatchContext.Provider>
</NetworkStateContext.Provider>
);
};
NetworkStateProvider.propTypes = {
children: PropTypes.node.isRequired
};
export default NetworkStateProvider;

View File

@ -0,0 +1,4 @@
import React from "react";
export const NetworkStateContext = React.createContext();
export const NetworkDispatchContext = React.createContext();

View File

@ -1,7 +0,0 @@
const styles = () => ({
root: {
margin: "15px"
}
});
export default styles;

View File

@ -1,33 +0,0 @@
import React, { useContext } from "react";
import { TextField, Button } from "@material-ui/core";
import {
ApplicationStateContext,
ApplicationDispatchContext
} from "../../../state/ApplicationContexts";
const NotesContainer = () => {
const state = useContext(ApplicationStateContext);
const dispatchActions = useContext(ApplicationDispatchContext);
const handleChange = prop => event => {
dispatchActions.onNetworkChange(prop, event.target.value);
};
return (
<>
<TextField
id="ertet"
label="Test"
onChange={handleChange("test")}
value={state.network.test}
/>
<br />
<br />
<Button variant="contained" color="primary">
Read machines
</Button>
</>
);
};
export default NotesContainer;

View File

@ -0,0 +1,49 @@
import React, { useState, useMemo } from "react";
import BrushIcon from "@material-ui/icons/Brush";
import NotificationsIcon from "@material-ui/icons/Notifications";
import { useTranslation } from "react-i18next";
import PageTitle from "../../components/common/PageTitle";
import NavigationButtons from "../../components/common/NavigationButtons";
import AppearanceContainer from "./appearance/AppearanceContainer";
import NotificationsContainer from "./notifications/NotificationsContainer";
const NavigationTabs = {
APPEARANCE: "Settings.Navigation.Appearance",
NOTIFICATIONS: "Settings.Navigation.Notifications"
};
const tabs = [
{
code: NavigationTabs.APPEARANCE,
icon: BrushIcon
},
{
code: NavigationTabs.NOTIFICATIONS,
icon: NotificationsIcon
}
];
const SettingsContainer = () => {
const [tab, setTab] = useState(NavigationTabs.APPEARANCE);
const { t } = useTranslation();
const navigationTabs = useMemo(
() => tabs.map(z => ({ ...z, tooltip: t(z.code) })),
[t]
);
return (
<>
<PageTitle
text={t(tab)}
navigation={
<NavigationButtons tabs={navigationTabs} onTabChange={setTab} />
}
/>
{tab === NavigationTabs.APPEARANCE && <AppearanceContainer />}
{tab === NavigationTabs.NOTIFICATIONS && <NotificationsContainer />}
</>
);
};
export default SettingsContainer;

View File

@ -0,0 +1,61 @@
import React from "react";
import { useApplicationTheme } from "../../../providers/ThemeProvider";
import { Grid, Paper, FormControlLabel, Switch } from "@material-ui/core";
import LanguageContainer from "./language/LanguageContainer";
import { makeStyles } from "@material-ui/core/styles";
const useStyles = makeStyles(theme => ({
paper: {
paddingTop: theme.spacing(1)
},
language: {
paddingLeft: theme.spacing(1)
}
}));
const AppearanceComponent = () => {
const { isDark, onDarkModeChanged } = useApplicationTheme();
const classes = useStyles();
const handleChange = event => {
const { checked } = event.target;
onDarkModeChanged(checked);
};
return (
<Paper variant="outlined" className={classes.paper}>
<Grid container spacing={0}>
<Grid item xs={12} sm={6} md={4} lg={3}>
<FormControlLabel
value="start"
control={
<Switch
checked={isDark}
onChange={handleChange}
color="secondary"
name="dark-mode-switch"
/>
}
label="Dark mode:"
labelPlacement="start"
/>
</Grid>
<Grid item xs={12} sm={6} md={4} lg={3}>
<FormControlLabel
value="start"
control={
<div className={classes.language}>
<LanguageContainer />
</div>
}
label="Language:"
labelPlacement="start"
/>
</Grid>
</Grid>
</Paper>
);
};
export default AppearanceComponent;

View File

@ -0,0 +1,8 @@
import React from "react";
import AppearanceComponent from "./AppearanceComponent";
const AppearanceContainer = () => {
return <AppearanceComponent />;
};
export default AppearanceContainer;

View File

@ -1,8 +1,8 @@
import React from "react"; import React from "react";
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import Flag from "react-flags"; import Flag from "react-flags";
import { IconButton } from "@material-ui/core"; import { IconButton, Menu, MenuItem } from "@material-ui/core";
import LanguageMenu from "./LanguageMenu"; import { useTranslation } from "react-i18next";
const LanguageComponent = ({ const LanguageComponent = ({
languageIsSet, languageIsSet,
@ -11,8 +11,11 @@ const LanguageComponent = ({
onLanguageChange, onLanguageChange,
onClose, onClose,
flag, flag,
getFlagsPath flagsPath
}) => { }) => {
const { t } = useTranslation();
const open = Boolean(anchorEl);
return ( return (
<> <>
<IconButton <IconButton
@ -20,6 +23,7 @@ const LanguageComponent = ({
aria-haspopup="true" aria-haspopup="true"
onClick={onMenuOpen} onClick={onMenuOpen}
color="inherit" color="inherit"
size="small"
> >
{languageIsSet && ( {languageIsSet && (
<Flag <Flag
@ -27,16 +31,33 @@ const LanguageComponent = ({
format="png" format="png"
pngSize={32} pngSize={32}
shiny={true} shiny={true}
basePath={getFlagsPath()} basePath={flagsPath}
alt={flag.alt} alt={flag.alt}
/> />
)} )}
</IconButton> </IconButton>
<LanguageMenu <Menu
id="language-menu"
anchorEl={anchorEl} anchorEl={anchorEl}
onLanguageChange={onLanguageChange} anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "left"
}}
open={open}
onClose={onClose} onClose={onClose}
/> >
<MenuItem onClick={onLanguageChange("ro")}>
{t("Language.Romanian")}
</MenuItem>
<MenuItem onClick={onLanguageChange("en")}>
{t("Language.English")}
</MenuItem>
</Menu>
</> </>
); );
}; };
@ -48,7 +69,7 @@ LanguageComponent.propTypes = {
onLanguageChange: PropTypes.func.isRequired, onLanguageChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired, onClose: PropTypes.func.isRequired,
flag: PropTypes.object.isRequired, flag: PropTypes.object.isRequired,
getFlagsPath: PropTypes.func.isRequired flagsPath: PropTypes.string.isRequired
}; };
export default LanguageComponent; export default LanguageComponent;

View File

@ -2,6 +2,10 @@ import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import LanguageComponent from "./LanguageComponent"; import LanguageComponent from "./LanguageComponent";
const flagsPath = process.env.PUBLIC_URL
? `${process.env.PUBLIC_URL}/flags`
: "flags";
const LanguageContainer = () => { const LanguageContainer = () => {
const [anchorEl, setAnchorEl] = useState(null); const [anchorEl, setAnchorEl] = useState(null);
@ -34,15 +38,6 @@ const LanguageContainer = () => {
setAnchorEl(null); setAnchorEl(null);
}; };
const getFlagsPath = () => {
const basePath = "flags";
if (process.env.PUBLIC_URL) {
return `${process.env.PUBLIC_URL}/${basePath}`;
} else {
return basePath;
}
};
return ( return (
<LanguageComponent <LanguageComponent
languageIsSet={i18n.language ? true : false} languageIsSet={i18n.language ? true : false}
@ -51,7 +46,7 @@ const LanguageContainer = () => {
onLanguageChange={handleLanguageChange} onLanguageChange={handleLanguageChange}
onClose={handleClose} onClose={handleClose}
flag={flag} flag={flag}
getFlagsPath={getFlagsPath} flagsPath={flagsPath}
/> />
); );
}; };

View File

@ -1,14 +0,0 @@
import React from "react";
import LanguageContainer from "./language/LanguageContainer";
import ThemeSettings from "./ThemeSettings";
const SettingsContainer = () => {
return (
<>
<LanguageContainer />
<ThemeSettings />
</>
);
};
export default SettingsContainer;

View File

@ -1,23 +0,0 @@
import React from "react";
import { useApplicationTheme } from "../../../providers/ThemeProvider";
import { Switch } from "@material-ui/core";
const ThemeSettings = () => {
const { isDark, onDarkModeChanged } = useApplicationTheme();
const handleChange = event => {
const { checked } = event.target;
onDarkModeChanged(checked);
};
return (
<Switch
checked={isDark}
onChange={handleChange}
color="primary"
name="app-theme-switch"
/>
);
};
export default ThemeSettings;

View File

@ -1,42 +0,0 @@
import React from "react";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
import { Menu, MenuItem } from "@material-ui/core";
const LanguageMenu = ({ anchorEl, onLanguageChange, onClose }) => {
const { t } = useTranslation();
const open = Boolean(anchorEl);
return (
<Menu
id="language-menu"
anchorEl={anchorEl}
anchorOrigin={{
vertical: "top",
horizontal: "right"
}}
keepMounted
transformOrigin={{
vertical: "top",
horizontal: "right"
}}
open={open}
onClose={onClose}
>
<MenuItem onClick={onLanguageChange("ro")}>
{t("Language.Romanian")}
</MenuItem>
<MenuItem onClick={onLanguageChange("en")}>
{t("Language.English")}
</MenuItem>
</Menu>
);
};
LanguageMenu.propTypes = {
anchorEl: PropTypes.object,
onLanguageChange: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired
};
export default LanguageMenu;

View File

@ -0,0 +1,13 @@
import React from "react";
const NotificationsContainer = () => {
return (
<div>
Enable/Disable email notifications (for each one separately - when
starting the machine, when stopping) You can go even further and have an
advanced site where you can configure each individual machine.
</div>
);
};
export default NotificationsContainer;

View File

@ -0,0 +1,7 @@
import React from "react";
const MainServicesContainer = () => {
return <div>MainServices</div>;
};
export default MainServicesContainer;

View File

@ -0,0 +1,49 @@
import React, { useState, useMemo } from "react";
import CategoryIcon from "@material-ui/icons/Category";
import GrainIcon from "@material-ui/icons/Grain";
import { useTranslation } from "react-i18next";
import PageTitle from "../../components/common/PageTitle";
import NavigationButtons from "../../components/common/NavigationButtons";
import MainServicesContainer from "./MainServicesContainer";
import AgentsContainer from "./agents/AgentsContainer";
const NavigationTabs = {
MAIN_SERVICES: "System.Navigation.MainServices",
AGENTS: "System.Navigation.Agents"
};
const tabs = [
{
code: NavigationTabs.MAIN_SERVICES,
icon: CategoryIcon
},
{
code: NavigationTabs.AGENTS,
icon: GrainIcon
}
];
const SystemContainer = () => {
const [tab, setTab] = useState(NavigationTabs.MAIN_SERVICES);
const { t } = useTranslation();
const navigationTabs = useMemo(
() => tabs.map(z => ({ ...z, tooltip: t(z.code) })),
[t]
);
return (
<>
<PageTitle
text={t(tab)}
navigation={
<NavigationButtons tabs={navigationTabs} onTabChange={setTab} />
}
/>
{tab === NavigationTabs.MAIN_SERVICES && <MainServicesContainer />}
{tab === NavigationTabs.AGENTS && <AgentsContainer />}
</>
);
};
export default SystemContainer;

View File

@ -0,0 +1,7 @@
import React from "react";
const AgentsContainer = () => {
return <div>Agents</div>;
};
export default AgentsContainer;

View File

@ -0,0 +1,110 @@
import React from "react";
import PropTypes from "prop-types";
import {
Grid,
List,
ListItem,
ListItemText,
ListItemIcon,
Link,
IconButton,
Tooltip
} from "@material-ui/core";
import UserProfilePicture from "./UserProfilePicture";
import BusinessCenterIcon from "@material-ui/icons/BusinessCenter";
import { FileCopyOutlined } from "@material-ui/icons";
import EmailIcon from "@material-ui/icons/Email";
import { useToast } from "../../../../hooks";
import { useTranslation } from "react-i18next";
import { makeStyles } from "@material-ui/core/styles";
import styles from "../styles";
const useStyles = makeStyles(styles);
const UserProfileCardContent = ({ userData }) => {
const { email, profilePictureUrl } = userData;
const { t } = useTranslation();
const { info } = useToast();
const classes = useStyles();
const handleCopyToClipboard = url => () => {
navigator.clipboard.writeText(url);
info(t("Generic.CopiedToClipboard"));
};
const handleEmailSending = event => {
window.location.href = `mailto:${email}`;
event.preventDefault();
};
const userName = `${userData.firstName} ${userData.lastName}`;
return (
<div className={classes.panel}>
<UserProfilePicture userData={userData} />
<Grid container spacing={2}>
<Grid item xs={12} sm={6}>
<List>
<ListItem dense>
<ListItemIcon>
<BusinessCenterIcon />
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("User.Profile.OpenPortfolio")}>
<Link href="https://lab.code-rove.com/tsp/" target="_blank">
{userName}
</Link>
</Tooltip>
}
/>
</ListItem>
<ListItem dense>
<ListItemIcon>
<EmailIcon />
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("Generic.SendEmail")}>
<Link href="#" onClick={handleEmailSending}>
{email}
</Link>
</Tooltip>
}
/>
</ListItem>
{profilePictureUrl && (
<ListItem dense>
<ListItemIcon>
<Tooltip title={t("Generic.Copy")}>
<IconButton
size="small"
onClick={handleCopyToClipboard(profilePictureUrl)}
>
<FileCopyOutlined />
</IconButton>
</Tooltip>
</ListItemIcon>
<ListItemText
primary={
<Tooltip title={t("Generic.OpenInNewTab")}>
<Link href={profilePictureUrl} target="_blank">
{profilePictureUrl}
</Link>
</Tooltip>
}
/>
</ListItem>
)}
</List>
</Grid>
</Grid>
</div>
);
};
UserProfileCardContent.propTypes = {
userData: PropTypes.object.isRequired
};
export default UserProfileCardContent;

View File

@ -0,0 +1,47 @@
import React, { useMemo } from "react";
import { useTranslation } from "react-i18next";
import { Card, CardHeader, CardContent } from "@material-ui/core";
import { useTuitioToken } from "@flare/tuitio-client-react";
import { camelizeKeys } from "../../../../utils/camelizeKeys";
import PageTitle from "../../../../components/common/PageTitle";
import UserProfileCardContent from "./UserProfileCardContent";
const UserProfileContainer = () => {
const { t } = useTranslation();
const { token } = useTuitioToken();
const decodedToken = useMemo(() => atob(token), [token]);
const userData = useMemo(
() => camelizeKeys(JSON.parse(decodedToken)),
[decodedToken]
);
console.log("userData", userData);
const userLoginDate = useMemo(
() =>
t("DATE_FORMAT", {
date: { value: userData.createdAt, format: "DD-MM-YYYY HH:mm:ss" }
}),
[t, userData.createdAt]
);
const userDescription = t("User.Profile.Description", {
userName: `${userData.firstName} ${userData.lastName}`,
loginDate: userLoginDate
});
return (
<>
<PageTitle
text={t("User.Profile.Hello", { userName: userData.firstName })}
/>
<Card>
<CardHeader title={userData.userName} subheader={userDescription} />
<CardContent>
<UserProfileCardContent userData={userData} />
</CardContent>
</Card>
</>
);
};
export default UserProfileContainer;

View File

@ -0,0 +1,21 @@
import React from "react";
import PropTypes from "prop-types";
import { makeStyles } from "@material-ui/core/styles";
import style from "../styles";
import Avatar from "@material-ui/core/Avatar";
import DefaultUserProfilePicture from "../../../../assets/images/DefaultUserProfilePicture.png";
const useStyles = makeStyles(style);
const UserProfilePicture = ({ userData }) => {
const classes = useStyles();
const { profilePictureUrl } = userData;
const url = profilePictureUrl ?? DefaultUserProfilePicture;
return <Avatar src={url} alt="..." className={classes.profilePicture} />;
};
UserProfilePicture.propTypes = {
userData: PropTypes.object.isRequired
};
export default UserProfilePicture;

View File

@ -0,0 +1,19 @@
const style = theme => {
return {
panel: {
display: "flex",
flexDirection: "row",
"@media (max-width: 600px)": {
flexDirection: "column" // change direction for small screens
}
},
profilePicture: {
margin: "auto",
display: "block",
width: theme.spacing(25),
height: theme.spacing(25)
}
};
};
export default style;

View File

@ -4,7 +4,6 @@ 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 App from "./components/App";
import { TuitioProvider } from "@flare/tuitio-client-react"; import { TuitioProvider } from "@flare/tuitio-client-react";
import ApplicationStateProvider from "./providers/ApplicationStateProvider";
import ToastProvider from "./providers/ToastProvider"; import ToastProvider from "./providers/ToastProvider";
import SensitiveInfoProvider from "./providers/SensitiveInfoProvider"; import SensitiveInfoProvider from "./providers/SensitiveInfoProvider";
import "./utils/i18n"; import "./utils/i18n";
@ -14,13 +13,11 @@ ReactDOM.render(
<ThemeProvider> <ThemeProvider>
<CssBaseline /> <CssBaseline />
<SensitiveInfoProvider> <SensitiveInfoProvider>
<ApplicationStateProvider>
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={<div>Loading...</div>}>
<ToastProvider> <ToastProvider>
<App /> <App />
</ToastProvider> </ToastProvider>
</Suspense> </Suspense>
</ApplicationStateProvider>
</SensitiveInfoProvider> </SensitiveInfoProvider>
</ThemeProvider> </ThemeProvider>
</TuitioProvider>, </TuitioProvider>,

View File

@ -1,33 +0,0 @@
import React, { useReducer, useMemo } from "react";
import PropTypes from "prop-types";
import {
ApplicationStateContext,
ApplicationDispatchContext
} from "../state/contexts";
import {
reducer,
dispatchActions as reducerDispatchActions
} from "../state/reducer";
import { initialState } from "../state/initialState";
const ApplicationStateProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const dispatchActions = useMemo(
() => reducerDispatchActions(dispatch),
[dispatch]
);
return (
<ApplicationStateContext.Provider value={state}>
<ApplicationDispatchContext.Provider value={dispatchActions}>
{children}
</ApplicationDispatchContext.Provider>
</ApplicationStateContext.Provider>
);
};
ApplicationStateProvider.propTypes = {
children: PropTypes.node.isRequired
};
export default ApplicationStateProvider;

View File

@ -34,12 +34,12 @@ const useSensitiveInfo = () => {
const mask = text => { const mask = text => {
if (!enabled) return text; if (!enabled) return text;
return obfuscate(text, "#"); return obfuscate(text, "");
}; };
const maskElements = list => { const maskElements = list => {
if (!enabled) return list; if (!enabled) return list;
const maskedList = list.map(z => obfuscate(z, "#")); const maskedList = list.map(z => obfuscate(z, "◻️"));
return maskedList; return maskedList;
}; };

View File

@ -1,4 +0,0 @@
import React from "react";
export const ApplicationStateContext = React.createContext();
export const ApplicationDispatchContext = React.createContext();

View File

@ -1,5 +1,5 @@
const primary = "#00695C"; const primary = "#00695C";
const secondary = "#FF5C93"; const secondary = "#DC7633";
const warning = "#ff9800"; const warning = "#ff9800";
const success = "#4caf50"; const success = "#4caf50";
const info = "#2196f3"; const info = "#2196f3";
@ -10,8 +10,8 @@ const defaultTheme = {
main: primary main: primary
}, },
secondary: { secondary: {
main: secondary, main: secondary
contrastText: "#ffcc00" // contrastText: "#ffcc00"
}, },
warning: { warning: {
main: warning main: warning

31
src/utils/camelizeKeys.js Normal file
View File

@ -0,0 +1,31 @@
function camelizeKeys(o) {
var newO, origKey, newKey, value;
if (o instanceof Array) {
return o.map(function (value) {
if (typeof value === "object") {
value = camelizeKeys(value);
}
return value;
});
} else {
newO = {};
for (origKey in o) {
if (Object.prototype.hasOwnProperty.call(o, origKey)) {
newKey = (
origKey.charAt(0).toLowerCase() + origKey.slice(1) || origKey
).toString();
value = o[origKey];
if (
value instanceof Array ||
(value !== null && value.constructor === Object)
) {
value = camelizeKeys(value);
}
newO[newKey] = value;
}
}
}
return newO;
}
export { camelizeKeys };

3
src/utils/index.js Normal file
View File

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

7
src/utils/random.js Normal file
View File

@ -0,0 +1,7 @@
const getRandomElement = array => {
const randomIndex = Math.floor(Math.random() * array.length);
const element = array[randomIndex];
return element;
};
export { getRandomElement };