mirror of
https://dev.azure.com/tstanciu94/NetworkResurrector/_git/NetworkResurrector_Frontend
synced 2023-05-06 14:40:17 +03:00
Compare commits
27 Commits
7e1a64c27b
...
52cd8f53f5
Author | SHA1 | Date | |
---|---|---|---|
52cd8f53f5 | |||
|
f08ba0221b | ||
|
58e5f61a97 | ||
|
46581b33c9 | ||
|
d8bad156dd | ||
|
eec6d9ceb2 | ||
|
e6d115a21e | ||
|
99b4929b2a | ||
|
853cc9c9ad | ||
|
5ffed1c995 | ||
|
d3f359c6dd | ||
|
c61df6b59b | ||
|
484b3d0ab2 | ||
|
6d1c2df40b | ||
|
78f7b2db2a | ||
|
a166e1c9e3 | ||
|
1ad78b3ef1 | ||
|
4195456227 | ||
|
64684674ba | ||
|
6ad7abf3d1 | ||
|
7115649f12 | ||
|
94f2138bc7 | ||
|
3cc5d8f6f3 | ||
|
234778cc43 | ||
|
6af31bdcca | ||
|
a49a289b2c | ||
|
b617d59b69 |
8
.env
8
.env
@ -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
|
||||||
|
@ -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
14
package-lock.json
generated
@ -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",
|
||||||
|
@ -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",
|
||||||
|
@ -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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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}}"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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,
|
||||||
|
BIN
src/assets/images/DefaultUserProfilePicture.png
Normal file
BIN
src/assets/images/DefaultUserProfilePicture.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 89 KiB |
42
src/components/common/DataLabel.js
Normal file
42
src/components/common/DataLabel.js
Normal 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;
|
45
src/components/common/NavigationButtons.js
Normal file
45
src/components/common/NavigationButtons.js
Normal 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;
|
@ -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}
|
||||||
{text}
|
<div className={classes.title}>
|
||||||
</Typography>
|
<Typography className={classes.titleText} variant="h3" size="sm">
|
||||||
|
{text}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
{toolBar && toolBar}
|
||||||
</div>
|
</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;
|
||||||
|
3
src/components/common/index.js
Normal file
3
src/components/common/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import DataLabel from "./DataLabel";
|
||||||
|
|
||||||
|
export { DataLabel };
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -62,6 +62,9 @@ const styles = theme => ({
|
|||||||
content: {
|
content: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
padding: theme.spacing(2)
|
padding: theme.spacing(2)
|
||||||
|
},
|
||||||
|
menuItemIcon: {
|
||||||
|
minWidth: "26px"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
58
src/features/about/AboutContainer.js
Normal file
58
src/features/about/AboutContainer.js
Normal 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;
|
@ -1,7 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
const AboutContainer = () => {
|
|
||||||
return <div>TEST</div>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AboutContainer;
|
|
27
src/features/about/releaseNotes/ReleaseNote.js
Normal file
27
src/features/about/releaseNotes/ReleaseNote.js
Normal 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;
|
37
src/features/about/releaseNotes/ReleaseNoteSummary.js
Normal file
37
src/features/about/releaseNotes/ReleaseNoteSummary.js
Normal 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;
|
40
src/features/about/releaseNotes/ReleaseNotesContainer.js
Normal file
40
src/features/about/releaseNotes/ReleaseNotesContainer.js
Normal 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;
|
64
src/features/about/releaseNotes/ReleaseNotesList.js
Normal file
64
src/features/about/releaseNotes/ReleaseNotesList.js
Normal 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;
|
103
src/features/about/system/AboutSystemComponent.js
Normal file
103
src/features/about/system/AboutSystemComponent.js
Normal 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;
|
35
src/features/about/system/AboutSystemContainer.js
Normal file
35
src/features/about/system/AboutSystemContainer.js
Normal 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;
|
154
src/features/about/system/SystemVersionComponent.js
Normal file
154
src/features/about/system/SystemVersionComponent.js
Normal 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;
|
22
src/features/about/system/SystemVersionContainer.js
Normal file
22
src/features/about/system/SystemVersionContainer.js
Normal 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;
|
117
src/features/about/timeline/TimelineComponent.js
Normal file
117
src/features/about/timeline/TimelineComponent.js
Normal 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;
|
@ -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;
|
|
@ -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 (
|
||||||
<>
|
<LoginCard
|
||||||
{tokenIsValid ? (
|
credentials={credentials}
|
||||||
<LoggedInComponent
|
onChange={handleChange}
|
||||||
credentials={credentials}
|
onLogin={handleLogin}
|
||||||
onChange={handleChange}
|
/>
|
||||||
onLogin={handleLogin}
|
|
||||||
onLogout={handleLogout}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<LoginCard
|
|
||||||
credentials={credentials}
|
|
||||||
onChange={handleChange}
|
|
||||||
onLogin={handleLogin}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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;
|
|
110
src/features/machines/components/MachineAccordion.js
Normal file
110
src/features/machines/components/MachineAccordion.js
Normal 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;
|
@ -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
|
<>
|
||||||
machine={machine}
|
{viewMode === ViewModes.TABLE && (
|
||||||
actions={actions}
|
<MachineTableRow
|
||||||
logs={logs}
|
machine={machine}
|
||||||
addLog={addLog}
|
actions={actions}
|
||||||
secondaryActionsMenuProps={secondaryActionsMenuProps}
|
logs={logs}
|
||||||
/>
|
addLog={addLog}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{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;
|
||||||
|
70
src/features/machines/components/MachineTableRow.js
Normal file
70
src/features/machines/components/MachineTableRow.js
Normal 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;
|
@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -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;
|
|
80
src/features/machines/components/MachinesListComponent.js
Normal file
80
src/features/machines/components/MachinesListComponent.js
Normal 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;
|
80
src/features/machines/components/ViewModeSelection.js
Normal file
80
src/features/machines/components/ViewModeSelection.js
Normal 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;
|
45
src/features/machines/components/common/ActionButton.js
Normal file
45
src/features/machines/components/common/ActionButton.js
Normal 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;
|
85
src/features/machines/components/common/ActionsGroup.js
Normal file
85
src/features/machines/components/common/ActionsGroup.js
Normal 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;
|
@ -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}
|
@ -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>
|
@ -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>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
27
src/features/network/state/NetworkStateProvider.js
Normal file
27
src/features/network/state/NetworkStateProvider.js
Normal 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;
|
4
src/features/network/state/contexts.js
Normal file
4
src/features/network/state/contexts.js
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
export const NetworkStateContext = React.createContext();
|
||||||
|
export const NetworkDispatchContext = React.createContext();
|
@ -1,7 +0,0 @@
|
|||||||
const styles = () => ({
|
|
||||||
root: {
|
|
||||||
margin: "15px"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default styles;
|
|
@ -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;
|
|
49
src/features/settings/SettingsContainer.js
Normal file
49
src/features/settings/SettingsContainer.js
Normal 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;
|
61
src/features/settings/appearance/AppearanceComponent.js
Normal file
61
src/features/settings/appearance/AppearanceComponent.js
Normal 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;
|
8
src/features/settings/appearance/AppearanceContainer.js
Normal file
8
src/features/settings/appearance/AppearanceContainer.js
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import React from "react";
|
||||||
|
import AppearanceComponent from "./AppearanceComponent";
|
||||||
|
|
||||||
|
const AppearanceContainer = () => {
|
||||||
|
return <AppearanceComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppearanceContainer;
|
@ -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;
|
@ -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}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
7
src/features/system/MainServicesContainer.js
Normal file
7
src/features/system/MainServicesContainer.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const MainServicesContainer = () => {
|
||||||
|
return <div>MainServices</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MainServicesContainer;
|
49
src/features/system/SystemContainer.js
Normal file
49
src/features/system/SystemContainer.js
Normal 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;
|
7
src/features/system/agents/AgentsContainer.js
Normal file
7
src/features/system/agents/AgentsContainer.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import React from "react";
|
||||||
|
|
||||||
|
const AgentsContainer = () => {
|
||||||
|
return <div>Agents</div>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AgentsContainer;
|
110
src/features/user/profile/components/UserProfileCardContent.js
Normal file
110
src/features/user/profile/components/UserProfileCardContent.js
Normal 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;
|
47
src/features/user/profile/components/UserProfileContainer.js
Normal file
47
src/features/user/profile/components/UserProfileContainer.js
Normal 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;
|
21
src/features/user/profile/components/UserProfilePicture.js
Normal file
21
src/features/user/profile/components/UserProfilePicture.js
Normal 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;
|
19
src/features/user/profile/styles.js
Normal file
19
src/features/user/profile/styles.js
Normal 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;
|
13
src/index.js
13
src/index.js
@ -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>,
|
||||||
|
@ -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;
|
|
@ -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;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
|
|
||||||
export const ApplicationStateContext = React.createContext();
|
|
||||||
export const ApplicationDispatchContext = React.createContext();
|
|
@ -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
31
src/utils/camelizeKeys.js
Normal 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
3
src/utils/index.js
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import { getRandomElement } from "./random";
|
||||||
|
|
||||||
|
export { getRandomElement };
|
7
src/utils/random.js
Normal file
7
src/utils/random.js
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
const getRandomElement = array => {
|
||||||
|
const randomIndex = Math.floor(Math.random() * array.length);
|
||||||
|
const element = array[randomIndex];
|
||||||
|
return element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export { getRandomElement };
|
Loading…
x
Reference in New Issue
Block a user