mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 08:19:52 +03:00
Warn user in WebUI of unsaved changes #911
This commit is contained in:
@@ -4,8 +4,6 @@ import { SnackbarProvider } from 'notistack';
|
||||
import { IconButton } from '@mui/material';
|
||||
import CloseIcon from '@mui/icons-material/Close';
|
||||
|
||||
import { FeaturesLoader } from './contexts/features';
|
||||
|
||||
import CustomTheme from './CustomTheme';
|
||||
import AppRouting from './AppRouting';
|
||||
|
||||
@@ -44,9 +42,9 @@ const App: FC = () => {
|
||||
</IconButton>
|
||||
)}
|
||||
>
|
||||
<FeaturesLoader>
|
||||
<AppRouting />
|
||||
</FeaturesLoader>
|
||||
{/* <FeaturesLoader> */}
|
||||
<AppRouting />
|
||||
{/* </FeaturesLoader> */}
|
||||
</SnackbarProvider>
|
||||
</CustomTheme>
|
||||
</TypesafeI18n>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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 { useI18nContext } from './i18n/i18n-react';
|
||||
|
||||
import { Authentication, AuthenticationContext } from './contexts/authentication';
|
||||
import { FeaturesContext } from './contexts/features';
|
||||
import { RequireAuthenticated, RequireUnauthenticated } from './components';
|
||||
|
||||
import SignIn from './SignIn';
|
||||
@@ -27,6 +28,7 @@ const RootRedirect: FC<SecurityRedirectProps> = ({ message, variant, signOut })
|
||||
return <Navigate to="/" />;
|
||||
};
|
||||
|
||||
// TODO still need this?
|
||||
export const RemoveTrailingSlashes = () => {
|
||||
const location = useLocation();
|
||||
return (
|
||||
@@ -42,7 +44,6 @@ export const RemoveTrailingSlashes = () => {
|
||||
};
|
||||
|
||||
const AppRouting: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
@@ -51,16 +52,14 @@ const AppRouting: FC = () => {
|
||||
<Routes>
|
||||
<Route path="/unauthorized" element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />} />
|
||||
<Route path="/fileUpdated" element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} variant="success" />} />
|
||||
{features.security && (
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireUnauthenticated>
|
||||
<SignIn />
|
||||
</RequireUnauthenticated>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireUnauthenticated>
|
||||
<SignIn />
|
||||
</RequireUnauthenticated>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/*"
|
||||
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 { AxiosError } from 'axios';
|
||||
|
||||
import { FeaturesContext } from './contexts/features';
|
||||
import * as AuthenticationApi from './api/authentication';
|
||||
import { PROJECT_PATH } from './api/env';
|
||||
import { AXIOS } from './api/endpoints';
|
||||
@@ -18,7 +17,6 @@ import System from './framework/system/System';
|
||||
import Security from './framework/security/Security';
|
||||
|
||||
const AuthenticatedRouting: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -41,23 +39,21 @@ const AuthenticatedRouting: FC = () => {
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
{features.project && <Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />}
|
||||
<Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />
|
||||
<Route path="/network/*" element={<NetworkConnection />} />
|
||||
<Route path="/ap/*" element={<AccessPoint />} />
|
||||
{features.ntp && <Route path="/ntp/*" element={<NetworkTime />} />}
|
||||
{features.mqtt && <Route path="/mqtt/*" element={<Mqtt />} />}
|
||||
{features.security && (
|
||||
<Route
|
||||
path="/security/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Security />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route path="/ntp/*" element={<NetworkTime />} />
|
||||
<Route path="/mqtt/*" element={<Mqtt />} />
|
||||
<Route
|
||||
path="/security/*"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<Security />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/system/*" element={<System />} />
|
||||
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute(features)} />} />
|
||||
<Route path="/*" element={<Navigate to={AuthenticationApi.getDefaultRoute} />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import * as H from 'history';
|
||||
import jwtDecode from 'jwt-decode';
|
||||
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 { PROJECT_PATH } from './env';
|
||||
@@ -11,7 +11,7 @@ import { PROJECT_PATH } from './env';
|
||||
export const SIGN_IN_PATHNAME = 'loginPathname';
|
||||
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> {
|
||||
return AXIOS.get('/verifyAuthorization');
|
||||
@@ -40,12 +40,12 @@ export function clearLoginRedirect() {
|
||||
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 signInSearch = getStorage().getItem(SIGN_IN_SEARCH);
|
||||
clearLoginRedirect();
|
||||
return {
|
||||
pathname: signInPathname || getDefaultRoute(features),
|
||||
pathname: signInPathname || `/${PROJECT_PATH}`,
|
||||
search: (signInPathname && signInSearch) || undefined
|
||||
};
|
||||
}
|
||||
|
||||
@@ -6,3 +6,4 @@ export * from './upload';
|
||||
export { default as SectionContent } from './SectionContent';
|
||||
export { default as ButtonRow } from './ButtonRow';
|
||||
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 MenuIcon from '@mui/icons-material/Menu';
|
||||
|
||||
import LayoutAuthMenu from './LayoutAuthMenu';
|
||||
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
|
||||
export const DRAWER_WIDTH = 240;
|
||||
|
||||
interface LayoutAppBarProps {
|
||||
@@ -15,8 +13,6 @@ interface LayoutAppBarProps {
|
||||
}
|
||||
|
||||
const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
|
||||
return (
|
||||
<AppBar
|
||||
position="fixed"
|
||||
@@ -41,7 +37,7 @@ const LayoutAppBar: FC<LayoutAppBarProps> = ({ title, onToggleDrawer }) => {
|
||||
{title}
|
||||
</Typography>
|
||||
<Box flexGrow={1} />
|
||||
{features.security && <LayoutAuthMenu />}
|
||||
<LayoutAuthMenu />
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import SettingsIcon from '@mui/icons-material/Settings';
|
||||
import LockIcon from '@mui/icons-material/Lock';
|
||||
import SettingsEthernetIcon from '@mui/icons-material/SettingsEthernet';
|
||||
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
import ProjectMenu from '../../project/ProjectMenu';
|
||||
|
||||
import LayoutMenuItem from './LayoutMenuItem';
|
||||
@@ -18,23 +17,20 @@ import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
const LayoutMenu: FC = () => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<>
|
||||
{features.project && (
|
||||
<List disablePadding component="nav">
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
</List>
|
||||
)}
|
||||
<List disablePadding component="nav">
|
||||
<ProjectMenu />
|
||||
<Divider />
|
||||
</List>
|
||||
<List disablePadding component="nav">
|
||||
<LayoutMenuItem icon={SettingsEthernetIcon} label={LL.NETWORK(0)} to="/network" />
|
||||
<LayoutMenuItem icon={SettingsInputAntennaIcon} label={LL.ACCESS_POINT(0)} to="/ap" />
|
||||
{features.ntp && <LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />}
|
||||
{features.mqtt && <LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />}
|
||||
<LayoutMenuItem icon={AccessTimeIcon} label="NTP" to="/ntp" />
|
||||
<LayoutMenuItem icon={DeviceHubIcon} label="MQTT" to="/mqtt" />
|
||||
<LayoutMenuItem
|
||||
icon={LockIcon}
|
||||
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 { AuthenticationContext } from '../../contexts/authentication';
|
||||
import { RequiredChildrenProps } from '../../utils';
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
|
||||
const RequireUnauthenticated: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const authenticationContext = useContext(AuthenticationContext);
|
||||
|
||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect(features)} /> : <>{children}</>;
|
||||
return authenticationContext.me ? <Navigate to={AuthenticationApi.fetchLoginRedirect()} /> : <>{children}</>;
|
||||
};
|
||||
|
||||
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 { useNavigate } from 'react-router-dom';
|
||||
|
||||
@@ -9,11 +9,9 @@ import { ACCESS_TOKEN } from '../../api/endpoints';
|
||||
import { RequiredChildrenProps } from '../../utils';
|
||||
import { LoadingSpinner } from '../../components';
|
||||
import { Me } from '../../types';
|
||||
import { FeaturesContext } from '../features';
|
||||
import { AuthenticationContext } from './context';
|
||||
|
||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
const navigate = useNavigate();
|
||||
@@ -43,11 +41,6 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!features.security) {
|
||||
setMe({ admin: true, username: 'admin' });
|
||||
setInitialized(true);
|
||||
return;
|
||||
}
|
||||
const accessToken = AuthenticationApi.getStorage().getItem(ACCESS_TOKEN);
|
||||
if (accessToken) {
|
||||
try {
|
||||
@@ -62,7 +55,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
setMe(undefined);
|
||||
setInitialized(true);
|
||||
}
|
||||
}, [features]);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
|
||||
@@ -13,7 +13,8 @@ import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
} from '../../components';
|
||||
|
||||
import { APProvisionMode, APSettings } from '../../types';
|
||||
@@ -27,7 +28,7 @@ export const isAPEnabled = ({ provision_mode }: APSettings) => {
|
||||
};
|
||||
|
||||
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>({
|
||||
read: APApi.readAPSettings,
|
||||
update: APApi.updateAPSettings
|
||||
@@ -195,6 +196,7 @@ const APSettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.ACCESS_POINT(1))} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -27,6 +27,7 @@ const AccessPoint: FC = () => {
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<APStatusForm />} />
|
||||
<Route index element={<Navigate to="status" />} />
|
||||
<Route
|
||||
path="settings"
|
||||
element={
|
||||
@@ -35,6 +36,7 @@ const AccessPoint: FC = () => {
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
{/* <Route path="/*" element={<Navigate to="status" />} /> */}
|
||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||
</Routes>
|
||||
</>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { FC, useState } from 'react';
|
||||
import { ValidateFieldsError } from 'async-validator';
|
||||
|
||||
import { Button, Checkbox, MenuItem, Grid, Typography, InputAdornment } from '@mui/material';
|
||||
|
||||
import WarningIcon from '@mui/icons-material/Warning';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
@@ -12,7 +13,8 @@ import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
} from '../../components';
|
||||
import { MqttSettings } from '../../types';
|
||||
import { numberValue, updateValueDirty, useRest } from '../../utils';
|
||||
@@ -21,7 +23,7 @@ import * as MqttApi from '../../api/mqtt';
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
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>({
|
||||
read: MqttApi.readMqttSettings,
|
||||
update: MqttApi.updateMqttSettings
|
||||
@@ -405,6 +407,7 @@ const MqttSettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('MQTT')} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,8 @@ import {
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField,
|
||||
MessageBox
|
||||
MessageBox,
|
||||
BlockNavigation
|
||||
} from '../../components';
|
||||
import { NetworkSettings } from '../../types';
|
||||
import * as NetworkApi from '../../api/network';
|
||||
@@ -61,6 +62,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
saveData,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
@@ -330,6 +332,7 @@ const WiFiSettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.NETWORK(1))} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,14 @@ import WarningIcon from '@mui/icons-material/Warning';
|
||||
import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
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 { updateValueDirty, useRest } from '../../utils';
|
||||
import * as NTPApi from '../../api/ntp';
|
||||
@@ -16,7 +23,7 @@ import { NTP_SETTINGS_VALIDATOR } from '../../validators/ntp';
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
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>({
|
||||
read: NTPApi.readNTPSettings,
|
||||
update: NTPApi.updateNTPSettings
|
||||
@@ -111,6 +118,7 @@ const NTPSettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('NTP')} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -7,7 +7,14 @@ import CancelIcon from '@mui/icons-material/Cancel';
|
||||
|
||||
import * as SecurityApi from '../../api/security';
|
||||
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 { updateValueDirty, useRest } from '../../utils';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
@@ -18,7 +25,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
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>({
|
||||
read: SecurityApi.readSecuritySettings,
|
||||
update: SecurityApi.updateSecuritySettings
|
||||
@@ -87,6 +94,7 @@ const SecuritySettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF(LL.SECURITY(1))} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -11,7 +11,8 @@ import {
|
||||
FormLoader,
|
||||
SectionContent,
|
||||
ValidatedPasswordField,
|
||||
ValidatedTextField
|
||||
ValidatedTextField,
|
||||
BlockNavigation
|
||||
} from '../../components';
|
||||
|
||||
import { OTASettings } from '../../types';
|
||||
@@ -24,7 +25,7 @@ import { OTA_SETTINGS_VALIDATOR } from '../../validators/system';
|
||||
import { useI18nContext } from '../../i18n/i18n-react';
|
||||
|
||||
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>({
|
||||
read: SystemApi.readOTASettings,
|
||||
update: SystemApi.updateOTASettings
|
||||
@@ -108,6 +109,7 @@ const OTASettingsForm: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.SETTINGS_OF('OTA')} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Tab } from '@mui/material';
|
||||
|
||||
import { useRouterTab, RouterTabs, useLayoutTitle, RequireAdmin } from '../../components';
|
||||
import { AuthenticatedContext } from '../../contexts/authentication';
|
||||
import { FeaturesContext } from '../../contexts/features';
|
||||
import UploadFileForm from './UploadFileForm';
|
||||
import SystemStatusForm from './SystemStatusForm';
|
||||
import OTASettingsForm from './OTASettingsForm';
|
||||
@@ -20,7 +19,6 @@ const System: FC = () => {
|
||||
useLayoutTitle(LL.SYSTEM(0));
|
||||
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
const { features } = useContext(FeaturesContext);
|
||||
const { routerTab } = useRouterTab();
|
||||
|
||||
return (
|
||||
@@ -28,33 +26,28 @@ const System: FC = () => {
|
||||
<RouterTabs value={routerTab}>
|
||||
<Tab value="status" label={LL.STATUS_OF(LL.SYSTEM(1))} />
|
||||
<Tab value="log" label={LL.LOG_OF(LL.SYSTEM(2))} />
|
||||
|
||||
{features.ota && <Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />}
|
||||
{features.upload_firmware && <Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />}
|
||||
<Tab value="ota" label={LL.SETTINGS_OF('OTA')} disabled={!me.admin} />
|
||||
<Tab value="upload" label={LL.UPLOAD_DOWNLOAD()} disabled={!me.admin} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="status" element={<SystemStatusForm />} />
|
||||
<Route path="log" element={<SystemLog />} />
|
||||
{features.ota && (
|
||||
<Route
|
||||
path="ota"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<OTASettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{features.upload_firmware && (
|
||||
<Route
|
||||
path="upload"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<UploadFileForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
<Route
|
||||
path="ota"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<OTASettingsForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="upload"
|
||||
element={
|
||||
<RequireAdmin>
|
||||
<UploadFileForm />
|
||||
</RequireAdmin>
|
||||
}
|
||||
/>
|
||||
<Route path="/*" element={<Navigate replace to="status" />} />
|
||||
</Routes>
|
||||
</>
|
||||
|
||||
@@ -302,7 +302,11 @@ const de: Translation = {
|
||||
NEW_NAME_OF: 'Ändere {0}',
|
||||
ENTITY: 'Entität',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const en: Translation = {
|
||||
NEW_NAME_OF: 'New {0} name',
|
||||
ENTITY: 'entity',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const fr: Translation = {
|
||||
NEW_NAME_OF: 'Nouveau nom de {0}',
|
||||
ENTITY: 'entité',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const nl: Translation = {
|
||||
NEW_NAME_OF: 'Hernoem {0}',
|
||||
ENTITY: 'Entiteit',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const no: Translation = {
|
||||
NEW_NAME_OF: 'Bytt navn {0}',
|
||||
ENTITY: 'Entitet',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const pl: BaseTranslation = {
|
||||
NEW_NAME_OF: 'Nowa nazwa {0}',
|
||||
ENTITY: 'encji',
|
||||
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;
|
||||
|
||||
@@ -302,7 +302,11 @@ const sv: Translation = {
|
||||
NEW_NAME_OF: 'Byt namn {0}',
|
||||
ENTITY: 'Entitet',
|
||||
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;
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { createBrowserRouter, createRoutesFromElements, Route, RouterProvider } from 'react-router-dom';
|
||||
|
||||
import App from './App';
|
||||
|
||||
const root = createRoot(document.getElementById('root') as HTMLElement);
|
||||
const router = createBrowserRouter(createRoutesFromElements(<Route path="/*" element={<App />}></Route>));
|
||||
|
||||
root.render(
|
||||
<StrictMode>
|
||||
<BrowserRouter>
|
||||
<App />
|
||||
</BrowserRouter>
|
||||
</StrictMode>
|
||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||
<React.StrictMode>
|
||||
<RouterProvider router={router} />
|
||||
</React.StrictMode>
|
||||
);
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
BlockFormControlLabel,
|
||||
ValidatedTextField,
|
||||
ButtonRow,
|
||||
MessageBox
|
||||
MessageBox,
|
||||
BlockNavigation
|
||||
} from '../components';
|
||||
import { numberValue, extractErrorMessage, updateValueDirty, useRest } from '../utils';
|
||||
|
||||
@@ -46,6 +47,7 @@ const SettingsApplication: FC = () => {
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} = useRest<Settings>({
|
||||
@@ -662,6 +664,7 @@ const SettingsApplication: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.APPLICATION_SETTINGS()} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : content()}
|
||||
</SectionContent>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { FC, useState, useEffect, useCallback } from 'react';
|
||||
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Button,
|
||||
Typography,
|
||||
@@ -34,7 +36,7 @@ import PowerSettingsNewIcon from '@mui/icons-material/PowerSettingsNew';
|
||||
|
||||
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';
|
||||
|
||||
@@ -54,6 +56,9 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
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 [restartNeeded, setRestartNeeded] = useState<boolean>(false);
|
||||
const [deviceEntities, setDeviceEntities] = useState<DeviceEntity[]>([emptyDeviceEntity]);
|
||||
@@ -67,6 +72,10 @@ const SettingsCustomization: FC = () => {
|
||||
// eslint-disable-next-line
|
||||
const [masks, setMasks] = useState(() => ['']);
|
||||
|
||||
useEffect(() => {
|
||||
countChanges();
|
||||
});
|
||||
|
||||
const entities_theme = useTheme({
|
||||
Table: `
|
||||
--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 () => {
|
||||
try {
|
||||
await EMSESP.restart();
|
||||
@@ -451,34 +464,30 @@ const SettingsCustomization: FC = () => {
|
||||
</Grid>
|
||||
|
||||
<Grid item>
|
||||
<Tooltip arrow placement="top" title="set selected entities to be both visible and output">
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(false)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||
<OptionIcon type="web_exclude" isSet={false} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(false)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={false} />
|
||||
<OptionIcon type="web_exclude" isSet={false} />
|
||||
</Button>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Tooltip arrow placement="top" title="set selected entities to be not visible and not output">
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(true)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Button
|
||||
size="small"
|
||||
sx={{ fontSize: 10 }}
|
||||
variant="outlined"
|
||||
color="inherit"
|
||||
onClick={() => maskDisabled(true)}
|
||||
>
|
||||
{LL.SET_ALL()}
|
||||
<OptionIcon type="api_mqtt_exclude" isSet={true} />
|
||||
<OptionIcon type="web_exclude" isSet={true} />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<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) {
|
||||
de.m = de.m | DeviceEntityMask.DV_WEB_EXCLUDE;
|
||||
}
|
||||
|
||||
if (de.m & DeviceEntityMask.DV_WEB_EXCLUDE) {
|
||||
de.m = de.m & ~DeviceEntityMask.DV_FAVORITE;
|
||||
}
|
||||
@@ -590,8 +598,6 @@ const SettingsCustomization: FC = () => {
|
||||
);
|
||||
|
||||
const renderContent = () => {
|
||||
const num_changes = getChanges().length;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Typography sx={{ pt: 2, pb: 2 }} variant="h6" color="primary">
|
||||
@@ -609,7 +615,7 @@ const SettingsCustomization: FC = () => {
|
||||
{!restartNeeded && (
|
||||
<Box display="flex" flexWrap="wrap">
|
||||
<Box flexGrow={1}>
|
||||
{num_changes !== 0 && (
|
||||
{numChanges !== 0 && (
|
||||
<ButtonRow>
|
||||
<Button
|
||||
startIcon={<WarningIcon color="warning" />}
|
||||
@@ -617,7 +623,7 @@ const SettingsCustomization: FC = () => {
|
||||
color="info"
|
||||
onClick={() => saveCustomization()}
|
||||
>
|
||||
{LL.APPLY_CHANGES(num_changes)}
|
||||
{LL.APPLY_CHANGES(numChanges)}
|
||||
</Button>
|
||||
</ButtonRow>
|
||||
)}
|
||||
@@ -712,6 +718,7 @@ const SettingsCustomization: FC = () => {
|
||||
|
||||
return (
|
||||
<SectionContent title={LL.CUSTOMIZATIONS()} titleGutter>
|
||||
{blocker ? <BlockNavigation blocker={blocker} /> : null}
|
||||
{restarting ? <RestartMonitor /> : renderContent()}
|
||||
{renderEditDialog()}
|
||||
</SectionContent>
|
||||
|
||||
@@ -6,6 +6,8 @@ import { extractErrorMessage } from '.';
|
||||
|
||||
import { useI18nContext } from '../i18n/i18n-react';
|
||||
|
||||
import { unstable_useBlocker as useBlocker } from 'react-router-dom';
|
||||
|
||||
export interface RestRequestOptions<D> {
|
||||
read: () => 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 [dirtyFlags, setDirtyFlags] = useState<string[]>();
|
||||
|
||||
let blocker = useBlocker(dirtyFlags?.length !== 0);
|
||||
|
||||
const loadData = useCallback(async () => {
|
||||
setData(undefined);
|
||||
setDirtyFlags([]);
|
||||
@@ -82,6 +86,7 @@ export const useRest = <D>({ read, update }: RestRequestOptions<D>) => {
|
||||
origData,
|
||||
dirtyFlags,
|
||||
setDirtyFlags,
|
||||
blocker,
|
||||
errorMessage,
|
||||
restartNeeded
|
||||
} as const;
|
||||
|
||||
Reference in New Issue
Block a user