Merged PR 93: Migrate from Create React App to Vite

- Update dependencies and refactor import for localStorage utility
- Refactor component prop types from JSX.Element to React.ReactElement for consistency
- refactor: migrate from Create React App to Vite
master
Tudor Stanciu 2025-04-27 00:15:51 +00:00
parent 1adc2f6de1
commit 5ab5d0777f
91 changed files with 3607 additions and 33677 deletions

View File

@ -233,4 +233,12 @@
◾ Upgrade packages to the latest versions
</Content>
</Note>
<Note>
<Version>1.4.1</Version>
<Date>2025-04-27 01:50</Date>
<Content>
NetworkResurrector UI: Migrtion from Create React App to Vite
◾ The frontend of the application has been migrated from Create React App to Vite, a modern build tool that significantly improves the development experience and build performance.
</Content>
</Note>
</ReleaseNotes>

View File

@ -1,8 +1,8 @@
REACT_APP_TUITIO_URL=http://#######
REACT_APP_NETWORK_RESURRECTOR_API_URL=http://#######
VITE_APP_TUITIO_URL=http://#######
VITE_APP_NETWORK_RESURRECTOR_API_URL=http://#######
#600000 milliseconds = 10 minutes
REACT_APP_MACHINE_PING_INTERVAL=600000
VITE_APP_MACHINE_PING_INTERVAL=600000
#300000 milliseconds = 5 minutes
REACT_APP_MACHINE_STARTING_TIME=300000
VITE_APP_MACHINE_STARTING_TIME=300000

View File

@ -1,9 +1,9 @@
PUBLIC_URL=
REACT_APP_TUITIO_URL=https://#######
REACT_APP_NETWORK_RESURRECTOR_API_URL=https://#######
VITE_APP_TUITIO_URL=https://#######
VITE_APP_NETWORK_RESURRECTOR_API_URL=https://#######
#900000 milliseconds = 15 minutes
REACT_APP_MACHINE_PING_INTERVAL=900000
VITE_APP_MACHINE_PING_INTERVAL=900000
#300000 milliseconds = 5 minutes
REACT_APP_MACHINE_STARTING_TIME=300000
VITE_APP_MACHINE_STARTING_TIME=300000

View File

@ -1,34 +0,0 @@
{
"root": true,
"extends": [
"prettier",
"plugin:prettier/recommended",
"eslint:recommended",
"plugin:react/recommended",
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended"
],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint", "prettier", "react", "react-hooks"],
"rules": {
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"@typescript-eslint/no-non-null-assertion": "off",
"@typescript-eslint/ban-ts-comment": "off",
"@typescript-eslint/no-explicit-any": "off",
"no-debugger": "warn"
},
"ignorePatterns": ["**/public"],
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true,
"node": true
},
"globals": {
"JSX": true
}
}

View File

@ -1,41 +0,0 @@
const getCacheIdentifier = require("react-dev-utils/getCacheIdentifier");
const shouldUseSourceMap = process.env.GENERATE_SOURCEMAP !== "false";
module.exports = function override(config, webpackEnv) {
console.log("overriding webpack config...");
const isEnvDevelopment = webpackEnv === "development";
const isEnvProduction = webpackEnv === "production";
const loaders = config.module.rules[1].oneOf;
loaders.splice(loaders.length - 1, 0, {
test: /\.(js|mjs|cjs)$/,
exclude: /@babel(?:\/|\\{1,2})runtime/,
loader: require.resolve("babel-loader"),
options: {
babelrc: false,
configFile: false,
compact: false,
presets: [[require.resolve("babel-preset-react-app/dependencies"), { helpers: true }]],
cacheDirectory: true,
// See #6846 for context on why cacheCompression is disabled
cacheCompression: false,
// @remove-on-eject-begin
cacheIdentifier: getCacheIdentifier(isEnvProduction ? "production" : isEnvDevelopment && "development", [
"babel-plugin-named-asset-import",
"babel-preset-react-app",
"react-dev-utils",
"react-scripts"
]),
// @remove-on-eject-end
// Babel sourcemaps are needed for debugging into node_modules
// code. Without the options below, debuggers like VSCode
// show incorrect code and set breakpoints on the wrong lines.
sourceMaps: shouldUseSourceMap,
inputSourceMap: shouldUseSourceMap
}
});
return config;
};

View File

@ -1,5 +1,5 @@
# BUILD ENVIRONMENT
FROM node:16-slim AS builder
FROM node:20-slim AS builder
WORKDIR /app
ARG APP_SUBFOLDER=""
@ -15,7 +15,7 @@ COPY . ./
RUN if [ -z "$APP_SUBFOLDER" ]; then npm run build; else PUBLIC_URL=/${APP_SUBFOLDER}/ npm run build; fi
# PRODUCTION ENVIRONMENT
FROM node:16-slim
FROM node:20-slim
ARG APP_SUBFOLDER=""

68
frontend/eslint.config.js Normal file
View File

@ -0,0 +1,68 @@
import js from "@eslint/js";
import globals from "globals";
import react from "eslint-plugin-react";
import reactHooks from "eslint-plugin-react-hooks";
import reactRefresh from "eslint-plugin-react-refresh";
import tseslint from "typescript-eslint";
import prettier from "eslint-plugin-prettier";
export default tseslint.config(
{ ignores: ["node_modules", "dist", "build", "**/public", "setenv.js"] },
{
extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ["**/*.{js,jsx,ts,tsx}"],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser
},
settings: {
react: {
version: "detect"
}
},
plugins: {
react,
"react-hooks": reactHooks,
"react-refresh": reactRefresh,
prettier
},
rules: {
...js.configs.recommended.rules,
...react.configs.recommended.rules,
...react.configs["jsx-runtime"].rules,
...reactHooks.configs.recommended.rules,
"react-refresh/only-export-components": ["warn", { allowConstantExport: true }],
"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": [
"warn",
{
argsIgnorePattern: "^_",
varsIgnorePattern: "^_",
ignoreRestSiblings: true
}
],
"@typescript-eslint/no-explicit-any": "off",
"no-debugger": "warn",
"react-hooks/rules-of-hooks": "error",
"react-hooks/exhaustive-deps": "warn",
"prefer-const": "warn",
"react/react-in-jsx-scope": "off",
"react/prop-types": "off",
"@typescript-eslint/no-unused-expressions": "off"
}
},
{
files: ["**/*.{ts,tsx}"],
rules: {
"no-undef": "off"
}
},
{
files: ["**/*.{js,jsx}"],
rules: {
"no-undef": "warn",
"no-unused-expressions": "off"
}
}
);

35
frontend/index.html Normal file
View File

@ -0,0 +1,35 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="/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="/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="/manifest.json" />
<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="/env.js"></script>
<script type="module" src="/src/index.tsx"></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>

36577
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -2,6 +2,7 @@
"name": "network-resurrector-frontend",
"version": "1.3.1",
"description": "Frontend component of Network resurrector system",
"type": "module",
"author": {
"name": "Tudor Stanciu",
"email": "tudor.stanciu94@gmail.com",
@ -13,54 +14,57 @@
},
"private": true,
"dependencies": {
"@emotion/react": "^11.11.1",
"@emotion/styled": "^11.11.0",
"@flare/js-utils": "^1.1.0",
"@flare/tuitio-client-react": "^1.2.10",
"@mui/icons-material": "^5.14.16",
"@mui/lab": "^5.0.0-alpha.169",
"@mui/material": "^5.14.16",
"axios": "^1.6.8",
"i18next": "^22.4.15",
"i18next-browser-languagedetector": "^7.0.1",
"i18next-http-backend": "^2.2.0",
"@emotion/babel-plugin": "^11.13.5",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@flare/tuitio-client-react": "^1.3.0",
"@flare/utiliyo": "^1.2.1",
"@mui/icons-material": "^7.0.2",
"@mui/lab": "^7.0.0-beta.11",
"@mui/material": "^7.0.2",
"@vitejs/plugin-react": "^4.4.1",
"axios": "^1.9.0",
"i18next": "^25.0.1",
"i18next-browser-languagedetector": "^8.0.5",
"i18next-http-backend": "^3.0.2",
"lodash": "^4.17.21",
"moment": "^2.29.4",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^12.2.2",
"moment": "^2.30.1",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-i18next": "^15.5.1",
"react-lazylog": "^4.5.3",
"react-router-dom": "^6.10.0",
"react-toastify": "^9.1.3",
"react-router-dom": "^7.5.2",
"react-toastify": "^11.0.5",
"react-world-flags": "^1.6.0",
"swr": "^2.2.5"
"swr": "^2.3.3"
},
"devDependencies": {
"@babel/plugin-proposal-private-property-in-object": "^7.21.11",
"@types/lodash": "^4.17.7",
"@types/react": "^18.2.33",
"@types/react-dom": "^18.2.14",
"@types/react-world-flags": "^1.4.5",
"eslint": "^8.34.0",
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"prettier": "^2.8.4",
"react-app-rewired": "^2.2.1",
"typescript": "^4.9.5"
"@eslint/js": "^9.25.1",
"@types/lodash": "^4.17.16",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-world-flags": "^1.6.0",
"eslint": "^9.25.1",
"eslint-plugin-prettier": "^5.2.6",
"eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-react-refresh": "^0.4.20",
"globals": "^16.0.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.31.0",
"vite": "^6.3.3",
"vite-plugin-checker": "^0.9.1",
"vite-plugin-eslint": "^1.8.1",
"vite-tsconfig-paths": "^5.1.4"
},
"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-app-rewired eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
"start": "vite",
"build": "tsc && vite build",
"lint": "eslint .",
"preview": "vite preview",
"test": "jest"
},
"browserslist": {
"production": [

View File

@ -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>

View File

@ -2,13 +2,13 @@
const fs = require("fs");
const path = require("path");
const prefix = "REACT_APP_";
const publicUrl = process.env.PUBLIC_URL || "";
const prefix = "VITE_APP_";
const publicUrl = import.meta.env.PUBLIC_URL || "";
const scriptPath = path.join("./application", publicUrl, "env.js");
function generateScriptContent() {
const prefixRegex = new RegExp(`^${prefix}`);
const env = process.env;
const env = import.meta.env;
const config = Object.keys(env)
.filter(key => prefixRegex.test(key))
.reduce((c, key) => Object.assign({}, c, { [key]: env[key] }), {});
@ -24,6 +24,4 @@ function saveScriptContent(scriptContents) {
console.log("Setting environment variables...");
const scriptContent = generateScriptContent();
saveScriptContent(scriptContent);
console.log(
`Updated ${scriptPath} with ${prefix}* environment variables: ${scriptContent}.`
);
console.log(`Updated ${scriptPath} with ${prefix}* environment variables: ${scriptContent}.`);

View File

@ -1,5 +1,6 @@
import React from "react";
import { UserPermissionsProvider, SensitiveInfoProvider } from "../providers";
import { SensitiveInfoProvider } from "../providers";
import { UserPermissionsProvider } from "../units/permissions";
import AppLayout from "./layout/AppLayout";
const App = () => {

View File

@ -4,7 +4,7 @@ import { BrowserRouter, Navigate, Route, Routes, useLocation } from "react-route
import { useTuitioToken } from "@flare/tuitio-client-react";
import LoginContainer from "../features/login/components/LoginContainer";
const PrivateRoute = ({ children }: { children: JSX.Element }): JSX.Element => {
const PrivateRoute = ({ children }: { children: React.ReactElement }): React.ReactElement => {
const { valid } = useTuitioToken();
const location = useLocation();
return valid ? (
@ -20,7 +20,7 @@ const PrivateRoute = ({ children }: { children: JSX.Element }): JSX.Element => {
);
};
const PublicRoute = ({ children }: { children: JSX.Element }): JSX.Element => {
const PublicRoute = ({ children }: { children: React.ReactElement }): React.ReactElement => {
const location = useLocation();
const { valid } = useTuitioToken();
const to = useMemo(() => {
@ -43,7 +43,7 @@ const PublicRoute = ({ children }: { children: JSX.Element }): JSX.Element => {
const AppRouter: React.FC = () => {
return (
<BrowserRouter basename={process.env.PUBLIC_URL || ""}>
<BrowserRouter basename={import.meta.env.PUBLIC_URL || ""}>
<Routes>
<Route path="/" element={<Navigate to="/dashboard" />} />
<Route

View File

@ -1,5 +1,4 @@
import React from "react";
import PropTypes from "prop-types";
import { Typography, Box } from "@mui/material";
const styles = {
@ -20,12 +19,18 @@ const styles = {
}
};
const PageTitle = ({ text, toolBar, navigation }) => {
type PageTitleProps = {
text: string;
toolBar?: React.ReactNode;
navigation?: React.ReactNode;
};
const PageTitle: React.FC<PageTitleProps> = ({ text, toolBar, navigation }) => {
return (
<Box sx={styles.box}>
{navigation && navigation}
<Box sx={styles.title}>
<Typography sx={styles.titleText} variant="h3" size="sm">
<Typography sx={styles.titleText} variant="h3">
{text}
</Typography>
</Box>
@ -34,10 +39,4 @@ const PageTitle = ({ text, toolBar, navigation }) => {
);
};
PageTitle.propTypes = {
text: PropTypes.string.isRequired,
toolBar: PropTypes.node,
navigation: PropTypes.node
};
export default PageTitle;

View File

@ -4,7 +4,7 @@ import { Icon as MuiIcon, IconProps } from "@mui/material";
interface Props extends IconProps {
code?: string | null;
fallback?: JSX.Element;
fallback?: React.ReactElement;
}
const DynamicIcon: React.FC<Props> = ({ code, fallback, ...res }) => {

View File

@ -1,7 +1,7 @@
import React from "react";
import { IconButton } from "@mui/material";
import { Brightness2 as MoonIcon, WbSunny as SunIcon } from "@mui/icons-material";
import { useApplicationTheme } from "../../providers/ThemeProvider";
import { useApplicationTheme } from "../../hooks";
const LightDarkToggle = () => {
const { isDark, onDarkModeChanged } = useApplicationTheme();

View File

@ -5,7 +5,7 @@ type MenuItem = {
code: string;
name: string;
route: string;
icon: JSX.Element;
icon: React.ReactElement;
order: number;
subMenus?: MenuItem[];
};

View File

@ -0,0 +1,6 @@
const COLOR_SCHEME = {
LIGHT: "light",
DARK: "dark"
};
export { COLOR_SCHEME };

View File

@ -7,13 +7,13 @@ const ReleaseNoteSummary = ({ releaseNote, collapsed }) => {
const { t } = useTranslation();
return (
<Grid container>
<Grid item xs={6} sm={2} md={2}>
<Grid container flexGrow={1}>
<Grid size={{ xs: 12, sm: 2 }}>
<Typography variant={collapsed ? "subtitle2" : "h6"}>
{`${t("About.ReleaseNotes.Version")}: ${releaseNote.version}`}
</Typography>
</Grid>
<Grid item xs={6} sm={2} md={collapsed ? 2 : 4}>
<Grid size={{ 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" }
@ -21,7 +21,7 @@ const ReleaseNoteSummary = ({ releaseNote, collapsed }) => {
</Typography>
</Grid>
{collapsed && (
<Grid item xs={12} sm={8} md={8}>
<Grid size={{ xs: 12, sm: 8, md: 6 }}>
<Typography variant="body2">{releaseNote.notes[0]}</Typography>
</Grid>
)}

View File

@ -43,7 +43,7 @@ const SystemVersionComponent: React.FC<Props> = ({ data }) => {
const frontend = t("DATE_FORMAT", {
date: {
value: process.env.APP_DATE ?? new Date(),
value: import.meta.env.APP_DATE ?? new Date(),
format
}
});
@ -109,7 +109,7 @@ const SystemVersionComponent: React.FC<Props> = ({ data }) => {
primary={
<VersionLabel>
{t("About.System.Version.Frontend", {
version: process.env.APP_VERSION ?? packageData.version
version: import.meta.env.APP_VERSION ?? packageData.version
})}
</VersionLabel>
}

View File

@ -58,7 +58,7 @@ type GridCellProps = {
const GridCell: React.FC<GridCellProps> = ({ label, value }) => {
const { mask } = useSensitiveInfo();
return (
<Grid item xs={12} md={6} lg={3}>
<Grid size={{ xs: 12, md: 6, lg: 3 }}>
<DataLabel label={label} data={mask(value)} />
</Grid>
);
@ -78,12 +78,13 @@ const MachineAccordion: React.FC<Props> = ({ machine, actions, logs, addLog }) =
<AccordionSummary aria-controls={`machine-${machine.machineId}-summary`} id={`machine-${machine.machineId}`}>
<Grid
container
flex={1}
sx={{
justifyContent: "center",
alignItems: "center"
}}
>
<Grid item xs={11}>
<Grid size={11}>
<Grid container>
<GridCell label={t("Machine.FullName")} value={machine.fullMachineName} />
<GridCell label={t("Machine.Name")} value={machine.machineName} />
@ -91,7 +92,7 @@ const MachineAccordion: React.FC<Props> = ({ machine, actions, logs, addLog }) =
<GridCell label={t("Machine.MAC")} value={machine.macAddress} />
</Grid>
</Grid>
<Grid item xs={1} style={{ textAlign: "right" }}>
<Grid size={1} style={{ textAlign: "right" }}>
<ActionsGroup machine={machine} actions={actions} addLog={addLog} />
</Grid>
</Grid>

View File

@ -1,7 +1,7 @@
import React, { useState, useCallback } from "react";
import MachineTableRow from "./MachineTableRow";
import MachineAccordion from "./MachineAccordion";
import { ViewModes } from "./ViewModeSelection";
import { ViewModes } from "../constants";
import { blip } from "../../../utils";
import { LastPage, RotateLeft, Launch, Stop } from "@mui/icons-material";
import { useTranslation } from "react-i18next";

View File

@ -3,7 +3,8 @@ import { NetworkStateContext, NetworkDispatchContext } from "../../network/state
import MachinesListComponent from "./MachinesListComponent";
import PageTitle from "../../../components/common/PageTitle";
import { useTranslation } from "react-i18next";
import ViewModeSelection, { ViewModes } from "./ViewModeSelection";
import ViewModeSelection from "./ViewModeSelection";
import { ViewModes } from "../constants";
import { endpoints } from "../../../utils/api";
import { useSWR, fetcher } from "units/swr";
import { blip } from "utils";

View File

@ -4,7 +4,7 @@ import { Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from
import Paper from "@mui/material/Paper";
import MachineContainer from "./MachineContainer";
import { useTranslation } from "react-i18next";
import { ViewModes } from "./ViewModeSelection";
import { ViewModes } from "../constants";
const MachinesList = ({ machines, viewMode }) => {
return (

View File

@ -5,11 +5,7 @@ import ViewListIcon from "@mui/icons-material/ViewList";
import { ToggleButtonGroup, ToggleButton } from "@mui/material";
import { Tooltip } from "@mui/material";
import { useTranslation } from "react-i18next";
export const ViewModes = {
TABLE: "table",
ACCORDION: "accordion"
};
import { ViewModes } from "../constants";
const ViewModeSelection = ({ initialMode, callback }) => {
const [state, setState] = useState({

View File

@ -2,7 +2,7 @@ import React from "react";
import PropTypes from "prop-types";
import { IconButton, Tooltip } from "@mui/material";
const ActionButton = React.forwardRef(props => {
const ActionButton = React.forwardRef((props, ref) => {
const { action, machine, callback, disabled } = props;
const id = `machine-item-${machine.machineId}-${action.code}`;
const handleActionClick = event => {
@ -21,6 +21,7 @@ const ActionButton = React.forwardRef(props => {
onFocus={event => event.stopPropagation()}
onClick={handleActionClick}
disabled={disabled}
ref={ref}
>
<action.icon />
</IconButton>
@ -36,7 +37,8 @@ ActionButton.propTypes = {
action: PropTypes.shape({
code: PropTypes.string.isRequired,
tooltip: PropTypes.string.isRequired,
effect: PropTypes.func.isRequired
effect: PropTypes.func.isRequired,
icon: PropTypes.elementType.isRequired
}).isRequired,
callback: PropTypes.func,
disabled: PropTypes.bool

View File

@ -13,8 +13,8 @@ const initialState = { on: false };
const defaultPingInterval = 1200000; //20 minutes
const defaultStartingTime = 300000; //5 minutes
const pingInterval = parseInt(process.env.REACT_APP_MACHINE_PING_INTERVAL ?? "") || defaultPingInterval;
const startingTime = parseInt(process.env.REACT_APP_MACHINE_STARTING_TIME ?? "") || defaultStartingTime;
const pingInterval = parseInt(import.meta.env.VITE_APP_MACHINE_PING_INTERVAL ?? "") || defaultPingInterval;
const startingTime = parseInt(import.meta.env.VITE_APP_MACHINE_STARTING_TIME ?? "") || defaultStartingTime;
type Props = {
machine: Machine;

View File

@ -0,0 +1,4 @@
export const ViewModes = {
TABLE: "table",
ACCORDION: "accordion"
};

View File

@ -4,7 +4,7 @@ import NetworkStateProvider from "../state/NetworkStateProvider";
import { usePermissions } from "../../../hooks";
import NotAllowed from "../../../components/common/NotAllowed";
const NetworkContainer = (): JSX.Element | null => {
const NetworkContainer = (): React.ReactElement | null => {
const { loading, viewMachines } = usePermissions();
if (loading) return null;

View File

@ -1,5 +1,5 @@
import React from "react";
import { useApplicationTheme } from "../../../providers/ThemeProvider";
import { useApplicationTheme } from "hooks";
import { Grid, Paper, FormControlLabel, Switch, Box } from "@mui/material";
import LanguageContainer from "./language/LanguageContainer";
import { PaperTitle } from "components/common";
@ -24,7 +24,7 @@ const AppearanceComponent = () => {
<Paper variant="outlined">
<PaperTitle text={t("Settings.Appearance.Title")} />
<Grid container spacing={0}>
<Grid item xs={12} sm={6} md={4} lg={3}>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
<FormControlLabel
value="start"
control={<Switch checked={isDark} onChange={handleChange} color="secondary" name="dark-mode-switch" />}
@ -32,7 +32,7 @@ const AppearanceComponent = () => {
labelPlacement="start"
/>
</Grid>
<Grid item xs={12} sm={6} md={4} lg={3}>
<Grid size={{ xs: 12, sm: 6, md: 4, lg: 3 }}>
<FormControlLabel
value="start"
control={

View File

@ -156,9 +156,9 @@ const ContactOptions = ({ contactOptions, userName }) => {
const chunks = useMemo(() => sliceContactOptions(sorted), [sorted]);
return (
<Grid container>
<Grid container flex={1}>
{chunks.map((chunk, index) => (
<Grid item xs={12} md={6} key={index}>
<Grid size={{ xs: 12, md: 6 }} key={index}>
<ContactOptionList options={chunk} />
</Grid>
))}

View File

@ -12,7 +12,7 @@ const SecurityComponent = ({ userGroups, userRoles }) => {
return (
<Paper>
<Grid container>
<Grid item xs={12} md={6}>
<Grid size={{ xs: 12, md: 6 }}>
<div style={styles.paper}>
<Typography gutterBottom variant="body1">
{t("User.Profile.Security.UserGroups")}
@ -24,7 +24,7 @@ const SecurityComponent = ({ userGroups, userRoles }) => {
</div>
</div>
</Grid>
<Grid item xs={12} md={6}>
<Grid size={{ xs: 12, md: 6 }}>
<div style={styles.paper}>
<Typography gutterBottom variant="body1">
{t("User.Profile.Security.UserRoles")}

View File

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

View File

@ -0,0 +1,15 @@
import { useContext } from "react";
import { ApplicationThemeContext } from "../providers/contexts";
import { COLOR_SCHEME } from "../constants";
const useApplicationTheme = () => {
const { state, actions } = useContext(ApplicationThemeContext);
const { onColorSchemeChanged } = actions;
const { scheme } = state;
const onDarkModeChanged = active => onColorSchemeChanged(active ? COLOR_SCHEME.DARK : COLOR_SCHEME.LIGHT);
return { isDark: scheme === COLOR_SCHEME.DARK, onDarkModeChanged };
};
export default useApplicationTheme;

View File

@ -0,0 +1,24 @@
import { useContext } from "react";
import { obfuscate } from "../utils/obfuscateStrings";
import { SensitiveInfoContext } from "../providers/contexts";
const useSensitiveInfo = () => {
const { state, actions } = useContext(SensitiveInfoContext);
const { enabled } = state;
const { onSensitiveInfoEnabled } = actions;
const mask = text => {
if (!enabled) return text;
return obfuscate(text, "◾");
};
const maskElements = list => {
if (!enabled) return list;
const maskedList = list.map(z => obfuscate(z, "◻️"));
return maskedList;
};
return { enabled, onSensitiveInfoEnabled, mask, maskElements };
};
export default useSensitiveInfo;

View File

@ -1,5 +1,5 @@
import React, { Suspense } from "react";
import ReactDOM from "react-dom/client";
import { createRoot } from "react-dom/client";
import ThemeProvider from "./providers/ThemeProvider";
import { CssBaseline } from "@mui/material";
import AppRouter from "./components/AppRouter";
@ -8,10 +8,22 @@ import { ToastProvider } from "./providers";
import env from "./utils/env";
import "./utils/i18n";
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
const domNode = document.getElementById("root");
if (!domNode) {
throw new Error("Could not find root element");
}
const root = createRoot(domNode, {
onUncaughtError: (error, errorInfo) => {
console.error(error, errorInfo);
},
onCaughtError: (error, errorInfo) => {
console.error(error, errorInfo);
}
});
root.render(
<TuitioProvider tuitioUrl={env.REACT_APP_TUITIO_URL}>
<TuitioProvider tuitioUrl={env.VITE_APP_TUITIO_URL}>
<ThemeProvider>
<CssBaseline />
<Suspense fallback={<div>Loading...</div>}>

View File

@ -1,8 +1,6 @@
import React, { useReducer, useMemo, useContext } from "react";
import React, { useReducer, useMemo } from "react";
import PropTypes from "prop-types";
import { obfuscate } from "../utils/obfuscateStrings";
const SensitiveInfoContext = React.createContext();
import { SensitiveInfoContext } from "./contexts";
const initialState = {
enabled: false
@ -26,25 +24,6 @@ const dispatchActions = dispatch => ({
onSensitiveInfoEnabled: enabled => dispatch({ type: "onSensitiveInfoEnabled", payload: { enabled } })
});
const useSensitiveInfo = () => {
const { state, actions } = useContext(SensitiveInfoContext);
const { enabled } = state;
const { onSensitiveInfoEnabled } = actions;
const mask = text => {
if (!enabled) return text;
return obfuscate(text, "◾");
};
const maskElements = list => {
if (!enabled) return list;
const maskedList = list.map(z => obfuscate(z, "◻️"));
return maskedList;
};
return { enabled, onSensitiveInfoEnabled, mask, maskElements };
};
const SensitiveInfoProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const actions = useMemo(() => dispatchActions(dispatch), [dispatch]);
@ -65,5 +44,5 @@ SensitiveInfoProvider.propTypes = {
children: PropTypes.node.isRequired
};
export { SensitiveInfoProvider, useSensitiveInfo };
export { SensitiveInfoProvider };
export default SensitiveInfoProvider;

View File

@ -1,17 +1,13 @@
import React, { useReducer, useMemo, useContext } from "react";
import React, { useReducer, useMemo } from "react";
import PropTypes from "prop-types";
import { ThemeProvider as MuiThemeProvider } from "@mui/material/styles";
import { localStorage } from "@flare/js-utils";
import { localStorage } from "@flare/utiliyo";
import { getThemes } from "../units/themes";
import { ApplicationThemeContext } from "./contexts";
import { COLOR_SCHEME } from "../constants";
const ApplicationThemeContext = React.createContext();
const LOCAL_STORAGE_COLOR_SCHEME_KEY = "network-resurrector-color-scheme";
const COLOR_SCHEME = {
LIGHT: "light",
DARK: "dark"
};
const colorScheme = localStorage.getItem(LOCAL_STORAGE_COLOR_SCHEME_KEY);
const prefersDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
@ -40,16 +36,6 @@ const dispatchActions = dispatch => ({
}
});
const useApplicationTheme = () => {
const { state, actions } = useContext(ApplicationThemeContext);
const { onColorSchemeChanged } = actions;
const { scheme } = state;
const onDarkModeChanged = active => onColorSchemeChanged(active ? COLOR_SCHEME.DARK : COLOR_SCHEME.LIGHT);
return { isDark: scheme === COLOR_SCHEME.DARK, onDarkModeChanged };
};
const ThemeProvider = ({ children }) => {
const [state, dispatch] = useReducer(reducer, initialState);
const actions = useMemo(() => dispatchActions(dispatch), [dispatch]);
@ -71,5 +57,5 @@ ThemeProvider.propTypes = {
children: PropTypes.node.isRequired
};
export { ThemeProvider, ApplicationThemeContext, useApplicationTheme };
export { ThemeProvider };
export default ThemeProvider;

View File

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

View File

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

View File

@ -0,0 +1,37 @@
import React, { useState, useMemo, ReactNode } from "react";
import { endpoints } from "../../utils/api";
import { PermissionsDto } from "types/dtos";
import { useSWR, fetcher } from "units/swr";
import { blip } from "utils";
type UserPermissionsContextPayload = {
permissions: string[];
loading: boolean;
};
const initialState: UserPermissionsContextPayload = {
permissions: [],
loading: true
};
const UserPermissionsContext = React.createContext<UserPermissionsContextPayload>(initialState);
type Props = {
children: ReactNode;
};
const UserPermissionsProvider: React.FC<Props> = ({ children }) => {
const [state, setState] = useState<UserPermissionsContextPayload>(initialState);
const url = useMemo(() => (state.permissions?.length ? null : endpoints.security.permissions), [state.permissions]);
useSWR<PermissionsDto, Error>(url, fetcher, {
revalidateOnFocus: false,
onError: err => blip.error(err.message),
onSuccess: data => setState({ ...data, loading: false })
});
return <UserPermissionsContext.Provider value={state}>{children}</UserPermissionsContext.Provider>;
};
export { UserPermissionsProvider, UserPermissionsContext };
export default UserPermissionsProvider;

View File

@ -1,8 +1,5 @@
import React, { useState, useContext, useMemo, ReactNode } from "react";
import { endpoints } from "../utils/api";
import { PermissionsDto } from "types/dtos";
import { useSWR, fetcher } from "units/swr";
import { blip } from "utils";
import { useContext, useMemo } from "react";
import { UserPermissionsContext } from "./UserPermissionsProvider";
const permissionCodes = {
VIEW_DASHBOARD: "VIEW_DASHBOARD",
@ -15,16 +12,6 @@ const permissionCodes = {
SYSTEM_ADMINISTRATION: "SYSTEM_ADMINISTRATION"
};
type UserPermissionsContextPayload = {
permissions: string[];
loading: boolean;
};
const initialState: UserPermissionsContextPayload = {
permissions: [],
loading: true
};
const getPermissionFlags = (permissions: string[]) => {
const viewDashboard = permissions.includes(permissionCodes.VIEW_DASHBOARD) ?? false;
const manageUsers = permissions.includes(permissionCodes.MANAGE_USERS) ?? false;
@ -56,30 +43,10 @@ const getPermissionFlags = (permissions: string[]) => {
};
};
const UserPermissionsContext = React.createContext<UserPermissionsContextPayload>(initialState);
const usePermissions = () => {
const { permissions, loading } = useContext(UserPermissionsContext);
const flags = useMemo(() => getPermissionFlags(permissions), [permissions]);
return { loading, ...flags };
};
type Props = {
children: ReactNode;
};
const UserPermissionsProvider: React.FC<Props> = ({ children }) => {
const [state, setState] = useState<UserPermissionsContextPayload>(initialState);
const url = useMemo(() => (state.permissions?.length ? null : endpoints.security.permissions), [state.permissions]);
useSWR<PermissionsDto, Error>(url, fetcher, {
revalidateOnFocus: false,
onError: err => blip.error(err.message),
onSuccess: data => setState({ ...data, loading: false })
});
return <UserPermissionsContext.Provider value={state}>{children}</UserPermissionsContext.Provider>;
};
export { UserPermissionsProvider, usePermissions };
export default UserPermissionsProvider;
export { usePermissions };

View File

@ -0,0 +1,4 @@
import UserPermissionsProvider from "./UserPermissionsProvider";
export * from "./hooks";
export { UserPermissionsProvider };

View File

@ -1,6 +1,6 @@
import env from "../utils/env";
const apiHost = env.REACT_APP_NETWORK_RESURRECTOR_API_URL;
const apiHost = env.VITE_APP_NETWORK_RESURRECTOR_API_URL;
const networkRoute = `${apiHost}/network`;
const systemRoute = `${apiHost}/system`;

View File

@ -1,4 +1,4 @@
const runtimeEnv = window.env;
const compileEnv = process.env;
const compileEnv = import.meta.env;
const env = { ...compileEnv, ...runtimeEnv };
export default env;

View File

@ -37,8 +37,8 @@ i18n
if (format === "intlTimeFromX") {
if (value && moment(value.start).isValid()) {
let startDate = moment(value.start);
let endDate = moment(value.end);
const startDate = moment(value.start);
const endDate = moment(value.end);
return moment(endDate).from(startDate, true);
}
return "";
@ -46,9 +46,9 @@ i18n
if (format === "intlHoursFromX") {
if (value && moment(value.start).isValid()) {
let startDate = moment(value.start);
let endDate = moment(value.end);
let span = moment.duration(endDate - startDate);
const startDate = moment(value.start);
const endDate = moment(value.end);
const span = moment.duration(endDate - startDate);
return `${parseInt(span.asHours(), 10)}h ${parseInt(span.asMinutes() % 60, 10)}m`;
}
return "";
@ -77,7 +77,7 @@ i18n
}
},
backend: {
loadPath: `${process.env.PUBLIC_URL || ""}/locales/{{lng}}/{{ns}}.json`
loadPath: `${import.meta.env.PUBLIC_URL || ""}/locales/{{lng}}/{{ns}}.json`
}
},
() => {

1
frontend/src/vite-env.d.ts vendored Normal file
View File

@ -0,0 +1 @@
/// <reference types="vite/client" />

View File

@ -18,7 +18,9 @@
"jsx": "react-jsx",
"noUnusedLocals": false,
"noUnusedParameters": true,
"allowUnreachableCode": false
"allowUnreachableCode": false,
"types": ["vite/client"]
},
"include": ["src"]
"include": ["src"],
"exclude": ["node_modules"]
}

30
frontend/vite.config.mts Normal file
View File

@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import viteTsconfigPaths from "vite-tsconfig-paths";
import eslintPlugin from "vite-plugin-eslint";
import checker from "vite-plugin-checker";
export default defineConfig({
// depending on your application, base can also be "/"
base: "/",
plugins: [
react({
jsxImportSource: "@emotion/react",
babel: {
plugins: ["@emotion/babel-plugin"]
}
}),
viteTsconfigPaths(),
eslintPlugin({
cache: false
}),
checker({ typescript: true })
],
server: {
open: true,
port: 3000
},
build: {
outDir: "build"
}
});