Compare commits
No commits in common. "08f1360f21915d2a6a334b3f1a269339ab3485e1" and "4b1f168beff1168db7142b104ae926b6812f6000" have entirely different histories.
08f1360f21
...
4b1f168bef
|
@ -1,7 +1,7 @@
|
||||||
<Project>
|
<Project>
|
||||||
<Import Project="dependencies.props" />
|
<Import Project="dependencies.props" />
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>1.2.7</Version>
|
<Version>1.2.5</Version>
|
||||||
<Authors>Tudor Stanciu</Authors>
|
<Authors>Tudor Stanciu</Authors>
|
||||||
<Company>STA</Company>
|
<Company>STA</Company>
|
||||||
<PackageTags>NetworkResurrector</PackageTags>
|
<PackageTags>NetworkResurrector</PackageTags>
|
|
@ -188,17 +188,6 @@
|
||||||
<Content>
|
<Content>
|
||||||
General improvements
|
General improvements
|
||||||
• Added environment variables support for frontend.
|
• Added environment variables support for frontend.
|
||||||
• From now on, there is no hardcoded URL in the source code of the application.
|
|
||||||
</Content>
|
|
||||||
</Note>
|
|
||||||
<Note>
|
|
||||||
<Version>1.2.7</Version>
|
|
||||||
<Date>2023-05-07 00:52</Date>
|
|
||||||
<Content>
|
|
||||||
Cache reset support
|
|
||||||
• A new permission called "System administration" has been added.
|
|
||||||
• The cache can now be reset from UI by a user who has "System administration" permission.
|
|
||||||
• Updated menu component to permanently display the selected item.
|
|
||||||
</Content>
|
</Content>
|
||||||
</Note>
|
</Note>
|
||||||
</ReleaseNotes>
|
</ReleaseNotes>
|
|
@ -1,28 +0,0 @@
|
||||||
using MediatR;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Netmash.Extensions.Caching.Services;
|
|
||||||
using NetworkResurrector.Api.PublishedLanguage.Commands;
|
|
||||||
using NetworkResurrector.Api.PublishedLanguage.Dto;
|
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace NetworkResurrector.Api.Application.CommandHandlers
|
|
||||||
{
|
|
||||||
internal class ResetCacheHandler : IRequestHandler<ResetCache, CommandResult>
|
|
||||||
{
|
|
||||||
private readonly ILogger<CancelMachineHandler> _logger;
|
|
||||||
private readonly ICacheService _cache;
|
|
||||||
|
|
||||||
public ResetCacheHandler(ILogger<CancelMachineHandler> logger, ICacheService cache)
|
|
||||||
{
|
|
||||||
_logger=logger;
|
|
||||||
_cache=cache;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<CommandResult> Handle(ResetCache request, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
_cache.Reset();
|
|
||||||
return await Task.FromResult(new CommandResult());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,5 +0,0 @@
|
||||||
if not exists (select top 1 1 from Permission where PermissionCode = 'SYSTEM_ADMINISTRATION')
|
|
||||||
begin
|
|
||||||
insert into Permission(PermissionCode, PermissionName, PermissionDescription)
|
|
||||||
values ('SYSTEM_ADMINISTRATION', 'System administration', 'The user with this permission can perform system administration operations such as resetting the cache.')
|
|
||||||
end
|
|
|
@ -1,7 +0,0 @@
|
||||||
using MediatR;
|
|
||||||
using NetworkResurrector.Api.PublishedLanguage.Dto;
|
|
||||||
|
|
||||||
namespace NetworkResurrector.Api.PublishedLanguage.Commands
|
|
||||||
{
|
|
||||||
public record ResetCache : IRequest<CommandResult> { }
|
|
||||||
}
|
|
|
@ -1,4 +0,0 @@
|
||||||
namespace NetworkResurrector.Api.PublishedLanguage.Dto
|
|
||||||
{
|
|
||||||
public record CommandResult();
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
using NetworkResurrector.Api.Domain.Constants;
|
|
||||||
|
|
||||||
namespace NetworkResurrector.Api.Authorization.Requirements
|
|
||||||
{
|
|
||||||
public class SystemAdministrationRequirement : IPermissionsBasedAuthorizationRequirement
|
|
||||||
{
|
|
||||||
public string[] AllRequired => new string[] { PermissionCodes.SYSTEM_ADMINISTRATION };
|
|
||||||
public string[] OneOf => null;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,7 +0,0 @@
|
||||||
.git
|
|
||||||
node_modules
|
|
||||||
build
|
|
||||||
__mocks__
|
|
||||||
.vscode
|
|
||||||
helm
|
|
||||||
private
|
|
|
@ -1,8 +0,0 @@
|
||||||
REACT_APP_TUITIO_URL=http://#######
|
|
||||||
REACT_APP_NETWORK_RESURRECTOR_API_URL=http://#######
|
|
||||||
|
|
||||||
#600000 milliseconds = 10 minutes
|
|
||||||
REACT_APP_MACHINE_PING_INTERVAL=600000
|
|
||||||
|
|
||||||
#300000 milliseconds = 5 minutes
|
|
||||||
REACT_APP_MACHINE_STARTING_TIME=300000
|
|
|
@ -1,9 +0,0 @@
|
||||||
PUBLIC_URL=
|
|
||||||
REACT_APP_TUITIO_URL=https://#######
|
|
||||||
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://#######
|
|
||||||
|
|
||||||
#900000 milliseconds = 15 minutes
|
|
||||||
REACT_APP_MACHINE_PING_INTERVAL=900000
|
|
||||||
|
|
||||||
#300000 milliseconds = 5 minutes
|
|
||||||
REACT_APP_MACHINE_STARTING_TIME=300000
|
|
|
@ -1,38 +0,0 @@
|
||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es6": true,
|
|
||||||
"node": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"eslint:recommended",
|
|
||||||
"plugin:react/recommended",
|
|
||||||
"plugin:jest/recommended"
|
|
||||||
],
|
|
||||||
"parser": "@babel/eslint-parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"requireConfigFile": false,
|
|
||||||
"babelOptions": {
|
|
||||||
"presets": ["@babel/preset-react"]
|
|
||||||
},
|
|
||||||
"ecmaFeatures": {
|
|
||||||
"experimentalObjectRestSpread": true,
|
|
||||||
"jsx": true
|
|
||||||
},
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": ["react", "react-hooks", "jest"],
|
|
||||||
"ignorePatterns": ["**/public"],
|
|
||||||
"rules": {
|
|
||||||
"indent": 0,
|
|
||||||
"linebreak-style": 0,
|
|
||||||
"quotes": 0,
|
|
||||||
"semi": 0,
|
|
||||||
"no-console": 0,
|
|
||||||
"no-debugger": "warn",
|
|
||||||
"react/display-name": "off",
|
|
||||||
"react-hooks/rules-of-hooks": "error",
|
|
||||||
"react-hooks/exhaustive-deps": "warn",
|
|
||||||
"no-unused-vars": [1, { "args": "after-used", "argsIgnorePattern": "^_" }]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +0,0 @@
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
|
||||||
|
|
||||||
# dependencies
|
|
||||||
/node_modules
|
|
||||||
/.pnp
|
|
||||||
.pnp.js
|
|
||||||
|
|
||||||
# testing
|
|
||||||
/coverage
|
|
||||||
|
|
||||||
# production
|
|
||||||
/build
|
|
||||||
|
|
||||||
# misc
|
|
||||||
.DS_Store
|
|
||||||
.env.local
|
|
||||||
.env.development
|
|
||||||
.env.development.local
|
|
||||||
.env.test.local
|
|
||||||
.env.production.local
|
|
||||||
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
|
|
||||||
.eslintcache
|
|
||||||
debug.log
|
|
||||||
|
|
||||||
build.sh
|
|
||||||
buildx.sh
|
|
|
@ -1 +0,0 @@
|
||||||
@flare:registry=https://lab.code-rove.com/public-node-registry
|
|
|
@ -1,7 +0,0 @@
|
||||||
{
|
|
||||||
"trailingComma": "none",
|
|
||||||
"tabWidth": 2,
|
|
||||||
"semi": true,
|
|
||||||
"singleQuote": false,
|
|
||||||
"arrowParens": "avoid"
|
|
||||||
}
|
|
|
@ -1,22 +0,0 @@
|
||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2020 Tudor Stanciu
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
|
||||||
a copy of this software and associated documentation files (the
|
|
||||||
"Software"), to deal in the Software without restriction, including
|
|
||||||
without limitation the rights to use, copy, modify, merge, publish,
|
|
||||||
distribute, sublicense, and/or sell copies of the Software, and to
|
|
||||||
permit persons to whom the Software is furnished to do so, subject to
|
|
||||||
the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be
|
|
||||||
included in all copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
||||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
||||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
||||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
||||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
||||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
||||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
|
@ -1,87 +0,0 @@
|
||||||
# Network resurrector
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
## Main components
|
|
||||||
|
|
||||||
### Frontend
|
|
||||||
|
|
||||||
- The [frontend](https://lab.code-rove.com/gitea/tudor.stanciu/network-resurrector-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
|
|
||||||
|
|
||||||
- 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
|
|
||||||
|
|
||||||
- The server component is a .NET 6 service specialized in executing 'WakeOnLAN', 'Ping' and 'Shutdown' actions for the machines in its network.
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
- The need for the agent appeared after I realized that the server cannot perform any action on a machine, being outside of it.
|
|
||||||
- The agent solves this problem because it is installed directly on the targeted machine. Later, the API component can delegate the resolution of an action directly to the agent.
|
|
||||||
- Most of the time, the execution flow of an action is realized in the following way:
|
|
||||||
- The user initiates an action, such as starting or stopping a machine, by pressing the corresponding button in the user interface.
|
|
||||||
- The frontend component sends the command to the API.
|
|
||||||
- The API checks who is configured as performer for the respective action for the machine and sends the command to it.
|
|
||||||
- The performer of the action (Agent or Server) executes the command and responds on the flow with status.
|
|
||||||
- In most cases, the Server component handles the machine startup action while the Agent component manages the machine shutdown action.
|
|
||||||
- As is probably already obvious, the agent can be installed on as many machines as desired.
|
|
||||||
|
|
||||||
### Tuitio
|
|
||||||
|
|
||||||
- [Tuitio](https://lab.code-rove.com/gitea/tudor.stanciu/tuitio) is my personal identity server. It manages user authentication within the application and authorizes requests made by it. Further information about [Tuitio](https://lab.code-rove.com/gitea/tudor.stanciu/tuitio/src/branch/master/README.md) can be found on its dedicated page.
|
|
||||||
|
|
||||||
All communication between the five main components is done exclusively through HTTP.
|
|
||||||
|
|
||||||
## Secondary components
|
|
||||||
|
|
||||||
- NATS Streaming - Used to publish notifications about executed actions.
|
|
||||||
- Correo - Used to send by email the notifications generated by the system.
|
|
||||||
- Seq - Used to collect and view system logs.
|
|
||||||
|
|
||||||
## Notification system
|
|
||||||
|
|
||||||
The notification system is focused on system actions (Wake, Shutdown, etc), and the configuration of notifications is done in structures of the following form:
|
|
||||||
|
|
||||||
```
|
|
||||||
{
|
|
||||||
"To": [ "user@homelab.com" ],
|
|
||||||
"Subject": "Network resurrector: Machine {MACHINE_NAME} has been waked. Status: {ACTION_STATUS}",
|
|
||||||
"Body": "Hello,{NEWLINE:2}Network resurrector processed a command to start machine {MACHINE_FULLNAME} with IP {MACHINE_IP} at {SYSTEM_DATETIME}.{NEWLINE}The performer who was delegated for this action was {ACTION_PERFORMER}.{NEWLINE}Action status: {ACTION_STATUS}{NEWLINE:2}Have a nice day,{NEWLINE}Network resurrector notification system."
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The texts can be written at the user's free choice, and the following placeholders can be used in their composition: `{MACHINE_NAME}`, `{MACHINE_FULLNAME}`, `{MACHINE_IP}`, `{ACTION_STATUS}`, `{ACTION_PERFORMER}`, `{SYSTEM_DATETIME}`, `{ERROR_MESSGE}`, `{NEWLINE}`, `{NEWLINE:2}`
|
|
||||||
|
|
||||||
`{NEWLINE:x}` is a dynamic placeholder. Any number can be written in place of the 'x' character and the notification system will add that many new lines.
|
|
||||||
|
|
||||||
## Database
|
|
||||||
|
|
||||||
Currently, the database server supported by the system is only Microsoft SQL Server. In the following versions, the system will also be compatible with PostgreSQL and SQLite.
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
The logging functionality is managed with Serilog, and its configuration is done in the `appsettings.json` file. In addition to its standard configuration, Network resurrector also has a preconfigured area where two destinations for logs are available: SqlServer database and Seq. Each of the destinations can be activated or not. If logging in the console is sufficient, all additional logging destinations can be disabled.
|
|
||||||
This configuration area is:
|
|
||||||
|
|
||||||
```
|
|
||||||
"Logs": {
|
|
||||||
"SqlServer": {
|
|
||||||
"Enabled": false,
|
|
||||||
"Connection": "Server=<server>;Database=<database>;User Id=<user>;Password=<password>;"
|
|
||||||
},
|
|
||||||
"Seq": {
|
|
||||||
"Enabled": false,
|
|
||||||
"Url": "",
|
|
||||||
"ApiKey": ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## Hosting
|
|
||||||
|
|
||||||
All the components of the system are written in cross-platform technologies, so its host can be any environment.
|
|
|
@ -1,44 +0,0 @@
|
||||||
# BUILD ENVIRONMENT
|
|
||||||
FROM node:14-slim as builder
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
ARG APP_SUBFOLDER=""
|
|
||||||
|
|
||||||
COPY .npmrc .npmrc
|
|
||||||
COPY package*.json ./
|
|
||||||
RUN npm install
|
|
||||||
RUN rm -f .npmrc
|
|
||||||
|
|
||||||
COPY . ./
|
|
||||||
|
|
||||||
# build the react app
|
|
||||||
RUN if [ -z "$APP_SUBFOLDER" ]; then npm run build; else PUBLIC_URL=/${APP_SUBFOLDER}/ npm run build; fi
|
|
||||||
|
|
||||||
# PRODUCTION ENVIRONMENT
|
|
||||||
FROM node:14-slim
|
|
||||||
|
|
||||||
ARG APP_SUBFOLDER=""
|
|
||||||
|
|
||||||
RUN printf '\n\n- Copy application files\n'
|
|
||||||
COPY --from=builder /app/build ./application/${APP_SUBFOLDER}
|
|
||||||
COPY --from=builder /app/build/index.html ./application/
|
|
||||||
COPY --from=builder /app/setenv.js ./application/setenv.js
|
|
||||||
|
|
||||||
#install static server
|
|
||||||
RUN npm install -g serve
|
|
||||||
|
|
||||||
# environment variables
|
|
||||||
ENV AUTHOR="Tudor Stanciu"
|
|
||||||
ENV PUBLIC_URL=/${APP_SUBFOLDER}/
|
|
||||||
ARG APP_VERSION=0.0.0
|
|
||||||
ENV APP_VERSION=${APP_VERSION}
|
|
||||||
|
|
||||||
ARG APP_DATE="-"
|
|
||||||
ENV APP_DATE=${APP_DATE}
|
|
||||||
|
|
||||||
#set workdir to root
|
|
||||||
WORKDIR /
|
|
||||||
|
|
||||||
EXPOSE 80
|
|
||||||
|
|
||||||
CMD ["sh", "-c", "node application/setenv.js && serve -s application -p 80"]
|
|
|
@ -1,6 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"baseUrl": "src"
|
|
||||||
},
|
|
||||||
"include": ["src"]
|
|
||||||
}
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,69 +0,0 @@
|
||||||
{
|
|
||||||
"name": "network-resurrector-frontend",
|
|
||||||
"version": "1.2.7",
|
|
||||||
"description": "Frontend component of Network resurrector system",
|
|
||||||
"author": {
|
|
||||||
"name": "Tudor Stanciu",
|
|
||||||
"email": "tudor.stanciu94@gmail.com",
|
|
||||||
"url": "https://lab.code-rove.com/tsp"
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://lab.code-rove.com/gitea/tudor.stanciu/network-resurrector-frontend"
|
|
||||||
},
|
|
||||||
"private": true,
|
|
||||||
"dependencies": {
|
|
||||||
"@flare/js-utils": "^1.1.0",
|
|
||||||
"@flare/tuitio-client-react": "^1.2.6",
|
|
||||||
"@material-ui/core": "^4.11.2",
|
|
||||||
"@material-ui/icons": "^4.11.2",
|
|
||||||
"@material-ui/lab": "^4.0.0-alpha.61",
|
|
||||||
"axios": "^1.3.4",
|
|
||||||
"i18next": "^19.4.4",
|
|
||||||
"i18next-browser-languagedetector": "^4.1.1",
|
|
||||||
"i18next-http-backend": "^1.4.0",
|
|
||||||
"moment": "^2.29.3",
|
|
||||||
"react": "^17.0.1",
|
|
||||||
"react-dom": "^17.0.1",
|
|
||||||
"react-flags": "^0.1.18",
|
|
||||||
"react-i18next": "^11.4.0",
|
|
||||||
"react-lazylog": "^4.5.3",
|
|
||||||
"react-router-dom": "^5.2.0",
|
|
||||||
"react-scripts": "4.0.1",
|
|
||||||
"react-toastify": "^6.2.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/eslint-parser": "^7.16.5",
|
|
||||||
"@testing-library/jest-dom": "^5.11.6",
|
|
||||||
"@testing-library/react": "^11.2.2",
|
|
||||||
"@testing-library/user-event": "^12.5.0",
|
|
||||||
"prettier": "^2.5.1"
|
|
||||||
},
|
|
||||||
"scripts": {
|
|
||||||
"start": "react-scripts start",
|
|
||||||
"build": "react-scripts build",
|
|
||||||
"test": "react-scripts test",
|
|
||||||
"eject": "react-scripts eject"
|
|
||||||
},
|
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
|
||||||
"production": [
|
|
||||||
">0.3%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
],
|
|
||||||
"development": [
|
|
||||||
"last 1 chrome version",
|
|
||||||
"last 1 firefox version",
|
|
||||||
"last 1 safari version",
|
|
||||||
">0.3%",
|
|
||||||
"not dead",
|
|
||||||
"not op_mini all"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,13 +0,0 @@
|
||||||
REACT v4:
|
|
||||||
https://v4.mui.com/getting-started/installation/
|
|
||||||
https://v4.mui.com/components/material-icons/
|
|
||||||
|
|
||||||
Theming:
|
|
||||||
https://v4.mui.com/customization/palette/ (Dark theme)
|
|
||||||
|
|
||||||
Add in settings:
|
|
||||||
- ping interval
|
|
||||||
- notifications
|
|
||||||
- test notification mechanism
|
|
||||||
- permissions
|
|
||||||
- permissions hierarchy
|
|
|
@ -1,2 +0,0 @@
|
||||||
// In this file will be injected the environment variables that will overwrite the application configurations.
|
|
||||||
window.env = {};
|
|
Binary file not shown.
Before Width: | Height: | Size: 46 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 937 B |
|
@ -1,52 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8" />
|
|
||||||
<link rel="icon" href="%PUBLIC_URL%/favicon.jpg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
||||||
<meta name="theme-color" content="#000000" />
|
|
||||||
<meta
|
|
||||||
name="description"
|
|
||||||
content="Web site created using create-react-app"
|
|
||||||
/>
|
|
||||||
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
|
|
||||||
<!--
|
|
||||||
manifest.json provides metadata used when your web app is installed on a
|
|
||||||
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
|
|
||||||
-->
|
|
||||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
|
||||||
<!--
|
|
||||||
Notice the use of %PUBLIC_URL% in the tags above.
|
|
||||||
It will be replaced with the URL of the `public` folder during the build.
|
|
||||||
Only files inside the `public` folder can be referenced from the HTML.
|
|
||||||
|
|
||||||
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
|
|
||||||
work correctly both with client-side routing and a non-root public URL.
|
|
||||||
Learn how to configure a non-root public URL by running `npm run build`.
|
|
||||||
-->
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap"
|
|
||||||
/>
|
|
||||||
<link
|
|
||||||
rel="stylesheet"
|
|
||||||
href="https://fonts.googleapis.com/icon?family=Material+Icons"
|
|
||||||
/>
|
|
||||||
<script src="%PUBLIC_URL%/env.js"></script>
|
|
||||||
<title>Network resurrector</title>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
|
||||||
<div id="root"></div>
|
|
||||||
<!--
|
|
||||||
This HTML file is a template.
|
|
||||||
If you open it directly in the browser, you will see an empty page.
|
|
||||||
|
|
||||||
You can add webfonts, meta tags, or analytics to this file.
|
|
||||||
The build step will place the bundled scripts into the <body> tag.
|
|
||||||
|
|
||||||
To begin the development, run `npm start` or `yarn start`.
|
|
||||||
To create a production bundle, use `npm run build` or `yarn build`.
|
|
||||||
-->
|
|
||||||
</body>
|
|
||||||
</html>
|
|
|
@ -1,143 +0,0 @@
|
||||||
{
|
|
||||||
"DATE": "{{date,intlDate}}",
|
|
||||||
"LONG_DATE": "{{date,intlLongDate}}",
|
|
||||||
"DATE_FORMAT": "{{date, format}}",
|
|
||||||
"TIME_FROM_X": "{{date,intlTimeFromX}}",
|
|
||||||
"H_FROM_X": "{{date,intlHoursFromX}}",
|
|
||||||
"H_FROM_M": "{{number,intlHoursFromMinutes}}",
|
|
||||||
"NUMBER": "{{number,intlNumber}}",
|
|
||||||
"DECIMAL": "{{number,intlDecimal}}",
|
|
||||||
"DECIMAL2": "{{number,intlDecimal2}}",
|
|
||||||
"Language": {
|
|
||||||
"English": "English",
|
|
||||||
"Romanian": "Romanian"
|
|
||||||
},
|
|
||||||
"Generic": {
|
|
||||||
"Copy": "Copy",
|
|
||||||
"OpenInNewTab": "Open in new tab",
|
|
||||||
"CopiedToClipboard": "Copied to clipboard",
|
|
||||||
"SendEmail": "Send email"
|
|
||||||
},
|
|
||||||
"Menu": {
|
|
||||||
"Dashboard": "Dashboard",
|
|
||||||
"Machines": "Machines",
|
|
||||||
"System": "System",
|
|
||||||
"Administration": "Administration",
|
|
||||||
"Settings": "Settings",
|
|
||||||
"About": "About"
|
|
||||||
},
|
|
||||||
"ViewModes": {
|
|
||||||
"Table": "Table",
|
|
||||||
"List": "List"
|
|
||||||
},
|
|
||||||
"Login": {
|
|
||||||
"Username": "Username",
|
|
||||||
"Password": "Password",
|
|
||||||
"Label": "Login",
|
|
||||||
"IncorrectCredentials": "Incorrect credentials."
|
|
||||||
},
|
|
||||||
"Announcements": {
|
|
||||||
"NotAllowed": {
|
|
||||||
"Title": "It seems that you do not have sufficient rights to view this page.",
|
|
||||||
"Message": "For more details, please contact an administrator."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Dashboard": {
|
|
||||||
"Announcements": {
|
|
||||||
"Guest": {
|
|
||||||
"Title": "Hello there! I'm glad you're here!",
|
|
||||||
"Message": "You are currently browsing my application as a guest. Keep in mind that you cannot perform actions, and most of the data you see is fake. The purpose of the application in this state is for presentation."
|
|
||||||
},
|
|
||||||
"User": {
|
|
||||||
"Title": "Welcome back, {{userName}}",
|
|
||||||
"Message": "The application is in continuous development, so if you identify a problem, please report it. Thank you!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"User": {
|
|
||||||
"Profile": {
|
|
||||||
"Label": "Profile",
|
|
||||||
"Hello": "Hi, {{userName}}",
|
|
||||||
"Description": "{{userName}}, authenticated on {{loginDate}}",
|
|
||||||
"OpenPortfolio": "Open portfolio",
|
|
||||||
"Security": {
|
|
||||||
"UserGroups": "User groups",
|
|
||||||
"UserRoles": "User roles"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Settings": "Settings",
|
|
||||||
"Logout": "Logout"
|
|
||||||
},
|
|
||||||
"Machine": {
|
|
||||||
"FullName": "Full machine name",
|
|
||||||
"Name": "Machine name",
|
|
||||||
"IP": "IP",
|
|
||||||
"MAC": "MAC address",
|
|
||||||
"Description": "Description",
|
|
||||||
"PoweredOn": "Powered on",
|
|
||||||
"Actions": {
|
|
||||||
"Wake": "Wake",
|
|
||||||
"Ping": "Ping",
|
|
||||||
"More": "More",
|
|
||||||
"Shutdown": "Shutdown",
|
|
||||||
"Restart": "Restart",
|
|
||||||
"Advanced": "Advanced"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System": {
|
|
||||||
"Navigation": {
|
|
||||||
"MainServices": "Main services",
|
|
||||||
"Agents": "Agents"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Settings": {
|
|
||||||
"Navigation": {
|
|
||||||
"System": "System",
|
|
||||||
"Appearance": "Appearance",
|
|
||||||
"Notifications": "Notifications"
|
|
||||||
},
|
|
||||||
"Cache": {
|
|
||||||
"Title": "Cache settings",
|
|
||||||
"Reset": "Reset",
|
|
||||||
"ResetInfo": "Cache reset."
|
|
||||||
},
|
|
||||||
"Appearance": {
|
|
||||||
"Title": "Appearance settings"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,134 +0,0 @@
|
||||||
{
|
|
||||||
"Language": {
|
|
||||||
"English": "Engleză",
|
|
||||||
"Romanian": "Română"
|
|
||||||
},
|
|
||||||
"Generic": {
|
|
||||||
"Copy": "Copiază",
|
|
||||||
"OpenInNewTab": "Deschide într-un tab nou",
|
|
||||||
"CopiedToClipboard": "Copiat în clipboard",
|
|
||||||
"SendEmail": "Trimite email"
|
|
||||||
},
|
|
||||||
"Menu": {
|
|
||||||
"Dashboard": "Bord",
|
|
||||||
"Machines": "Mașini",
|
|
||||||
"System": "Sistem",
|
|
||||||
"Administration": "Administrare",
|
|
||||||
"Settings": "Setări",
|
|
||||||
"About": "Despre"
|
|
||||||
},
|
|
||||||
"ViewModes": {
|
|
||||||
"Table": "Tabel",
|
|
||||||
"List": "Lista"
|
|
||||||
},
|
|
||||||
"Login": {
|
|
||||||
"Username": "Utilizator",
|
|
||||||
"Password": "Parolă",
|
|
||||||
"Label": "Autentificare",
|
|
||||||
"IncorrectCredentials": "Credențiale incorecte."
|
|
||||||
},
|
|
||||||
"Announcements": {
|
|
||||||
"NotAllowed": {
|
|
||||||
"Title": "Se pare că nu aveți suficiente drepturi pentru a vizualiza această pagină.",
|
|
||||||
"Message": "Pentru mai multe detalii, vă rugăm să contactați un administrator."
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Dashboard": {
|
|
||||||
"Announcements": {
|
|
||||||
"Guest": {
|
|
||||||
"Title": "Salutare! Sunt bucuros ca ești aici!",
|
|
||||||
"Message": "În acest moment, răsfoiești aplicația mea că invitat. Reține că nu poți efectua acțiuni, iar majoritatea datelor pe care le vezi sunt false. Scopul aplicației în această stare este de prezentare."
|
|
||||||
},
|
|
||||||
"User": {
|
|
||||||
"Title": "Bine ai revenit, {{userName}}",
|
|
||||||
"Message": "Aplicația este în continuă dezvoltare, așa că dacă identifici o problemă, te rog să o raportezi. Mulțumesc!"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"User": {
|
|
||||||
"Profile": {
|
|
||||||
"Label": "Profil",
|
|
||||||
"Hello": "Salut, {{userName}}",
|
|
||||||
"Description": "{{userName}}, autentificat pe {{loginDate}}",
|
|
||||||
"OpenPortfolio": "Deschide portofoliu",
|
|
||||||
"Security": {
|
|
||||||
"UserGroups": "Grupuri utilizator",
|
|
||||||
"UserRoles": "Roluri utilizator"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Settings": "Setări",
|
|
||||||
"Logout": "Deconectare"
|
|
||||||
},
|
|
||||||
"Machine": {
|
|
||||||
"FullName": "Nume intreg masina",
|
|
||||||
"Name": "Nume masina",
|
|
||||||
"IP": "IP",
|
|
||||||
"MAC": "Adresa MAC",
|
|
||||||
"Description": "Descriere",
|
|
||||||
"PoweredOn": "Pornit",
|
|
||||||
"Actions": {
|
|
||||||
"Wake": "Pornește",
|
|
||||||
"Ping": "Ping",
|
|
||||||
"More": "Mai mult",
|
|
||||||
"Shutdown": "Oprește",
|
|
||||||
"Restart": "Repornește",
|
|
||||||
"Advanced": "Avansat"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"System": {
|
|
||||||
"Navigation": {
|
|
||||||
"MainServices": "Servicii principale",
|
|
||||||
"Agents": "Agenți"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Settings": {
|
|
||||||
"Navigation": {
|
|
||||||
"System": "Sistem",
|
|
||||||
"Appearance": "Aspect",
|
|
||||||
"Notifications": "Notificări"
|
|
||||||
},
|
|
||||||
"Cache": {
|
|
||||||
"Title": "Setări cache",
|
|
||||||
"Reset": "Resetați",
|
|
||||||
"ResetInfo": "Cache resetat."
|
|
||||||
},
|
|
||||||
"Appearance": {
|
|
||||||
"Title": "Setări de aspect"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"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}}"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Binary file not shown.
Before Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
Before Width: | Height: | Size: 9.4 KiB |
|
@ -1,25 +0,0 @@
|
||||||
{
|
|
||||||
"short_name": "React App",
|
|
||||||
"name": "Create React App Sample",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "favicon.ico",
|
|
||||||
"sizes": "64x64 32x32 24x24 16x16",
|
|
||||||
"type": "image/x-icon"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo192.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "192x192"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "logo512.png",
|
|
||||||
"type": "image/png",
|
|
||||||
"sizes": "512x512"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"start_url": ".",
|
|
||||||
"display": "standalone",
|
|
||||||
"theme_color": "#000000",
|
|
||||||
"background_color": "#ffffff"
|
|
||||||
}
|
|
|
@ -1,3 +0,0 @@
|
||||||
# https://www.robotstxt.org/robotstxt.html
|
|
||||||
User-agent: *
|
|
||||||
Disallow:
|
|
|
@ -1,29 +0,0 @@
|
||||||
"use strict";
|
|
||||||
const fs = require("fs");
|
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
const prefix = "REACT_APP_";
|
|
||||||
const publicUrl = process.env.PUBLIC_URL || "";
|
|
||||||
const scriptPath = path.join("./application", publicUrl, "env.js");
|
|
||||||
|
|
||||||
function generateScriptContent() {
|
|
||||||
const prefixRegex = new RegExp(`^${prefix}`);
|
|
||||||
const env = process.env;
|
|
||||||
const config = Object.keys(env)
|
|
||||||
.filter(key => prefixRegex.test(key))
|
|
||||||
.reduce((c, key) => Object.assign({}, c, { [key]: env[key] }), {});
|
|
||||||
return `window.env=${JSON.stringify(config)};`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveScriptContent(scriptContents) {
|
|
||||||
fs.writeFile(scriptPath, scriptContents, "utf8", function (err) {
|
|
||||||
if (err) throw err;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log("Setting environment variables...");
|
|
||||||
const scriptContent = generateScriptContent();
|
|
||||||
saveScriptContent(scriptContent);
|
|
||||||
console.log(
|
|
||||||
`Updated ${scriptPath} with ${prefix}* environment variables: ${scriptContent}.`
|
|
||||||
);
|
|
Binary file not shown.
Before Width: | Height: | Size: 89 KiB |
|
@ -1,15 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { UserPermissionsProvider, SensitiveInfoProvider } from "../providers";
|
|
||||||
import AppLayout from "./layout/AppLayout";
|
|
||||||
|
|
||||||
const App = () => {
|
|
||||||
return (
|
|
||||||
<UserPermissionsProvider>
|
|
||||||
<SensitiveInfoProvider>
|
|
||||||
<AppLayout />
|
|
||||||
</SensitiveInfoProvider>
|
|
||||||
</UserPermissionsProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default App;
|
|
|
@ -1,73 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import App from "./App";
|
|
||||||
import { BrowserRouter, Switch, Redirect, Route } from "react-router-dom";
|
|
||||||
import { useTuitioToken } from "@flare/tuitio-client-react";
|
|
||||||
import LoginContainer from "../features/login/components/LoginContainer";
|
|
||||||
|
|
||||||
const PrivateRoute = ({ component, ...rest }) => {
|
|
||||||
const { valid } = useTuitioToken();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...rest}
|
|
||||||
render={props =>
|
|
||||||
valid ? (
|
|
||||||
React.createElement(component, props)
|
|
||||||
) : (
|
|
||||||
<Redirect
|
|
||||||
to={{
|
|
||||||
pathname: "/login",
|
|
||||||
state: {
|
|
||||||
from: props.location
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PrivateRoute.propTypes = {
|
|
||||||
component: PropTypes.func.isRequired,
|
|
||||||
location: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
const PublicRoute = ({ component, ...rest }) => {
|
|
||||||
const { valid } = useTuitioToken();
|
|
||||||
return (
|
|
||||||
<Route
|
|
||||||
{...rest}
|
|
||||||
render={props =>
|
|
||||||
valid ? (
|
|
||||||
<Redirect
|
|
||||||
to={{
|
|
||||||
pathname: "/"
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
React.createElement(component, props)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PublicRoute.propTypes = {
|
|
||||||
component: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
const AppRouter = () => {
|
|
||||||
return (
|
|
||||||
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/" render={() => <Redirect to="/dashboard" />} />
|
|
||||||
<PublicRoute path="/login" component={LoginContainer} />
|
|
||||||
<PrivateRoute path="/" component={App} />
|
|
||||||
</Switch>
|
|
||||||
</BrowserRouter>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRouter;
|
|
|
@ -1,42 +0,0 @@
|
||||||
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;
|
|
|
@ -1,45 +0,0 @@
|
||||||
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;
|
|
|
@ -1,29 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
alert: {
|
|
||||||
width: "100%",
|
|
||||||
"& > * + *": {
|
|
||||||
marginTop: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const NotAllowed = () => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.alert}>
|
|
||||||
<Alert variant="outlined" severity="error">
|
|
||||||
<AlertTitle>{t("Announcements.NotAllowed.Title")}</AlertTitle>
|
|
||||||
{t("Announcements.NotAllowed.Message")}
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NotAllowed;
|
|
|
@ -1,44 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { Typography } from "@material-ui/core";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
box: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
marginBottom: theme.spacing(1),
|
|
||||||
marginTop: theme.spacing(0)
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
flexDirection: "column",
|
|
||||||
minHeight: "40px"
|
|
||||||
},
|
|
||||||
titleText: { textTransform: "uppercase" }
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PageTitle = ({ text, toolBar, navigation }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.box}>
|
|
||||||
{navigation && navigation}
|
|
||||||
<div className={classes.title}>
|
|
||||||
<Typography className={classes.titleText} variant="h3" size="sm">
|
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
{toolBar && toolBar}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PageTitle.propTypes = {
|
|
||||||
text: PropTypes.string.isRequired,
|
|
||||||
toolBar: PropTypes.node,
|
|
||||||
navigation: PropTypes.node
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PageTitle;
|
|
|
@ -1,27 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { Typography } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
paper: {
|
|
||||||
margin: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PaperTitle = ({ text }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
return (
|
|
||||||
<div className={classes.paper}>
|
|
||||||
<Typography variant="h5" gutterBottom>
|
|
||||||
{text}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PaperTitle.propTypes = {
|
|
||||||
text: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PaperTitle;
|
|
|
@ -1,4 +0,0 @@
|
||||||
import DataLabel from "./DataLabel";
|
|
||||||
import PaperTitle from "./PaperTitle";
|
|
||||||
|
|
||||||
export { DataLabel, PaperTitle };
|
|
|
@ -1,61 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import {
|
|
||||||
InputAdornment,
|
|
||||||
TextField,
|
|
||||||
makeStyles,
|
|
||||||
IconButton
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { Visibility, VisibilityOff, LockOutlined } from "@material-ui/icons";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
margin: {
|
|
||||||
margin: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const PasswordField = ({ label, ...rest }) => {
|
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const handleClickShowPassword = () => {
|
|
||||||
setShowPassword(!showPassword);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleMouseDownPassword = event => {
|
|
||||||
event.preventDefault();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TextField
|
|
||||||
className={classes.margin}
|
|
||||||
label={label || "Password"}
|
|
||||||
{...rest}
|
|
||||||
type={showPassword ? "text" : "password"}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<LockOutlined />
|
|
||||||
</InputAdornment>
|
|
||||||
),
|
|
||||||
endAdornment: (
|
|
||||||
<InputAdornment position="end">
|
|
||||||
<IconButton
|
|
||||||
aria-label="toggle password visibility"
|
|
||||||
onClick={handleClickShowPassword}
|
|
||||||
onMouseDown={handleMouseDownPassword}
|
|
||||||
>
|
|
||||||
{showPassword ? <Visibility /> : <VisibilityOff />}
|
|
||||||
</IconButton>
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
PasswordField.propTypes = {
|
|
||||||
label: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default PasswordField;
|
|
|
@ -1,34 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import AppRoutes from "./AppRoutes";
|
|
||||||
import TopBar from "./TopBar";
|
|
||||||
import Sidebar from "./Sidebar";
|
|
||||||
import styles from "./styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
const AppLayout = () => {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const handleDrawerOpen = () => {
|
|
||||||
setOpen(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDrawerClose = () => {
|
|
||||||
setOpen(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.root}>
|
|
||||||
<TopBar open={open} handleDrawerOpen={handleDrawerOpen} />
|
|
||||||
<Sidebar open={open} handleDrawerClose={handleDrawerClose} />
|
|
||||||
<main className={classes.content}>
|
|
||||||
<div className={classes.toolbar} />
|
|
||||||
<AppRoutes />
|
|
||||||
</main>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppLayout;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { Route, Switch } from "react-router-dom";
|
|
||||||
import PageNotFound from "./PageNotFound";
|
|
||||||
import NetworkContainer from "../../features/network/components/NetworkContainer";
|
|
||||||
import SystemContainer from "../../features/system/SystemContainer";
|
|
||||||
import SettingsContainer from "../../features/settings/SettingsContainer";
|
|
||||||
import DashboardContainer from "../../features/dashboard/DashboardContainer";
|
|
||||||
import UserProfileContainer from "../../features/user/profile/card/UserProfileContainer";
|
|
||||||
import AboutContainer from "../../features/about/AboutContainer";
|
|
||||||
|
|
||||||
const AppRoutes = () => {
|
|
||||||
return (
|
|
||||||
<Switch>
|
|
||||||
<Route exact path="/dashboard" component={DashboardContainer} />
|
|
||||||
<Route exact path="/user-profile" component={UserProfileContainer} />
|
|
||||||
<Route exact path="/machines" component={NetworkContainer} />
|
|
||||||
<Route exact path="/system" component={SystemContainer} />
|
|
||||||
<Route exact path="/settings" component={SettingsContainer} />
|
|
||||||
<Route exact path="/about" component={AboutContainer} />
|
|
||||||
<Route component={PageNotFound} />
|
|
||||||
</Switch>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppRoutes;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { IconButton } from "@material-ui/core";
|
|
||||||
import {
|
|
||||||
Brightness2 as MoonIcon,
|
|
||||||
WbSunny as SunIcon
|
|
||||||
} from "@material-ui/icons";
|
|
||||||
import { useApplicationTheme } from "../../providers/ThemeProvider";
|
|
||||||
|
|
||||||
const LightDarkToggle = () => {
|
|
||||||
const { isDark, onDarkModeChanged } = useApplicationTheme();
|
|
||||||
|
|
||||||
const handleChange = () => onDarkModeChanged(!isDark);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
aria-label="light-dark-toggle"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleChange}
|
|
||||||
>
|
|
||||||
{isDark ? <SunIcon /> : <MoonIcon />}
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LightDarkToggle;
|
|
|
@ -1,5 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
const PageNotFound = () => <h1>Oops! Page not found</h1>;
|
|
||||||
|
|
||||||
export default PageNotFound;
|
|
|
@ -1,103 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import {
|
|
||||||
IconButton,
|
|
||||||
Menu,
|
|
||||||
MenuItem,
|
|
||||||
Typography,
|
|
||||||
ListItemIcon
|
|
||||||
} from "@material-ui/core";
|
|
||||||
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 { useTuitioClient } from "@flare/tuitio-client-react";
|
|
||||||
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 history = useHistory();
|
|
||||||
const { error } = useToast();
|
|
||||||
const classes = useStyles();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const { logout } = useTuitioClient({
|
|
||||||
onLogoutFailed: errorMessage => error(errorMessage),
|
|
||||||
onLogoutError: err => error(err.message)
|
|
||||||
});
|
|
||||||
|
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
|
||||||
const openUserMenu = Boolean(anchorEl);
|
|
||||||
|
|
||||||
const handleMenu = event => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<IconButton
|
|
||||||
aria-label="account of current user"
|
|
||||||
aria-controls="menu-appbar"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={handleMenu}
|
|
||||||
color="inherit"
|
|
||||||
>
|
|
||||||
<AccountCircle />
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
id="menu-appbar"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right"
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right"
|
|
||||||
}}
|
|
||||||
open={openUserMenu}
|
|
||||||
onClose={handleClose}
|
|
||||||
>
|
|
||||||
<MenuItem
|
|
||||||
onClick={() => {
|
|
||||||
history.push("/user-profile");
|
|
||||||
handleClose();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<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>
|
|
||||||
</Menu>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ProfileButton;
|
|
|
@ -1,25 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { IconButton } from "@material-ui/core";
|
|
||||||
import {
|
|
||||||
Visibility as VisibilityIcon,
|
|
||||||
VisibilityOff as VisibilityOffIcon
|
|
||||||
} from "@material-ui/icons";
|
|
||||||
import { useSensitiveInfo } from "../../hooks";
|
|
||||||
|
|
||||||
const SensitiveInfoToggle = () => {
|
|
||||||
const { enabled, onSensitiveInfoEnabled } = useSensitiveInfo();
|
|
||||||
|
|
||||||
const handleChange = () => onSensitiveInfoEnabled(!enabled);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<IconButton
|
|
||||||
aria-label="sensitive-info-toggle"
|
|
||||||
color="inherit"
|
|
||||||
onClick={handleChange}
|
|
||||||
>
|
|
||||||
{enabled ? <VisibilityOffIcon /> : <VisibilityIcon />}
|
|
||||||
</IconButton>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SensitiveInfoToggle;
|
|
|
@ -1,162 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { makeStyles, useTheme } from "@material-ui/core/styles";
|
|
||||||
import {
|
|
||||||
Drawer,
|
|
||||||
List,
|
|
||||||
Divider,
|
|
||||||
IconButton,
|
|
||||||
ListItemIcon,
|
|
||||||
ListItemText
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import ChevronLeftIcon from "@material-ui/icons/ChevronLeft";
|
|
||||||
import ChevronRightIcon from "@material-ui/icons/ChevronRight";
|
|
||||||
import ListItem from "@material-ui/core/ListItem";
|
|
||||||
import BuildIcon from "@material-ui/icons/Build";
|
|
||||||
import DnsIcon from "@material-ui/icons/Dns";
|
|
||||||
import DeviceHubIcon from "@material-ui/icons/DeviceHub";
|
|
||||||
import SettingsIcon from "@material-ui/icons/Settings";
|
|
||||||
import DashboardIcon from "@material-ui/icons/Dashboard";
|
|
||||||
import FeaturedPlayListIcon from "@material-ui/icons/FeaturedPlayList";
|
|
||||||
import { useHistory } from "react-router-dom";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import styles from "./styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
const menu = [
|
|
||||||
{
|
|
||||||
order: 0,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
code: "dashboard",
|
|
||||||
name: "Menu.Dashboard",
|
|
||||||
route: "/dashboard",
|
|
||||||
icon: <DashboardIcon />,
|
|
||||||
order: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "machines",
|
|
||||||
name: "Menu.Machines",
|
|
||||||
route: "/machines",
|
|
||||||
icon: <DnsIcon />,
|
|
||||||
order: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "system",
|
|
||||||
name: "Menu.System",
|
|
||||||
route: "/system",
|
|
||||||
icon: <DeviceHubIcon />,
|
|
||||||
order: 2
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 1,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
code: "administration",
|
|
||||||
name: "Menu.Administration",
|
|
||||||
route: "/administration",
|
|
||||||
icon: <BuildIcon />,
|
|
||||||
order: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "settings",
|
|
||||||
name: "Menu.Settings",
|
|
||||||
route: "/settings",
|
|
||||||
icon: <SettingsIcon />,
|
|
||||||
order: 1
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
order: 2,
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
code: "about",
|
|
||||||
name: "Menu.About",
|
|
||||||
route: "/about",
|
|
||||||
icon: <FeaturedPlayListIcon />,
|
|
||||||
order: 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const sortedMenu = menu.sort((i1, i2) => i1 - i2);
|
|
||||||
|
|
||||||
const Sidebar = ({ open, handleDrawerClose }) => {
|
|
||||||
const [selected, setSelected] = useState(null);
|
|
||||||
|
|
||||||
const classes = useStyles();
|
|
||||||
const theme = useTheme();
|
|
||||||
const history = useHistory();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleClick = route => () => {
|
|
||||||
setSelected(route);
|
|
||||||
history.push(route);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isSelected = key => selected === key;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer
|
|
||||||
variant="permanent"
|
|
||||||
className={clsx(classes.drawer, {
|
|
||||||
[classes.drawerOpen]: open,
|
|
||||||
[classes.drawerClose]: !open
|
|
||||||
})}
|
|
||||||
classes={{
|
|
||||||
paper: clsx({
|
|
||||||
[classes.drawerOpen]: open,
|
|
||||||
[classes.drawerClose]: !open
|
|
||||||
})
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className={classes.toolbar}>
|
|
||||||
<IconButton onClick={handleDrawerClose}>
|
|
||||||
{theme.direction === "rtl" ? (
|
|
||||||
<ChevronRightIcon />
|
|
||||||
) : (
|
|
||||||
<ChevronLeftIcon />
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
</div>
|
|
||||||
<Divider />
|
|
||||||
|
|
||||||
{sortedMenu.map((menu, index) => {
|
|
||||||
const isLast = index === sortedMenu.length - 1;
|
|
||||||
return (
|
|
||||||
<React.Fragment key={`menu-${menu.order}`}>
|
|
||||||
<List>
|
|
||||||
{menu.items
|
|
||||||
.sort((i1, i2) => i1 - i2)
|
|
||||||
.map(item => (
|
|
||||||
<ListItem
|
|
||||||
button
|
|
||||||
key={item.code}
|
|
||||||
onClick={handleClick(item.route)}
|
|
||||||
selected={isSelected(item.route)}
|
|
||||||
>
|
|
||||||
<ListItemIcon>{item.icon}</ListItemIcon>
|
|
||||||
<ListItemText primary={t(item.name)} />
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
{!isLast && <Divider />}
|
|
||||||
</React.Fragment>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
Sidebar.propTypes = {
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
handleDrawerClose: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Sidebar;
|
|
|
@ -1,52 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import clsx from "clsx";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { AppBar, Toolbar, Typography, IconButton } from "@material-ui/core";
|
|
||||||
import MenuIcon from "@material-ui/icons/Menu";
|
|
||||||
import ProfileButton from "./ProfileButton";
|
|
||||||
import LightDarkToggle from "./LightDarkToggle";
|
|
||||||
import SensitiveInfoToggle from "./SensitiveInfoToggle";
|
|
||||||
import styles from "./styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
const TopBar = ({ open, handleDrawerOpen }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AppBar
|
|
||||||
position="fixed"
|
|
||||||
className={clsx(classes.appBar, {
|
|
||||||
[classes.appBarShift]: open
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<Toolbar>
|
|
||||||
<IconButton
|
|
||||||
color="inherit"
|
|
||||||
aria-label="open drawer"
|
|
||||||
onClick={handleDrawerOpen}
|
|
||||||
edge="start"
|
|
||||||
className={clsx(classes.menuButton, {
|
|
||||||
[classes.hide]: open
|
|
||||||
})}
|
|
||||||
>
|
|
||||||
<MenuIcon />
|
|
||||||
</IconButton>
|
|
||||||
<Typography variant="h6" noWrap className={classes.title}>
|
|
||||||
Network resurrector
|
|
||||||
</Typography>
|
|
||||||
<SensitiveInfoToggle />
|
|
||||||
<LightDarkToggle />
|
|
||||||
<ProfileButton />
|
|
||||||
</Toolbar>
|
|
||||||
</AppBar>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
TopBar.propTypes = {
|
|
||||||
open: PropTypes.bool.isRequired,
|
|
||||||
handleDrawerOpen: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TopBar;
|
|
|
@ -1,71 +0,0 @@
|
||||||
const drawerWidth = 240;
|
|
||||||
|
|
||||||
const styles = theme => ({
|
|
||||||
root: {
|
|
||||||
display: "flex"
|
|
||||||
},
|
|
||||||
appBar: {
|
|
||||||
zIndex: theme.zIndex.drawer + 1,
|
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen
|
|
||||||
})
|
|
||||||
},
|
|
||||||
appBarShift: {
|
|
||||||
marginLeft: drawerWidth,
|
|
||||||
width: `calc(100% - ${drawerWidth}px)`,
|
|
||||||
transition: theme.transitions.create(["width", "margin"], {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.enteringScreen
|
|
||||||
})
|
|
||||||
},
|
|
||||||
menuButton: {
|
|
||||||
marginRight: 36
|
|
||||||
},
|
|
||||||
hide: {
|
|
||||||
display: "none"
|
|
||||||
},
|
|
||||||
drawer: {
|
|
||||||
width: drawerWidth,
|
|
||||||
flexShrink: 0,
|
|
||||||
whiteSpace: "nowrap"
|
|
||||||
},
|
|
||||||
drawerOpen: {
|
|
||||||
width: drawerWidth,
|
|
||||||
transition: theme.transitions.create("width", {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.enteringScreen
|
|
||||||
})
|
|
||||||
},
|
|
||||||
drawerClose: {
|
|
||||||
transition: theme.transitions.create("width", {
|
|
||||||
easing: theme.transitions.easing.sharp,
|
|
||||||
duration: theme.transitions.duration.leavingScreen
|
|
||||||
}),
|
|
||||||
overflowX: "hidden",
|
|
||||||
width: theme.spacing(7) + 1,
|
|
||||||
[theme.breakpoints.up("sm")]: {
|
|
||||||
width: theme.spacing(9) + 1
|
|
||||||
}
|
|
||||||
},
|
|
||||||
toolbar: {
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
justifyContent: "flex-end",
|
|
||||||
padding: theme.spacing(0, 1),
|
|
||||||
// necessary for content to be below app bar
|
|
||||||
...theme.mixins.toolbar
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
flexGrow: 1
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flexGrow: 1,
|
|
||||||
padding: theme.spacing(2)
|
|
||||||
},
|
|
||||||
menuItemIcon: {
|
|
||||||
minWidth: "26px"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default styles;
|
|
|
@ -1,58 +0,0 @@
|
||||||
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,27 +0,0 @@
|
||||||
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;
|
|
|
@ -1,37 +0,0 @@
|
||||||
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;
|
|
|
@ -1,36 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import ReleaseNotesList from "./ReleaseNotesList";
|
|
||||||
import TimelineComponent from "../timeline/TimelineComponent";
|
|
||||||
import { routes, get } from "../../../utils/api";
|
|
||||||
|
|
||||||
const sort = releases =>
|
|
||||||
releases.sort((a, b) => new Date(b.date) - new Date(a.date));
|
|
||||||
|
|
||||||
const ReleaseNotesContainer = ({ view }) => {
|
|
||||||
const [state, setState] = useState({ data: [], loaded: false });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.loaded) return;
|
|
||||||
get(routes.releaseNotes, {
|
|
||||||
onCompleted: data => setState({ data, loaded: true })
|
|
||||||
});
|
|
||||||
}, [state.loaded]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{state.loaded &&
|
|
||||||
(view === "timeline" ? (
|
|
||||||
<TimelineComponent releases={sort(state.data)} />
|
|
||||||
) : (
|
|
||||||
<ReleaseNotesList releases={sort(state.data)} />
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
ReleaseNotesContainer.propTypes = {
|
|
||||||
view: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReleaseNotesContainer;
|
|
|
@ -1,59 +0,0 @@
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
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;
|
|
|
@ -1,103 +0,0 @@
|
||||||
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;
|
|
|
@ -1,35 +0,0 @@
|
||||||
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;
|
|
|
@ -1,154 +0,0 @@
|
||||||
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;
|
|
|
@ -1,18 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import SystemVersionComponent from "./SystemVersionComponent";
|
|
||||||
import { routes, get } from "../../../utils/api";
|
|
||||||
|
|
||||||
const SystemVersionContainer = () => {
|
|
||||||
const [state, setState] = useState({ data: {}, loaded: false });
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (state.loaded) return;
|
|
||||||
get(routes.systemVersion, {
|
|
||||||
onCompleted: data => setState({ data, loaded: true })
|
|
||||||
});
|
|
||||||
}, [state.loaded]);
|
|
||||||
|
|
||||||
return <>{state.loaded && <SystemVersionComponent data={state.data} />}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SystemVersionContainer;
|
|
|
@ -1,117 +0,0 @@
|
||||||
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,10 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import AnnouncementsSection from "./announcements/AnnouncementsSection";
|
|
||||||
|
|
||||||
const DashboardContainer = () => (
|
|
||||||
<>
|
|
||||||
<AnnouncementsSection />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default DashboardContainer;
|
|
|
@ -1,17 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import GuestAnnouncement from "./GuestAnnouncement";
|
|
||||||
import UserAnnouncement from "./UserAnnouncement";
|
|
||||||
import { usePermissions } from "../../../hooks";
|
|
||||||
|
|
||||||
const AnnouncementsSection = () => {
|
|
||||||
const { loading, isGuest, isUser } = usePermissions();
|
|
||||||
if (loading) return "";
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{isUser && <UserAnnouncement />}
|
|
||||||
{isGuest && <GuestAnnouncement />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AnnouncementsSection;
|
|
|
@ -1,21 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
|
||||||
import styles from "../styles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
export default function GuestAnnouncement() {
|
|
||||||
const classes = useStyles();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.alert}>
|
|
||||||
<Alert variant="outlined" severity="warning">
|
|
||||||
<AlertTitle>{t("Dashboard.Announcements.Guest.Title")}</AlertTitle>
|
|
||||||
{t("Dashboard.Announcements.Guest.Message")}
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,27 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { Alert, AlertTitle } from "@material-ui/lab";
|
|
||||||
import styles from "../styles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useTuitioUser } from "@flare/tuitio-client-react";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
export default function UserAnnouncement() {
|
|
||||||
const classes = useStyles();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { userName } = useTuitioUser();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.alert}>
|
|
||||||
<Alert variant="outlined" severity="info">
|
|
||||||
<AlertTitle>
|
|
||||||
{t("Dashboard.Announcements.User.Title", {
|
|
||||||
userName
|
|
||||||
})}
|
|
||||||
</AlertTitle>
|
|
||||||
{t("Dashboard.Announcements.User.Message")}
|
|
||||||
</Alert>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,10 +0,0 @@
|
||||||
const styles = theme => ({
|
|
||||||
alert: {
|
|
||||||
width: "100%",
|
|
||||||
"& > * + *": {
|
|
||||||
marginTop: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default styles;
|
|
|
@ -1,32 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { Card } from "@material-ui/core";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import styles from "../styles";
|
|
||||||
import LoginComponent from "./LoginComponent";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
const LoginCard = ({ credentials, onChange, onLogin }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.appLogin}>
|
|
||||||
<Card variant="outlined">
|
|
||||||
<LoginComponent
|
|
||||||
credentials={credentials}
|
|
||||||
onChange={onChange}
|
|
||||||
onLogin={onLogin}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LoginCard.propTypes = {
|
|
||||||
credentials: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onLogin: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginCard;
|
|
|
@ -1,71 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import {
|
|
||||||
TextField,
|
|
||||||
InputAdornment,
|
|
||||||
Button,
|
|
||||||
CardActions,
|
|
||||||
CardContent
|
|
||||||
} from "@material-ui/core";
|
|
||||||
import { AccountCircleOutlined } from "@material-ui/icons";
|
|
||||||
import PasswordField from "../../../components/common/inputs/PasswordField";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import styles from "../styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(styles);
|
|
||||||
|
|
||||||
const LoginComponent = ({ credentials, onChange, onLogin }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<CardContent>
|
|
||||||
<TextField
|
|
||||||
className={classes.field}
|
|
||||||
id="username"
|
|
||||||
label={t("Login.Username")}
|
|
||||||
onChange={onChange("userName")}
|
|
||||||
value={credentials.userName}
|
|
||||||
InputProps={{
|
|
||||||
startAdornment: (
|
|
||||||
<InputAdornment position="start">
|
|
||||||
<AccountCircleOutlined />
|
|
||||||
</InputAdornment>
|
|
||||||
)
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<br />
|
|
||||||
<PasswordField
|
|
||||||
id="password"
|
|
||||||
label={t("Login.Password")}
|
|
||||||
className={classes.field}
|
|
||||||
onChange={onChange("password")}
|
|
||||||
value={credentials.password}
|
|
||||||
onKeyDown={e => {
|
|
||||||
if (e.key === "Enter") onLogin();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</CardContent>
|
|
||||||
<CardActions className={classes.actions}>
|
|
||||||
<Button
|
|
||||||
className={classes.onRight}
|
|
||||||
variant="contained"
|
|
||||||
color="primary"
|
|
||||||
onClick={onLogin}
|
|
||||||
>
|
|
||||||
{t("Login.Label")}
|
|
||||||
</Button>
|
|
||||||
</CardActions>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LoginComponent.propTypes = {
|
|
||||||
credentials: PropTypes.object.isRequired,
|
|
||||||
onChange: PropTypes.func.isRequired,
|
|
||||||
onLogin: PropTypes.func.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginComponent;
|
|
|
@ -1,38 +0,0 @@
|
||||||
import React, { useState } from "react";
|
|
||||||
import LoginCard from "./LoginCard";
|
|
||||||
import { useToast } from "../../../hooks";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useTuitioClient } from "@flare/tuitio-client-react";
|
|
||||||
|
|
||||||
const LoginContainer = () => {
|
|
||||||
const [credentials, setCredentials] = useState({
|
|
||||||
userName: "",
|
|
||||||
password: ""
|
|
||||||
});
|
|
||||||
|
|
||||||
const { error } = useToast();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { login } = useTuitioClient({
|
|
||||||
onLoginFailed: () => error(t("Login.IncorrectCredentials")),
|
|
||||||
onLoginError: err => error(err.message)
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleChange = prop => event => {
|
|
||||||
setCredentials(prev => ({ ...prev, [prop]: event.target.value }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLogin = () => {
|
|
||||||
const { userName, password } = credentials;
|
|
||||||
return login(userName, password);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LoginCard
|
|
||||||
credentials={credentials}
|
|
||||||
onChange={handleChange}
|
|
||||||
onLogin={handleLogin}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LoginContainer;
|
|
|
@ -1,21 +0,0 @@
|
||||||
const styles = theme => ({
|
|
||||||
onRight: {
|
|
||||||
marginLeft: "auto"
|
|
||||||
},
|
|
||||||
appLogin: {
|
|
||||||
minHeight: "100vh",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center"
|
|
||||||
},
|
|
||||||
field: {
|
|
||||||
margin: theme.spacing(1),
|
|
||||||
width: "300px"
|
|
||||||
},
|
|
||||||
actions: {
|
|
||||||
paddingRight: "16px",
|
|
||||||
paddingLeft: "16px"
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
export default styles;
|
|
|
@ -1,114 +0,0 @@
|
||||||
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 MachineCollapsedContent from "./common/MachineCollapsedContent";
|
|
||||||
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>
|
|
||||||
<MachineCollapsedContent
|
|
||||||
description={machine.description}
|
|
||||||
logs={logs}
|
|
||||||
style={{ width: "100%" }}
|
|
||||||
/>
|
|
||||||
</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,133 +0,0 @@
|
||||||
import React, { useState, useCallback } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import MachineTableRow from "./MachineTableRow";
|
|
||||||
import MachineAccordion from "./MachineAccordion";
|
|
||||||
import { ViewModes } from "./ViewModeSelection";
|
|
||||||
import { useToast } from "../../../hooks";
|
|
||||||
import { LastPage, RotateLeft, Launch, Stop } from "@material-ui/icons";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { routes, post } from "../../../utils/api";
|
|
||||||
|
|
||||||
const MachineContainer = ({ machine, viewMode }) => {
|
|
||||||
const [logs, setLogs] = useState([]);
|
|
||||||
|
|
||||||
const { success, error } = useToast();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const addLog = useCallback(
|
|
||||||
text => {
|
|
||||||
setLogs(prev => [...prev, text]);
|
|
||||||
},
|
|
||||||
[setLogs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const manageActionResponse = useCallback(
|
|
||||||
response => {
|
|
||||||
addLog(`Success: ${response.success}. Status: ${response.status}`);
|
|
||||||
if (response.success) {
|
|
||||||
success(response.status);
|
|
||||||
} else {
|
|
||||||
error(response.status);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[error, success, addLog]
|
|
||||||
);
|
|
||||||
|
|
||||||
const pingMachine = useCallback(
|
|
||||||
async machine => {
|
|
||||||
await post(
|
|
||||||
routes.pingMachine,
|
|
||||||
{ machineId: machine.machineId },
|
|
||||||
{
|
|
||||||
onCompleted: manageActionResponse
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[manageActionResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
const shutdownMachine = useCallback(
|
|
||||||
async machine => {
|
|
||||||
await post(
|
|
||||||
routes.shutdownMachine,
|
|
||||||
{ machineId: machine.machineId, delay: 0, force: false },
|
|
||||||
{
|
|
||||||
onCompleted: manageActionResponse
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[manageActionResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
const restartMachine = useCallback(
|
|
||||||
async machine => {
|
|
||||||
await post(
|
|
||||||
routes.restartMachine,
|
|
||||||
{ machineId: machine.machineId, delay: 0, force: false },
|
|
||||||
{
|
|
||||||
onCompleted: manageActionResponse
|
|
||||||
}
|
|
||||||
);
|
|
||||||
},
|
|
||||||
[manageActionResponse]
|
|
||||||
);
|
|
||||||
|
|
||||||
const actions = [
|
|
||||||
{
|
|
||||||
code: "ping",
|
|
||||||
effect: pingMachine,
|
|
||||||
icon: LastPage,
|
|
||||||
tooltip: t("Machine.Actions.Ping"),
|
|
||||||
main: true
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "shutdown",
|
|
||||||
effect: shutdownMachine,
|
|
||||||
icon: Stop,
|
|
||||||
tooltip: t("Machine.Actions.Shutdown"),
|
|
||||||
main: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "restart",
|
|
||||||
effect: restartMachine,
|
|
||||||
icon: RotateLeft,
|
|
||||||
tooltip: t("Machine.Actions.Restart"),
|
|
||||||
main: false
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: "advanced",
|
|
||||||
effect: () => {},
|
|
||||||
icon: Launch,
|
|
||||||
tooltip: t("Machine.Actions.Advanced"),
|
|
||||||
main: false
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{viewMode === ViewModes.TABLE && (
|
|
||||||
<MachineTableRow
|
|
||||||
machine={machine}
|
|
||||||
actions={actions}
|
|
||||||
logs={logs}
|
|
||||||
addLog={addLog}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{viewMode === ViewModes.ACCORDION && (
|
|
||||||
<MachineAccordion
|
|
||||||
machine={machine}
|
|
||||||
actions={actions}
|
|
||||||
logs={logs}
|
|
||||||
addLog={addLog}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MachineContainer.propTypes = {
|
|
||||||
machine: PropTypes.object.isRequired,
|
|
||||||
viewMode: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MachineContainer;
|
|
|
@ -1,74 +0,0 @@
|
||||||
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 MachineCollapsedContent from "./common/MachineCollapsedContent";
|
|
||||||
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>
|
|
||||||
<MachineCollapsedContent
|
|
||||||
description={machine.description}
|
|
||||||
logs={logs}
|
|
||||||
style={{ paddingBottom: "10px" }}
|
|
||||||
/>
|
|
||||||
</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,55 +0,0 @@
|
||||||
import React, { useContext, useEffect, useCallback, useState } from "react";
|
|
||||||
import {
|
|
||||||
NetworkStateContext,
|
|
||||||
NetworkDispatchContext
|
|
||||||
} from "../../network/state/contexts";
|
|
||||||
import MachinesListComponent from "./MachinesListComponent";
|
|
||||||
import PageTitle from "../../../components/common/PageTitle";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import ViewModeSelection, { ViewModes } from "./ViewModeSelection";
|
|
||||||
import { routes, get } from "../../../utils/api";
|
|
||||||
|
|
||||||
const MachinesContainer = () => {
|
|
||||||
const [viewMode, setViewMode] = useState(null);
|
|
||||||
|
|
||||||
const state = useContext(NetworkStateContext);
|
|
||||||
const dispatchActions = useContext(NetworkDispatchContext);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
|
|
||||||
const handleReadMachines = useCallback(async () => {
|
|
||||||
await get(routes.machines, {
|
|
||||||
onCompleted: machines => {
|
|
||||||
const data = Object.assign(machines, { loaded: true });
|
|
||||||
dispatchActions.onNetworkChange("machines", data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [dispatchActions]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!state.network.machines.loaded) {
|
|
||||||
handleReadMachines();
|
|
||||||
}
|
|
||||||
}, [handleReadMachines, state.network.machines.loaded]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<PageTitle
|
|
||||||
text={t("Menu.Machines")}
|
|
||||||
toolBar={
|
|
||||||
<ViewModeSelection
|
|
||||||
callback={setViewMode}
|
|
||||||
initialMode={ViewModes.TABLE}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
{viewMode && (
|
|
||||||
<MachinesListComponent
|
|
||||||
machines={state.network.machines}
|
|
||||||
viewMode={viewMode}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MachinesContainer;
|
|
|
@ -1,80 +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";
|
|
||||||
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;
|
|
|
@ -1,80 +0,0 @@
|
||||||
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;
|
|
|
@ -1,47 +0,0 @@
|
||||||
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, disabled } = 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}
|
|
||||||
disabled={disabled}
|
|
||||||
>
|
|
||||||
<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,
|
|
||||||
disabled: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ActionButton;
|
|
|
@ -1,93 +0,0 @@
|
||||||
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";
|
|
||||||
import { usePermissions } from "../../../../hooks";
|
|
||||||
|
|
||||||
const ActionsGroup = ({ machine, actions, addLog }) => {
|
|
||||||
const [menuAnchor, setMenuAnchor] = useState(null);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { operateMachines: canOperateMachines } = usePermissions();
|
|
||||||
|
|
||||||
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}
|
|
||||||
disabled={!canOperateMachines}
|
|
||||||
/>
|
|
||||||
{mainActions.map(action => (
|
|
||||||
<ActionButton
|
|
||||||
key={`machine-item-${machine.machineId}-${action.code}`}
|
|
||||||
action={action}
|
|
||||||
machine={machine}
|
|
||||||
disabled={!canOperateMachines}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
<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}
|
|
||||||
disabled={!canOperateMachines}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</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,56 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import MachineLog from "./MachineLog";
|
|
||||||
import Typography from "@material-ui/core/Typography";
|
|
||||||
import { useSensitiveInfo } from "../../../../hooks";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(_theme => ({
|
|
||||||
panel: {
|
|
||||||
display: "flex"
|
|
||||||
},
|
|
||||||
label: {
|
|
||||||
marginRight: "4px"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const MachineDescription = ({ description }) => {
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.panel}>
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
className={classes.label}
|
|
||||||
color="textSecondary"
|
|
||||||
>
|
|
||||||
{"Description:"}
|
|
||||||
</Typography>
|
|
||||||
<Typography variant="body2" color="textSecondary">
|
|
||||||
{description}
|
|
||||||
</Typography>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MachineDescription.propTypes = {
|
|
||||||
description: PropTypes.string
|
|
||||||
};
|
|
||||||
|
|
||||||
const MachineCollapsedContent = ({ description, logs, style }) => {
|
|
||||||
const { mask } = useSensitiveInfo();
|
|
||||||
return (
|
|
||||||
<div style={style}>
|
|
||||||
<MachineLog logs={logs} />
|
|
||||||
{description && <MachineDescription description={mask(description)} />}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MachineCollapsedContent.propTypes = {
|
|
||||||
logs: PropTypes.array.isRequired,
|
|
||||||
description: PropTypes.string,
|
|
||||||
style: PropTypes.object
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MachineCollapsedContent;
|
|
|
@ -1,40 +0,0 @@
|
||||||
import React, { useMemo } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { Box } from "@material-ui/core";
|
|
||||||
import { useSensitiveInfo } from "../../../../hooks";
|
|
||||||
import { LazyLog, ScrollFollow } from "react-lazylog";
|
|
||||||
|
|
||||||
const MachineLog = ({ logs }) => {
|
|
||||||
const { maskElements } = useSensitiveInfo();
|
|
||||||
|
|
||||||
const displayLogs = useMemo(
|
|
||||||
() => (logs.length > 0 ? maskElements(logs).join("\n") : "..."),
|
|
||||||
[logs, maskElements]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<div style={{ height: 200 }}>
|
|
||||||
<ScrollFollow
|
|
||||||
startFollowing={true}
|
|
||||||
render={({ follow, onScroll }) => (
|
|
||||||
<LazyLog
|
|
||||||
extraLines={0}
|
|
||||||
enableSearch
|
|
||||||
text={displayLogs}
|
|
||||||
caseInsensitive
|
|
||||||
follow={follow}
|
|
||||||
onScroll={onScroll}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
MachineLog.propTypes = {
|
|
||||||
logs: PropTypes.array.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MachineLog;
|
|
|
@ -1,119 +0,0 @@
|
||||||
import React, { useState, useEffect, useCallback } from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import { IconButton, Tooltip } from "@material-ui/core";
|
|
||||||
import { PowerSettingsNew } from "@material-ui/icons";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import { useToast } from "../../../../hooks";
|
|
||||||
import { msToMinAndSec } from "../../../../utils/time";
|
|
||||||
import { routes, post } from "../../../../utils/api";
|
|
||||||
|
|
||||||
const initialState = { on: false };
|
|
||||||
const defaultPingInterval = 1200000; //20 minutes
|
|
||||||
const defaultStartingTime = 300000; //5 minutes
|
|
||||||
|
|
||||||
const WakeComponent = ({ machine, addLog, disabled }) => {
|
|
||||||
const [state, setState] = useState(initialState);
|
|
||||||
const [trigger, setTrigger] = useState(false);
|
|
||||||
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { success, error } = useToast();
|
|
||||||
|
|
||||||
const pingInterval =
|
|
||||||
process.env.REACT_APP_MACHINE_PING_INTERVAL || defaultPingInterval;
|
|
||||||
const startingTime =
|
|
||||||
process.env.REACT_APP_MACHINE_STARTING_TIME || defaultStartingTime;
|
|
||||||
|
|
||||||
const getCurrentDateTime = useCallback(() => {
|
|
||||||
const currentDateTime = Date.now();
|
|
||||||
const result = t("DATE_FORMAT", {
|
|
||||||
date: { value: currentDateTime, format: "DD-MM-YYYY HH:mm:ss" }
|
|
||||||
});
|
|
||||||
return result;
|
|
||||||
}, [t]);
|
|
||||||
|
|
||||||
const log = useCallback(
|
|
||||||
message => addLog(`[${getCurrentDateTime()}] ${message}`),
|
|
||||||
[addLog, getCurrentDateTime]
|
|
||||||
);
|
|
||||||
|
|
||||||
const wakeMachine = useCallback(async () => {
|
|
||||||
await post(
|
|
||||||
routes.wakeMachine,
|
|
||||||
{ machineId: machine.machineId },
|
|
||||||
{
|
|
||||||
onCompleted: result => {
|
|
||||||
setState(prev => ({ ...prev, on: result.success }));
|
|
||||||
log(`[Wake]: Success: ${result.success}. Status: ${result.status}`);
|
|
||||||
if (result.success) {
|
|
||||||
success(result.status);
|
|
||||||
|
|
||||||
//retrigger
|
|
||||||
log(
|
|
||||||
`Periodic ping will be re-triggered in ${startingTime} ms [${msToMinAndSec(
|
|
||||||
startingTime
|
|
||||||
)}]`
|
|
||||||
);
|
|
||||||
setTimeout(() => {
|
|
||||||
setTrigger(prev => !prev);
|
|
||||||
}, startingTime);
|
|
||||||
} else {
|
|
||||||
error(result.status);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [log, success, error, startingTime, machine.machineId]);
|
|
||||||
|
|
||||||
const pingInLoop = useCallback(async () => {
|
|
||||||
if (disabled) return;
|
|
||||||
await post(
|
|
||||||
routes.pingMachine,
|
|
||||||
{ machineId: machine.machineId },
|
|
||||||
{
|
|
||||||
onCompleted: result => {
|
|
||||||
setState(prev => ({ ...prev, on: result.success }));
|
|
||||||
log(`[Ping]: Success: ${result.success}. Status: ${result.status}`);
|
|
||||||
|
|
||||||
if (result.success) {
|
|
||||||
setTimeout(() => {
|
|
||||||
setTrigger(prev => !prev);
|
|
||||||
}, pingInterval);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onError: () => {}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}, [machine, log, pingInterval, disabled]);
|
|
||||||
|
|
||||||
useEffect(pingInLoop, [trigger, pingInLoop]);
|
|
||||||
|
|
||||||
const handleWakeClick = event => {
|
|
||||||
wakeMachine();
|
|
||||||
event.stopPropagation();
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip title={t(state.on ? "Machine.PoweredOn" : "Machine.Actions.Wake")}>
|
|
||||||
<span>
|
|
||||||
<IconButton
|
|
||||||
id={`machine-${machine.machineId}-wake`}
|
|
||||||
size={"small"}
|
|
||||||
disabled={disabled || state.on}
|
|
||||||
onClick={handleWakeClick}
|
|
||||||
style={state.on ? { color: "#33cc33" } : undefined}
|
|
||||||
onFocus={event => event.stopPropagation()}
|
|
||||||
>
|
|
||||||
<PowerSettingsNew />
|
|
||||||
</IconButton>
|
|
||||||
</span>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
WakeComponent.propTypes = {
|
|
||||||
machine: PropTypes.object.isRequired,
|
|
||||||
addLog: PropTypes.func.isRequired,
|
|
||||||
disabled: PropTypes.bool
|
|
||||||
};
|
|
||||||
|
|
||||||
export default WakeComponent;
|
|
|
@ -1,20 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import MachinesContainer from "../../machines/components/MachinesContainer";
|
|
||||||
import NetworkStateProvider from "../state/NetworkStateProvider";
|
|
||||||
import { usePermissions } from "../../../hooks";
|
|
||||||
import NotAllowed from "../../../components/common/NotAllowed";
|
|
||||||
|
|
||||||
const NetworkContainer = () => {
|
|
||||||
const { loading, viewMachines } = usePermissions();
|
|
||||||
|
|
||||||
if (loading) return "";
|
|
||||||
if (!viewMachines) return <NotAllowed />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NetworkStateProvider>
|
|
||||||
<MachinesContainer />
|
|
||||||
</NetworkStateProvider>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default NetworkContainer;
|
|
|
@ -1,27 +0,0 @@
|
||||||
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;
|
|
|
@ -1,4 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
|
|
||||||
export const NetworkStateContext = React.createContext();
|
|
||||||
export const NetworkDispatchContext = React.createContext();
|
|
|
@ -1,6 +0,0 @@
|
||||||
export const initialState = {
|
|
||||||
network: {
|
|
||||||
machines: Object.assign([], { loaded: false }),
|
|
||||||
test: ""
|
|
||||||
}
|
|
||||||
};
|
|
|
@ -1,22 +0,0 @@
|
||||||
export function reducer(state, action) {
|
|
||||||
switch (action.type) {
|
|
||||||
case "onNetworkChange": {
|
|
||||||
const { prop, value } = action.payload;
|
|
||||||
return {
|
|
||||||
...state,
|
|
||||||
network: {
|
|
||||||
...state.network,
|
|
||||||
[prop]: value
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const dispatchActions = dispatch => ({
|
|
||||||
onNetworkChange: (prop, value) =>
|
|
||||||
dispatch({ type: "onNetworkChange", payload: { prop, value } })
|
|
||||||
});
|
|
|
@ -1,57 +0,0 @@
|
||||||
import React, { useState, useMemo } from "react";
|
|
||||||
import BubbleChartIcon from "@material-ui/icons/BubbleChart";
|
|
||||||
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 SystemContainer from "./system/SystemContainer";
|
|
||||||
import AppearanceContainer from "./appearance/AppearanceContainer";
|
|
||||||
import NotificationsContainer from "./notifications/NotificationsContainer";
|
|
||||||
|
|
||||||
const NavigationTabs = {
|
|
||||||
SYSTEM: "Settings.Navigation.System",
|
|
||||||
APPEARANCE: "Settings.Navigation.Appearance",
|
|
||||||
NOTIFICATIONS: "Settings.Navigation.Notifications"
|
|
||||||
};
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{
|
|
||||||
code: NavigationTabs.SYSTEM,
|
|
||||||
icon: BubbleChartIcon
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: NavigationTabs.APPEARANCE,
|
|
||||||
icon: BrushIcon
|
|
||||||
},
|
|
||||||
{
|
|
||||||
code: NavigationTabs.NOTIFICATIONS,
|
|
||||||
icon: NotificationsIcon
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
const SettingsContainer = () => {
|
|
||||||
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 && <SystemContainer />}
|
|
||||||
{tab === NavigationTabs.APPEARANCE && <AppearanceContainer />}
|
|
||||||
{tab === NavigationTabs.NOTIFICATIONS && <NotificationsContainer />}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default SettingsContainer;
|
|
|
@ -1,61 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import { useApplicationTheme } from "../../../providers/ThemeProvider";
|
|
||||||
import { Grid, Paper, FormControlLabel, Switch } from "@material-ui/core";
|
|
||||||
import LanguageContainer from "./language/LanguageContainer";
|
|
||||||
import { PaperTitle } from "components/common";
|
|
||||||
import { makeStyles } from "@material-ui/core/styles";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const useStyles = makeStyles(theme => ({
|
|
||||||
language: {
|
|
||||||
paddingLeft: theme.spacing(1)
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
const AppearanceComponent = () => {
|
|
||||||
const { isDark, onDarkModeChanged } = useApplicationTheme();
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const classes = useStyles();
|
|
||||||
|
|
||||||
const handleChange = event => {
|
|
||||||
const { checked } = event.target;
|
|
||||||
onDarkModeChanged(checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Paper variant="outlined">
|
|
||||||
<PaperTitle text={t("Settings.Appearance.Title")} />
|
|
||||||
<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;
|
|
|
@ -1,8 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import AppearanceComponent from "./AppearanceComponent";
|
|
||||||
|
|
||||||
const AppearanceContainer = () => {
|
|
||||||
return <AppearanceComponent />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AppearanceContainer;
|
|
|
@ -1,75 +0,0 @@
|
||||||
import React from "react";
|
|
||||||
import PropTypes from "prop-types";
|
|
||||||
import Flag from "react-flags";
|
|
||||||
import { IconButton, Menu, MenuItem } from "@material-ui/core";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
const LanguageComponent = ({
|
|
||||||
languageIsSet,
|
|
||||||
anchorEl,
|
|
||||||
onMenuOpen,
|
|
||||||
onLanguageChange,
|
|
||||||
onClose,
|
|
||||||
flag,
|
|
||||||
flagsPath
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const open = Boolean(anchorEl);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<IconButton
|
|
||||||
aria-controls="language-menu"
|
|
||||||
aria-haspopup="true"
|
|
||||||
onClick={onMenuOpen}
|
|
||||||
color="inherit"
|
|
||||||
size="small"
|
|
||||||
>
|
|
||||||
{languageIsSet && (
|
|
||||||
<Flag
|
|
||||||
name={flag.name}
|
|
||||||
format="png"
|
|
||||||
pngSize={32}
|
|
||||||
shiny={true}
|
|
||||||
basePath={flagsPath}
|
|
||||||
alt={flag.alt}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</IconButton>
|
|
||||||
<Menu
|
|
||||||
id="language-menu"
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
anchorOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "right"
|
|
||||||
}}
|
|
||||||
keepMounted
|
|
||||||
transformOrigin={{
|
|
||||||
vertical: "top",
|
|
||||||
horizontal: "left"
|
|
||||||
}}
|
|
||||||
open={open}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<MenuItem onClick={onLanguageChange("ro")}>
|
|
||||||
{t("Language.Romanian")}
|
|
||||||
</MenuItem>
|
|
||||||
<MenuItem onClick={onLanguageChange("en")}>
|
|
||||||
{t("Language.English")}
|
|
||||||
</MenuItem>
|
|
||||||
</Menu>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
LanguageComponent.propTypes = {
|
|
||||||
languageIsSet: PropTypes.bool.isRequired,
|
|
||||||
anchorEl: PropTypes.object,
|
|
||||||
onMenuOpen: PropTypes.func.isRequired,
|
|
||||||
onLanguageChange: PropTypes.func.isRequired,
|
|
||||||
onClose: PropTypes.func.isRequired,
|
|
||||||
flag: PropTypes.object.isRequired,
|
|
||||||
flagsPath: PropTypes.string.isRequired
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LanguageComponent;
|
|
|
@ -1,54 +0,0 @@
|
||||||
import React, { useState, useEffect } from "react";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import LanguageComponent from "./LanguageComponent";
|
|
||||||
|
|
||||||
const flagsPath = process.env.PUBLIC_URL
|
|
||||||
? `${process.env.PUBLIC_URL}/flags`
|
|
||||||
: "flags";
|
|
||||||
|
|
||||||
const LanguageContainer = () => {
|
|
||||||
const [anchorEl, setAnchorEl] = useState(null);
|
|
||||||
|
|
||||||
const { i18n } = useTranslation();
|
|
||||||
const [flag, setFlag] = useState({
|
|
||||||
name: "RO",
|
|
||||||
alt: "-"
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!i18n.language) return;
|
|
||||||
setFlag({
|
|
||||||
name: i18n.language === "en" ? "GB" : i18n.language.toUpperCase(),
|
|
||||||
alt: i18n.language
|
|
||||||
});
|
|
||||||
}, [i18n.language]);
|
|
||||||
|
|
||||||
const handleMenuOpen = event => {
|
|
||||||
setAnchorEl(event.currentTarget);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleLanguageChange = language => () => {
|
|
||||||
if (language !== i18n.language) {
|
|
||||||
i18n.changeLanguage(language);
|
|
||||||
}
|
|
||||||
setAnchorEl(null);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<LanguageComponent
|
|
||||||
languageIsSet={i18n.language ? true : false}
|
|
||||||
anchorEl={anchorEl}
|
|
||||||
onMenuOpen={handleMenuOpen}
|
|
||||||
onLanguageChange={handleLanguageChange}
|
|
||||||
onClose={handleClose}
|
|
||||||
flag={flag}
|
|
||||||
flagsPath={flagsPath}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default LanguageContainer;
|
|
|
@ -1,13 +0,0 @@
|
||||||
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;
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue