mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
Warn user in WebUI of unsaved changes #911
This commit is contained in:
@@ -1,5 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
<<<<<<< HEAD
|
||||||
"adapter": "react",
|
"adapter": "react",
|
||||||
"baseLocale": "pl",
|
"baseLocale": "pl",
|
||||||
"$schema": "https://unpkg.com/typesafe-i18n@5.22.0/schema/typesafe-i18n.json"
|
"$schema": "https://unpkg.com/typesafe-i18n@5.22.0/schema/typesafe-i18n.json"
|
||||||
}
|
}
|
||||||
|
=======
|
||||||
|
"adapter": "react",
|
||||||
|
"baseLocale": "pl",
|
||||||
|
"$schema": "https://unpkg.com/typesafe-i18n@5.24.0/schema/typesafe-i18n.json"
|
||||||
|
}
|
||||||
|
>>>>>>> 941146db (Warn user in WebUI of unsaved changes #911)
|
||||||
|
|||||||
12725
interface/package-lock.json
generated
12725
interface/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,11 @@
|
|||||||
"react-icons": "^4.7.1",
|
"react-icons": "^4.7.1",
|
||||||
"react-scripts": "^5.0.1",
|
"react-scripts": "^5.0.1",
|
||||||
"sockette": "^2.0.6",
|
"sockette": "^2.0.6",
|
||||||
|
<<<<<<< HEAD
|
||||||
"typesafe-i18n": "5.22.0",
|
"typesafe-i18n": "5.22.0",
|
||||||
|
=======
|
||||||
|
"typesafe-i18n": "^5.24.0",
|
||||||
|
>>>>>>> 941146db (Warn user in WebUI of unsaved changes #911)
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.9.5",
|
||||||
"axios": "^1.3.2",
|
"axios": "^1.3.2",
|
||||||
"react-router-dom": "^6.8.0"
|
"react-router-dom": "^6.8.0"
|
||||||
|
|||||||
@@ -4,8 +4,6 @@ import { SnackbarProvider } from 'notistack';
|
|||||||
import { IconButton } from '@mui/material';
|
import { IconButton } from '@mui/material';
|
||||||
import CloseIcon from '@mui/icons-material/Close';
|
import CloseIcon from '@mui/icons-material/Close';
|
||||||
|
|
||||||
import { FeaturesLoader } from './contexts/features';
|
|
||||||
|
|
||||||
import CustomTheme from './CustomTheme';
|
import CustomTheme from './CustomTheme';
|
||||||
import AppRouting from './AppRouting';
|
import AppRouting from './AppRouting';
|
||||||
|
|
||||||
@@ -44,9 +42,9 @@ const App: FC = () => {
|
|||||||
</IconButton>
|
</IconButton>
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<FeaturesLoader>
|
{/* <FeaturesLoader> */}
|
||||||
<AppRouting />
|
<AppRouting />
|
||||||
</FeaturesLoader>
|
{/* </FeaturesLoader> */}
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</CustomTheme>
|
</CustomTheme>
|
||||||
</TypesafeI18n>
|
</TypesafeI18n>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { FC, useContext, useEffect } from 'react';
|
import { FC, useContext, useEffect } from 'react';
|
||||||
import { Navigate, Routes, Route, useLocation } from 'react-router-dom';
|
|
||||||
|
import { Route, Routes, Navigate, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
import { useSnackbar, VariantType } from 'notistack';
|
import { useSnackbar, VariantType } from 'notistack';
|
||||||
|
|
||||||
import { useI18nContext } from './i18n/i18n-react';
|
import { useI18nContext } from './i18n/i18n-react';
|
||||||
|
|
||||||
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
||||||
import { FeaturesContext } from './contexts/features';
|
|
||||||
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
||||||
|
|
||||||
import SignIn from './SignIn';
|
import SignIn from './SignIn';
|
||||||
@@ -27,6 +28,7 @@ const RootRedirect: FC<SecurityRedirectProps> = ({ message, variant, signOut })
|
|||||||
return <Navigate to="/" />;
|
return <Navigate to="/" />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// TODO still need this?
|
||||||
export const RemoveTrailingSlashes = () => {
|
export const RemoveTrailingSlashes = () => {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
return (
|
return (
|
||||||
@@ -42,7 +44,6 @@ export const RemoveTrailingSlashes = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppRouting: FC = () => {
|
const AppRouting: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,16 +52,14 @@ const AppRouting: FC = () => {
|
|||||||
<Routes>
|
<Routes>
|
||||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
||||||
{features.security && (
|
<Route
|
||||||
<Route
|
path="/"
|
||||||
path="/"
|
element={
|
||||||
element={
|
<RequireUnauthenticated>
|
||||||
<RequireUnauthenticated>
|
<SignIn />
|
||||||
<SignIn />
|
</RequireUnauthenticated>
|
||||||
</RequireUnauthenticated>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route
|
<Route
|
||||||
path="/*"
|
path="/*"
|
||||||
element={
|
element={
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { FC, useCallback, useContext, useEffect } from 'react';
|
import { FC, useCallback, useEffect } from 'react';
|
||||||
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
import { Navigate, Routes, Route, useNavigate, useLocation } from 'react-router-dom';
|
||||||
import { AxiosError } from 'axios';
|
import { AxiosError } from 'axios';
|
||||||
|
|
||||||
import { FeaturesContext } from './contexts/features';
|
|
||||||
import * as AuthenticationApi from './api/authentication';
|
import * as AuthenticationApi from './api/authentication';
|
||||||
import { PROJECT_PATH } from './api/env';
|
import { PROJECT_PATH } from './api/env';
|
||||||
import { AXIOS } from './api/endpoints';
|
import { AXIOS } from './api/endpoints';
|
||||||
@@ -18,7 +17,6 @@ import System from './framework/system/System';
|
|||||||
import Security from './framework/security/Security';
|
import Security from './framework/security/Security';
|
||||||
|
|
||||||
const AuthenticatedRouting: FC = () => {
|
const AuthenticatedRouting: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
@@ -41,23 +39,21 @@ const AuthenticatedRouting: FC = () => {
|
|||||||
return (
|
return (
|
||||||
<Layout>
|
<Layout>
|
||||||
<Routes>
|
<Routes>
|
||||||
{features.project && <Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />}
|
<Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />
|
||||||
<Route path="/network/*" element={<NetworkConnection />} />
|
<Route path="/network/*" element={<NetworkConnection />} />
|
||||||
<Route path="/ap/*" element={<AccessPoint />} />
|
<Route path="/ap/*" element={<AccessPoint />} />
|
||||||
{features.ntp && <Route path="/ntp/*" element={<NetworkTime />} />}
|
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||||
{features.mqtt && <Route path="/mqtt/*" element={<Mqtt />} />}
|
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||||
{features.security && (
|
<Route
|
||||||
<Route
|
path="/security/*"
|
||||||
path="/security/*"
|
element={
|
||||||
element={
|
<RequireAdmin>
|
||||||
<RequireAdmin>
|
<Security />
|
||||||
<Security />
|
</RequireAdmin>
|
||||||
</RequireAdmin>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route path="/system/*" element={<System />} />
|
<Route path="/system/*" element={<System />} />
|
||||||
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute(features)} />} />
|
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute} />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Layout>
|
</Layout>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as H from 'history';
|
|||||||
import jwtDecode from 'jwt-decode';
|
import jwtDecode from 'jwt-decode';
|
||||||
import { Path } from 'react-router-dom';
|
import { Path } from 'react-router-dom';
|
||||||
|
|
||||||
import { Features, Me, SignInRequest, SignInResponse } from '../types';
|
import { Me, SignInRequest, SignInResponse } from '../types';
|
||||||
|
|
||||||
import { ACCESS_TOKEN, AXIOS } from './endpoints';
|
import { ACCESS_TOKEN, AXIOS } from './endpoints';
|
||||||
import { PROJECT_PATH } from './env';
|
import { PROJECT_PATH } from './env';
|
||||||
@@ -11,7 +11,7 @@ import { PROJECT_PATH } from './env';
|
|||||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||||
export const SIGN_IN_SEARCH = 'loginSearch';
|
export const SIGN_IN_SEARCH = 'loginSearch';
|
||||||
|
|
||||||
export const getDefaultRoute = (features: Features) => (features.project ? `/${PROJECT_PATH}` : '/wifi');
|
export const getDefaultRoute = `/${PROJECT_PATH}`;
|
||||||
|
|
||||||
export function verifyAuthorization(): AxiosPromise<void> {
|
export function verifyAuthorization(): AxiosPromise<void> {
|
||||||
return AXIOS.get('/verifyAuthorization');
|
return AXIOS.get('/verifyAuthorization');
|
||||||
@@ -40,12 +40,12 @@ export function clearLoginRedirect() {
|
|||||||
getStorage().removeItem(SIGN_IN_SEARCH);
|
getStorage().removeItem(SIGN_IN_SEARCH);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fetchLoginRedirect(features: Features): Partial<Path> {
|
export function fetchLoginRedirect(): Partial<Path> {
|
||||||
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
const signInPathname = getStorage().getItem(SIGN_IN_PATHNAME);
|
||||||
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
const signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||||
clearLoginRedirect();
|
clearLoginRedirect();
|
||||||
return {
|
return {
|
||||||
pathname: signInPathname || getDefaultRoute(features),
|
pathname: signInPathname || `/${PROJECT_PATH}`,
|
||||||
search: (signInPathname && signInSearch) || undefined
|
search: (signInPathname && signInSearch) || undefined
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,3 +6,4 @@ export * from './upload';
|
|||||||
export { default as SectionContent } from './SectionContent';
|
export { default as SectionContent } from './SectionContent';
|
||||||
export { default as ButtonRow } from './ButtonRow';
|
export { default as ButtonRow } from './ButtonRow';
|
||||||
export { default as MessageBox } from './MessageBox';
|
export { default as MessageBox } from './MessageBox';
|
||||||
|
export { default as BlockNavigation } from './routing/BlockNavigation';
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { FC, useContext } from 'react';
|
import { FC } from 'react';
|
||||||
|
|
||||||
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
import { AppBar, Box, IconButton, Toolbar, Typography } from '@mui/material';
|
||||||
import MenuIcon from '@mui/icons-material/Menu';
|
import MenuIcon from '@mui/icons-material/Menu';
|
||||||
|
|
||||||
import LayoutAuthMenu from './LayoutAuthMenu';
|
import LayoutAuthMenu from './LayoutAuthMenu';
|
||||||
|
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
|
|
||||||
export const DRAWER_WIDTH = 240;
|
export const DRAWER_WIDTH = 240;
|
||||||
|
|
||||||
interface LayoutAppBarProps {
|
interface LayoutAppBarProps {
|
||||||
@@ -15,8 +13,6 @@ interface LayoutAppBarProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppBar
|
<AppBar
|
||||||
position="fixed"
|
position="fixed"
|
||||||
@@ -41,7 +37,7 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
|||||||
{title}
|
{title}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Box flexGrow={1} />
|
<Box flexGrow={1} />
|
||||||
{features.security && <LayoutAuthMenu />}
|
<LayoutAuthMenu />
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
|||||||
import LockIcon from '@mui/icons-material/Lock';
|
import LockIcon from '@mui/icons-material/Lock';
|
||||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||||
|
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
import ProjectMenu from '../../project/ProjectMenu';
|
import ProjectMenu from '../../project/ProjectMenu';
|
||||||
|
|
||||||
import LayoutMenuItem from './LayoutMenuItem';
|
import LayoutMenuItem from './LayoutMenuItem';
|
||||||
@@ -18,23 +17,20 @@ import { AuthenticatedContext } from '../../contexts/authentication';
|
|||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const LayoutMenu: FC = () => {
|
const LayoutMenu: FC = () => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const authenticatedContext = useContext(AuthenticatedContext);
|
const authenticatedContext = useContext(AuthenticatedContext);
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{features.project && (
|
<List disablePadding component="nav">
|
||||||
<List disablePadding component="nav">
|
<ProjectMenu />
|
||||||
<ProjectMenu />
|
<Divider />
|
||||||
<Divider />
|
</List>
|
||||||
</List>
|
|
||||||
)}
|
|
||||||
<List disablePadding component="nav">
|
<List disablePadding component="nav">
|
||||||
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
||||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
||||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
|
<LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />
|
||||||
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
<LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
|
||||||
<LayoutMenuItem
|
<LayoutMenuItem
|
||||||
icon={LockIcon}
|
icon={LockIcon}
|
||||||
label={LL.SECURITY(0)}
|
label={LL.SECURITY(0)}
|
||||||
|
|||||||
30
interface/src/components/routing/BlockNavigation.tsx
Normal file
30
interface/src/components/routing/BlockNavigation.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { FC } from 'react';
|
||||||
|
import type { Blocker } from '@remix-run/router';
|
||||||
|
import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from '@mui/material';
|
||||||
|
|
||||||
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
|
interface BlockNavigationProps {
|
||||||
|
blocker: Blocker;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BlockNavigation: FC<BlockNavigationProps> = ({ blocker }) => {
|
||||||
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={blocker.state === 'blocked'}>
|
||||||
|
<DialogTitle>{LL.BLOCK_NAVIGATE_1()}</DialogTitle>
|
||||||
|
<DialogContent dividers>{LL.BLOCK_NAVIGATE_2()}</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button variant="outlined" onClick={() => blocker.reset?.()} color="secondary">
|
||||||
|
{LL.STAY()}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" onClick={() => blocker.proceed?.()} color="primary" autoFocus>
|
||||||
|
{LL.LEAVE()}
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default BlockNavigation;
|
||||||
@@ -4,13 +4,11 @@ import { Navigate } from 'react-router-dom';
|
|||||||
import * as AuthenticationApi from '../../api/authentication';
|
import * as AuthenticationApi from '../../api/authentication';
|
||||||
import { AuthenticationContext } from '../../contexts/authentication';
|
import { AuthenticationContext } from '../../contexts/authentication';
|
||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
|
|
||||||
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const authenticationContext = useContext(AuthenticationContext);
|
const authenticationContext = useContext(AuthenticationContext);
|
||||||
|
|
||||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect(features)} /> : <>{children}</>;
|
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default RequireUnauthenticated;
|
export default RequireUnauthenticated;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FC, useCallback, useContext, useEffect, useState } from 'react';
|
import { FC, useCallback, useEffect, useState } from 'react';
|
||||||
import { useSnackbar } from 'notistack';
|
import { useSnackbar } from 'notistack';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
|
|
||||||
@@ -9,11 +9,9 @@ import { ACCESS_TOKEN } from '../../api/endpoints';
|
|||||||
import { RequiredChildrenProps } from '../../utils';
|
import { RequiredChildrenProps } from '../../utils';
|
||||||
import { LoadingSpinner } from '../../components';
|
import { LoadingSpinner } from '../../components';
|
||||||
import { Me } from '../../types';
|
import { Me } from '../../types';
|
||||||
import { FeaturesContext } from '../features';
|
|
||||||
import { AuthenticationContext } from './context';
|
import { AuthenticationContext } from './context';
|
||||||
|
|
||||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -43,11 +41,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
if (!features.security) {
|
|
||||||
setMe({ admin: true, username: 'admin' });
|
|
||||||
setInitialized(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
||||||
if (accessToken) {
|
if (accessToken) {
|
||||||
try {
|
try {
|
||||||
@@ -62,7 +55,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
|||||||
setMe(undefined);
|
setMe(undefined);
|
||||||
setInitialized(true);
|
setInitialized(true);
|
||||||
}
|
}
|
||||||
}, [features]);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
refresh();
|
refresh();
|
||||||
|
|||||||
@@ -13,7 +13,8 @@ import {
|
|||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
|
BlockNavigation
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
|
|
||||||
import { APProvisionMode, APSettings } from '../../types';
|
import { APProvisionMode, APSettings } from '../../types';
|
||||||
@@ -27,7 +28,7 @@ export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const APSettingsForm: FC = () => {
|
const APSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } =
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
useRest<APSettings>({
|
useRest<APSettings>({
|
||||||
read: APApi.readAPSettings,
|
read: APApi.readAPSettings,
|
||||||
update: APApi.updateAPSettings
|
update: APApi.updateAPSettings
|
||||||
@@ -195,6 +196,7 @@ const APSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const AccessPoint: FC = () => {
|
|||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<APStatusForm />} />
|
<Route path="status" element={<APStatusForm />} />
|
||||||
|
<Route index element={<Navigate to="status" />} />
|
||||||
<Route
|
<Route
|
||||||
path="settings"
|
path="settings"
|
||||||
element={
|
element={
|
||||||
@@ -35,6 +36,7 @@ const AccessPoint: FC = () => {
|
|||||||
</RequireAdmin>
|
</RequireAdmin>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
{/* <Route path="/*" element={<Navigate to="status" />} /> */}
|
||||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { FC, useState } from 'react';
|
|||||||
import { ValidateFieldsError } from 'async-validator';
|
import { ValidateFieldsError } from 'async-validator';
|
||||||
|
|
||||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
||||||
|
|
||||||
import WarningIcon from '@mui/icons-material/Warning';
|
import WarningIcon from '@mui/icons-material/Warning';
|
||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ import {
|
|||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
|
BlockNavigation
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { MqttSettings } from '../../types';
|
import { MqttSettings } from '../../types';
|
||||||
import { numberValue, updateValueDirty, useRest } from '../../utils';
|
import { numberValue, updateValueDirty, useRest } from '../../utils';
|
||||||
@@ -21,7 +23,7 @@ import * as MqttApi from '../../api/mqtt';
|
|||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const MqttSettingsForm: FC = () => {
|
const MqttSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } =
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
useRest<MqttSettings>({
|
useRest<MqttSettings>({
|
||||||
read: MqttApi.readMqttSettings,
|
read: MqttApi.readMqttSettings,
|
||||||
update: MqttApi.updateMqttSettings
|
update: MqttApi.updateMqttSettings
|
||||||
@@ -405,6 +407,7 @@ const MqttSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -29,7 +29,8 @@ import {
|
|||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField,
|
ValidatedTextField,
|
||||||
MessageBox
|
MessageBox,
|
||||||
|
BlockNavigation
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
import { NetworkSettings } from '../../types';
|
import { NetworkSettings } from '../../types';
|
||||||
import * as NetworkApi from '../../api/network';
|
import * as NetworkApi from '../../api/network';
|
||||||
@@ -61,6 +62,7 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
origData,
|
origData,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
saveData,
|
saveData,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
restartNeeded
|
restartNeeded
|
||||||
@@ -330,6 +332,7 @@ const WiFiSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : content()}
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -6,7 +6,14 @@ import WarningIcon from '@mui/icons-material/Warning';
|
|||||||
import CancelIcon from '@mui/icons-material/Cancel';
|
import CancelIcon from '@mui/icons-material/Cancel';
|
||||||
|
|
||||||
import { validate } from '../../validators';
|
import { validate } from '../../validators';
|
||||||
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
|
import {
|
||||||
|
BlockFormControlLabel,
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedTextField,
|
||||||
|
BlockNavigation
|
||||||
|
} from '../../components';
|
||||||
import { NTPSettings } from '../../types';
|
import { NTPSettings } from '../../types';
|
||||||
import { updateValueDirty, useRest } from '../../utils';
|
import { updateValueDirty, useRest } from '../../utils';
|
||||||
import * as NTPApi from '../../api/ntp';
|
import * as NTPApi from '../../api/ntp';
|
||||||
@@ -16,7 +23,7 @@ import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
|
|||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const NTPSettingsForm: FC = () => {
|
const NTPSettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } =
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
useRest<NTPSettings>({
|
useRest<NTPSettings>({
|
||||||
read: NTPApi.readNTPSettings,
|
read: NTPApi.readNTPSettings,
|
||||||
update: NTPApi.updateNTPSettings
|
update: NTPApi.updateNTPSettings
|
||||||
@@ -111,6 +118,7 @@ const NTPSettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
|||||||
|
|
||||||
import * as SecurityApi from '../../api/security';
|
import * as SecurityApi from '../../api/security';
|
||||||
import { SecuritySettings } from '../../types';
|
import { SecuritySettings } from '../../types';
|
||||||
import { ButtonRow, FormLoader, MessageBox, SectionContent, ValidatedPasswordField } from '../../components';
|
import {
|
||||||
|
ButtonRow,
|
||||||
|
FormLoader,
|
||||||
|
MessageBox,
|
||||||
|
SectionContent,
|
||||||
|
ValidatedPasswordField,
|
||||||
|
BlockNavigation
|
||||||
|
} from '../../components';
|
||||||
import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
|
import { SECURITY_SETTINGS_VALIDATOR, validate } from '../../validators';
|
||||||
import { updateValueDirty, useRest } from '../../utils';
|
import { updateValueDirty, useRest } from '../../utils';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
@@ -18,7 +25,7 @@ const SecuritySettingsForm: FC = () => {
|
|||||||
const { LL } = useI18nContext();
|
const { LL } = useI18nContext();
|
||||||
|
|
||||||
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
|
||||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } =
|
const { loadData, saving, data, setData, origData, dirtyFlags, blocker, setDirtyFlags, saveData, errorMessage } =
|
||||||
useRest<SecuritySettings>({
|
useRest<SecuritySettings>({
|
||||||
read: SecurityApi.readSecuritySettings,
|
read: SecurityApi.readSecuritySettings,
|
||||||
update: SecurityApi.updateSecuritySettings
|
update: SecurityApi.updateSecuritySettings
|
||||||
@@ -87,6 +94,7 @@ const SecuritySettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ import {
|
|||||||
FormLoader,
|
FormLoader,
|
||||||
SectionContent,
|
SectionContent,
|
||||||
ValidatedPasswordField,
|
ValidatedPasswordField,
|
||||||
ValidatedTextField
|
ValidatedTextField,
|
||||||
|
BlockNavigation
|
||||||
} from '../../components';
|
} from '../../components';
|
||||||
|
|
||||||
import { OTASettings } from '../../types';
|
import { OTASettings } from '../../types';
|
||||||
@@ -24,7 +25,7 @@ import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
|
|||||||
import { useI18nContext } from '../../i18n/i18n-react';
|
import { useI18nContext } from '../../i18n/i18n-react';
|
||||||
|
|
||||||
const OTASettingsForm: FC = () => {
|
const OTASettingsForm: FC = () => {
|
||||||
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, saveData, errorMessage } =
|
const { loadData, saving, data, setData, origData, dirtyFlags, setDirtyFlags, blocker, saveData, errorMessage } =
|
||||||
useRest<OTASettings>({
|
useRest<OTASettings>({
|
||||||
read: SystemApi.readOTASettings,
|
read: SystemApi.readOTASettings,
|
||||||
update: SystemApi.updateOTASettings
|
update: SystemApi.updateOTASettings
|
||||||
@@ -108,6 +109,7 @@ const OTASettingsForm: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{content()}
|
{content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { Tab } from '@mui/material';
|
|||||||
|
|
||||||
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
|
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
|
||||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||||
import { FeaturesContext } from '../../contexts/features';
|
|
||||||
import UploadFileForm from './UploadFileForm';
|
import UploadFileForm from './UploadFileForm';
|
||||||
import SystemStatusForm from './SystemStatusForm';
|
import SystemStatusForm from './SystemStatusForm';
|
||||||
import OTASettingsForm from './OTASettingsForm';
|
import OTASettingsForm from './OTASettingsForm';
|
||||||
@@ -20,7 +19,6 @@ const System: FC = () => {
|
|||||||
useLayoutTitle(LL.SYSTEM(0));
|
useLayoutTitle(LL.SYSTEM(0));
|
||||||
|
|
||||||
const { me } = useContext(AuthenticatedContext);
|
const { me } = useContext(AuthenticatedContext);
|
||||||
const { features } = useContext(FeaturesContext);
|
|
||||||
const { routerTab } = useRouterTab();
|
const { routerTab } = useRouterTab();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -28,33 +26,28 @@ const System: FC = () => {
|
|||||||
<RouterTabs value={routerTab}>
|
<RouterTabs value={routerTab}>
|
||||||
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
||||||
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
||||||
|
<Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />
|
||||||
{features.ota && <Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />}
|
<Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />
|
||||||
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
|
|
||||||
</RouterTabs>
|
</RouterTabs>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path="status" element={<SystemStatusForm />} />
|
<Route path="status" element={<SystemStatusForm />} />
|
||||||
<Route path="log" element={<SystemLog />} />
|
<Route path="log" element={<SystemLog />} />
|
||||||
{features.ota && (
|
<Route
|
||||||
<Route
|
path="ota"
|
||||||
path="ota"
|
element={
|
||||||
element={
|
<RequireAdmin>
|
||||||
<RequireAdmin>
|
<OTASettingsForm />
|
||||||
<OTASettingsForm />
|
</RequireAdmin>
|
||||||
</RequireAdmin>
|
}
|
||||||
}
|
/>
|
||||||
/>
|
<Route
|
||||||
)}
|
path="upload"
|
||||||
{features.upload_firmware && (
|
element={
|
||||||
<Route
|
<RequireAdmin>
|
||||||
path="upload"
|
<UploadFileForm />
|
||||||
element={
|
</RequireAdmin>
|
||||||
<RequireAdmin>
|
}
|
||||||
<UploadFileForm />
|
/>
|
||||||
</RequireAdmin>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const de: Translation = {
|
|||||||
NEW_NAME_OF: 'Ändere {0}',
|
NEW_NAME_OF: 'Ändere {0}',
|
||||||
ENTITY: 'Entität',
|
ENTITY: 'Entität',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default de;
|
export default de;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const en: Translation = {
|
|||||||
NEW_NAME_OF: 'New {0} name',
|
NEW_NAME_OF: 'New {0} name',
|
||||||
ENTITY: 'entity',
|
ENTITY: 'entity',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes',
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?',
|
||||||
|
STAY: 'Stay',
|
||||||
|
LEAVE: 'Leave'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default en;
|
export default en;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const fr: Translation = {
|
|||||||
NEW_NAME_OF: 'Nouveau nom de {0}',
|
NEW_NAME_OF: 'Nouveau nom de {0}',
|
||||||
ENTITY: 'entité',
|
ENTITY: 'entité',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default fr;
|
export default fr;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const nl: Translation = {
|
|||||||
NEW_NAME_OF: 'Hernoem {0}',
|
NEW_NAME_OF: 'Hernoem {0}',
|
||||||
ENTITY: 'Entiteit',
|
ENTITY: 'Entiteit',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nl;
|
export default nl;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const no: Translation = {
|
|||||||
NEW_NAME_OF: 'Bytt navn {0}',
|
NEW_NAME_OF: 'Bytt navn {0}',
|
||||||
ENTITY: 'Entitet',
|
ENTITY: 'Entitet',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default no;
|
export default no;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const pl: BaseTranslation = {
|
|||||||
NEW_NAME_OF: 'Nowa nazwa {0}',
|
NEW_NAME_OF: 'Nowa nazwa {0}',
|
||||||
ENTITY: 'encji',
|
ENTITY: 'encji',
|
||||||
MIN: 'Min.',
|
MIN: 'Min.',
|
||||||
MAX: 'Maks.'
|
MAX: 'Maks.',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default pl;
|
export default pl;
|
||||||
|
|||||||
@@ -302,7 +302,11 @@ const sv: Translation = {
|
|||||||
NEW_NAME_OF: 'Byt namn {0}',
|
NEW_NAME_OF: 'Byt namn {0}',
|
||||||
ENTITY: 'Entitet',
|
ENTITY: 'Entitet',
|
||||||
MIN: 'min',
|
MIN: 'min',
|
||||||
MAX: 'max'
|
MAX: 'max',
|
||||||
|
BLOCK_NAVIGATE_1: 'You have unsaved changes', // TODO translate
|
||||||
|
BLOCK_NAVIGATE_2: 'If you navigate to a different page, your unsaved changes will be lost. Are you sure you want to leave this page?', // TODO translate
|
||||||
|
STAY: 'Stay', // TODO translate
|
||||||
|
LEAVE: 'Leave' // TODO translate
|
||||||
};
|
};
|
||||||
|
|
||||||
export default sv;
|
export default sv;
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { StrictMode } from 'react';
|
import React from 'react';
|
||||||
import { createRoot } from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
import { BrowserRouter } from 'react-router-dom';
|
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';
|
||||||
|
|
||||||
import App from './App';
|
import App from './App';
|
||||||
|
|
||||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
const router = createBrowserRouter(createRoutesFromElements(<Route path="/*" element={<App />}></Route>));
|
||||||
|
|
||||||
root.render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<StrictMode>
|
<React.StrictMode>
|
||||||
<BrowserRouter>
|
<RouterProvider router={router} />
|
||||||
<App />
|
</React.StrictMode>
|
||||||
</BrowserRouter>
|
|
||||||
</StrictMode>
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ import {
|
|||||||
BlockFormControlLabel,
|
BlockFormControlLabel,
|
||||||
ValidatedTextField,
|
ValidatedTextField,
|
||||||
ButtonRow,
|
ButtonRow,
|
||||||
MessageBox
|
MessageBox,
|
||||||
|
BlockNavigation
|
||||||
} from '../components';
|
} from '../components';
|
||||||
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from '../utils';
|
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from '../utils';
|
||||||
|
|
||||||
@@ -46,6 +47,7 @@ const SettingsApplication: FC = () => {
|
|||||||
origData,
|
origData,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
restartNeeded
|
restartNeeded
|
||||||
} = useRest<Settings>({
|
} = useRest<Settings>({
|
||||||
@@ -662,6 +664,7 @@ const SettingsApplication: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
|
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : content()}
|
{restarting ? <RestartMonitor /> : content()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { FC, useState, useEffect, useCallback } from 'react';
|
import { FC, useState, useEffect, useCallback } from 'react';
|
||||||
|
|
||||||
|
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Typography,
|
Typography,
|
||||||
@@ -34,7 +36,7 @@ import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
|||||||
|
|
||||||
import OptionIcon from './OptionIcon';
|
import OptionIcon from './OptionIcon';
|
||||||
|
|
||||||
import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox } from '../components';
|
import { ButtonRow, FormLoader, ValidatedTextField, SectionContent, MessageBox, BlockNavigation } from '../components';
|
||||||
|
|
||||||
import * as EMSESP from './api';
|
import * as EMSESP from './api';
|
||||||
|
|
||||||
@@ -54,6 +56,9 @@ const SettingsCustomization: FC = () => {
|
|||||||
|
|
||||||
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
|
const emptyDeviceEntity = { id: '', v: 0, n: '', cn: '', m: 0, w: false };
|
||||||
|
|
||||||
|
const [numChanges, setNumChanges] = useState<number>(0);
|
||||||
|
let blocker = useBlocker(numChanges !== 0);
|
||||||
|
|
||||||
const [restarting, setRestarting] = useState<boolean>(false);
|
const [restarting, setRestarting] = useState<boolean>(false);
|
||||||
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
const [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
|
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
|
||||||
@@ -67,6 +72,10 @@ const SettingsCustomization: FC = () => {
|
|||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const [masks, setMasks] = useState(() => ['']);
|
const [masks, setMasks] = useState(() => ['']);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
countChanges();
|
||||||
|
});
|
||||||
|
|
||||||
const entities_theme = useTheme({
|
const entities_theme = useTheme({
|
||||||
Table: `
|
Table: `
|
||||||
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px;
|
--data-table-library_grid-template-columns: 150px repeat(1, minmax(80px, 1fr)) 45px 45px 120px;
|
||||||
@@ -278,6 +287,10 @@ const SettingsCustomization: FC = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countChanges = () => {
|
||||||
|
setNumChanges(getChanges().length);
|
||||||
|
};
|
||||||
|
|
||||||
const restart = async () => {
|
const restart = async () => {
|
||||||
try {
|
try {
|
||||||
await EMSESP.restart();
|
await EMSESP.restart();
|
||||||
@@ -451,34 +464,30 @@ const SettingsCustomization: FC = () => {
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Tooltip arrow placement="top" title="set selected entities to be both visible and output">
|
<Button
|
||||||
<Button
|
size="small"
|
||||||
size="small"
|
sx={{ fontSize: 10 }}
|
||||||
sx={{ fontSize: 10 }}
|
variant="outlined"
|
||||||
variant="outlined"
|
color="inherit"
|
||||||
color="inherit"
|
onClick={() => maskDisabled(false)}
|
||||||
onClick={() => maskDisabled(false)}
|
>
|
||||||
>
|
{LL.SET_ALL()}
|
||||||
{LL.SET_ALL()}
|
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
<OptionIcon type="web_exclude" isSet={false} />
|
||||||
<OptionIcon type="web_exclude" isSet={false} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item>
|
<Grid item>
|
||||||
<Tooltip arrow placement="top" title="set selected entities to be not visible and not output">
|
<Button
|
||||||
<Button
|
size="small"
|
||||||
size="small"
|
sx={{ fontSize: 10 }}
|
||||||
sx={{ fontSize: 10 }}
|
variant="outlined"
|
||||||
variant="outlined"
|
color="inherit"
|
||||||
color="inherit"
|
onClick={() => maskDisabled(true)}
|
||||||
onClick={() => maskDisabled(true)}
|
>
|
||||||
>
|
{LL.SET_ALL()}
|
||||||
{LL.SET_ALL()}
|
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
<OptionIcon type="web_exclude" isSet={true} />
|
||||||
<OptionIcon type="web_exclude" isSet={true} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
<Table data={{ nodes: shown_data }} theme={entities_theme} layout={{ custom: true }}>
|
||||||
@@ -511,7 +520,6 @@ const SettingsCustomization: FC = () => {
|
|||||||
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
if (de.n === '' && de.m & DeviceEntityMask.DV_READONLY) {
|
||||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||||
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||||
}
|
}
|
||||||
@@ -590,8 +598,6 @@ const SettingsCustomization: FC = () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const renderContent = () => {
|
const renderContent = () => {
|
||||||
const num_changes = getChanges().length;
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||||
@@ -609,7 +615,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
{!restartNeeded && (
|
{!restartNeeded && (
|
||||||
<Box display="flex" flexWrap="wrap">
|
<Box display="flex" flexWrap="wrap">
|
||||||
<Box flexGrow={1}>
|
<Box flexGrow={1}>
|
||||||
{num_changes !== 0 && (
|
{numChanges !== 0 && (
|
||||||
<ButtonRow>
|
<ButtonRow>
|
||||||
<Button
|
<Button
|
||||||
startIcon={<WarningIcon color="warning" />}
|
startIcon={<WarningIcon color="warning" />}
|
||||||
@@ -617,7 +623,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
color="info"
|
color="info"
|
||||||
onClick={() => saveCustomization()}
|
onClick={() => saveCustomization()}
|
||||||
>
|
>
|
||||||
{LL.APPLY_CHANGES(num_changes)}
|
{LL.APPLY_CHANGES(numChanges)}
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonRow>
|
</ButtonRow>
|
||||||
)}
|
)}
|
||||||
@@ -712,6 +718,7 @@ const SettingsCustomization: FC = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
||||||
|
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||||
{restarting ? <RestartMonitor /> : renderContent()}
|
{restarting ? <RestartMonitor /> : renderContent()}
|
||||||
{renderEditDialog()}
|
{renderEditDialog()}
|
||||||
</SectionContent>
|
</SectionContent>
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { extractErrorMessage } from '.';
|
|||||||
|
|
||||||
import { useI18nContext } from '../i18n/i18n-react';
|
import { useI18nContext } from '../i18n/i18n-react';
|
||||||
|
|
||||||
|
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||||
|
|
||||||
export interface RestRequestOptions<D> {
|
export interface RestRequestOptions<D> {
|
||||||
read: () => AxiosPromise<D>;
|
read: () => AxiosPromise<D>;
|
||||||
update?: (value: D) => AxiosPromise<D>;
|
update?: (value: D) => AxiosPromise<D>;
|
||||||
@@ -24,6 +26,8 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
|||||||
const [origData, setOrigData] = useState<D>();
|
const [origData, setOrigData] = useState<D>();
|
||||||
const [dirtyFlags, setDirtyFlags] = useState<string[]>();
|
const [dirtyFlags, setDirtyFlags] = useState<string[]>();
|
||||||
|
|
||||||
|
let blocker = useBlocker(dirtyFlags?.length !== 0);
|
||||||
|
|
||||||
const loadData = useCallback(async () => {
|
const loadData = useCallback(async () => {
|
||||||
setData(undefined);
|
setData(undefined);
|
||||||
setDirtyFlags([]);
|
setDirtyFlags([]);
|
||||||
@@ -82,6 +86,7 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
|||||||
origData,
|
origData,
|
||||||
dirtyFlags,
|
dirtyFlags,
|
||||||
setDirtyFlags,
|
setDirtyFlags,
|
||||||
|
blocker,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
restartNeeded
|
restartNeeded
|
||||||
} as const;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user