mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2026-06-21 23:36:26 +03:00
react-router optimizations
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import { memo, useEffect, useState } from 'react';
|
||||
import { Outlet } from 'react-router';
|
||||
|
||||
import AppRouting from 'AppRouting';
|
||||
import CustomTheme from 'CustomTheme';
|
||||
import { Toaster } from 'components/toast';
|
||||
import { Authentication } from 'contexts/authentication';
|
||||
import TypesafeI18n from 'i18n/i18n-react';
|
||||
import type { Locales } from 'i18n/i18n-types';
|
||||
import { loadLocaleAsync } from 'i18n/i18n-util.async';
|
||||
@@ -43,7 +44,9 @@ const App = memo(() => {
|
||||
return (
|
||||
<TypesafeI18n locale={locale}>
|
||||
<CustomTheme>
|
||||
<AppRouting />
|
||||
<Authentication>
|
||||
<Outlet />
|
||||
</Authentication>
|
||||
<Toaster />
|
||||
</CustomTheme>
|
||||
</TypesafeI18n>
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { type FC, memo, useContext, useEffect, useRef } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
|
||||
import AuthenticatedRouting from 'AuthenticatedRouting';
|
||||
import SignIn from 'SignIn';
|
||||
import { RequireAuthenticated, RequireUnauthenticated } from 'components';
|
||||
import { toast } from 'components/toast';
|
||||
import { Authentication, AuthenticationContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
interface SecurityRedirectProps {
|
||||
readonly message: string;
|
||||
readonly signOut?: boolean;
|
||||
}
|
||||
|
||||
const RootRedirect: FC<SecurityRedirectProps> = memo(
|
||||
({ message, signOut = false }) => {
|
||||
const { signOut: contextSignOut } = useContext(AuthenticationContext);
|
||||
const hasShownToast = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Prevent duplicate toasts on strict mode or re-renders
|
||||
if (!hasShownToast.current) {
|
||||
hasShownToast.current = true;
|
||||
if (signOut) {
|
||||
contextSignOut(false);
|
||||
}
|
||||
toast.success(message);
|
||||
}
|
||||
// Only run once on mount - using ref to track execution
|
||||
}, []);
|
||||
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
);
|
||||
|
||||
const AppRouting: FC = memo(() => {
|
||||
const { LL } = useI18nContext();
|
||||
|
||||
return (
|
||||
<Authentication>
|
||||
<Routes>
|
||||
<Route
|
||||
path="/unauthorized"
|
||||
element={<RootRedirect message={LL.PLEASE_SIGNIN()} signOut />}
|
||||
/>
|
||||
<Route
|
||||
path="/fileUpdated"
|
||||
element={<RootRedirect message={LL.UPLOAD_SUCCESSFUL()} />}
|
||||
/>
|
||||
<Route
|
||||
path="/"
|
||||
element={
|
||||
<RequireUnauthenticated>
|
||||
<SignIn />
|
||||
</RequireUnauthenticated>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/*"
|
||||
element={
|
||||
<RequireAuthenticated>
|
||||
<AuthenticatedRouting />
|
||||
</RequireAuthenticated>
|
||||
}
|
||||
/>
|
||||
</Routes>
|
||||
</Authentication>
|
||||
);
|
||||
});
|
||||
|
||||
export default AppRouting;
|
||||
@@ -1,81 +0,0 @@
|
||||
import { memo, useContext } from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router';
|
||||
|
||||
import Commands from 'app/main/Commands';
|
||||
import CustomEntities from 'app/main/CustomEntities';
|
||||
import Customizations from 'app/main/Customizations';
|
||||
import Dashboard from 'app/main/Dashboard';
|
||||
import Devices from 'app/main/Devices';
|
||||
import Help from 'app/main/Help';
|
||||
import Modules from 'app/main/Modules';
|
||||
import Scheduler from 'app/main/Scheduler';
|
||||
import Sensors from 'app/main/Sensors';
|
||||
import UserProfile from 'app/main/UserProfile';
|
||||
import APSettings from 'app/settings/APSettings';
|
||||
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||
import MqttSettings from 'app/settings/MqttSettings';
|
||||
import NTPSettings from 'app/settings/NTPSettings';
|
||||
import Settings from 'app/settings/Settings';
|
||||
import Version from 'app/settings/Version';
|
||||
import Network from 'app/settings/network/Network';
|
||||
import Security from 'app/settings/security/Security';
|
||||
import APStatus from 'app/status/APStatus';
|
||||
import Activity from 'app/status/Activity';
|
||||
import HardwareStatus from 'app/status/HardwareStatus';
|
||||
import MqttStatus from 'app/status/MqttStatus';
|
||||
import NTPStatus from 'app/status/NTPStatus';
|
||||
import NetworkStatus from 'app/status/NetworkStatus';
|
||||
import Status from 'app/status/Status';
|
||||
import SystemLog from 'app/status/SystemLog';
|
||||
import { Layout } from 'components';
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
|
||||
const AuthenticatedRouting = memo(() => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
return (
|
||||
<Layout>
|
||||
<Routes>
|
||||
<Route path="/dashboard/*" element={<Dashboard />} />
|
||||
<Route path="/devices/*" element={<Devices />} />
|
||||
<Route path="/sensors/*" element={<Sensors />} />
|
||||
<Route path="/help/*" element={<Help />} />
|
||||
<Route path="/user/*" element={<UserProfile />} />
|
||||
|
||||
<Route path="/status/*" element={<Status />} />
|
||||
<Route path="/status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||
<Route path="/status/activity" element={<Activity />} />
|
||||
<Route path="/status/log" element={<SystemLog />} />
|
||||
<Route path="/status/mqtt" element={<MqttStatus />} />
|
||||
<Route path="/status/ntp" element={<NTPStatus />} />
|
||||
<Route path="/status/ap" element={<APStatus />} />
|
||||
<Route path="/status/network" element={<NetworkStatus />} />
|
||||
|
||||
{me.admin && (
|
||||
<>
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
<Route path="/settings/version" element={<Version />} />
|
||||
<Route path="/settings/application" element={<ApplicationSettings />} />
|
||||
<Route path="/settings/mqtt" element={<MqttSettings />} />
|
||||
<Route path="/settings/ntp" element={<NTPSettings />} />
|
||||
<Route path="/settings/ap" element={<APSettings />} />
|
||||
<Route path="/settings/modules" element={<Modules />} />
|
||||
<Route path="/settings/downloadUpload" element={<DownloadUpload />} />
|
||||
|
||||
<Route path="/settings/network/*" element={<Network />} />
|
||||
<Route path="/settings/security/*" element={<Security />} />
|
||||
|
||||
<Route path="/customizations" element={<Customizations />} />
|
||||
<Route path="/commands" element={<Commands />} />
|
||||
<Route path="/scheduler" element={<Scheduler />} />
|
||||
<Route path="/customentities" element={<CustomEntities />} />
|
||||
</>
|
||||
)}
|
||||
|
||||
<Route path="/*" element={<Navigate to="/" />} />
|
||||
</Routes>
|
||||
</Layout>
|
||||
);
|
||||
});
|
||||
|
||||
export default AuthenticatedRouting;
|
||||
@@ -1,12 +1,5 @@
|
||||
import { memo, useState } from 'react';
|
||||
import {
|
||||
Navigate,
|
||||
Route,
|
||||
Routes,
|
||||
matchRoutes,
|
||||
useLocation,
|
||||
useNavigate
|
||||
} from 'react-router';
|
||||
import { Outlet, useMatch, useNavigate } from 'react-router';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
@@ -14,27 +7,13 @@ import { RouterTabs, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
import type { WiFiNetwork } from 'types';
|
||||
|
||||
import NetworkSettings from './NetworkSettings';
|
||||
import { WiFiConnectionContext } from './WiFiConnectionContext';
|
||||
import WiFiNetworkScanner from './WiFiNetworkScanner';
|
||||
|
||||
const Network = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.NETWORK(0));
|
||||
|
||||
// this also works!
|
||||
// const routerTab = useMatch(`settings/network/:path/*`)?.pathname || false;
|
||||
const matchedRoutes = matchRoutes(
|
||||
[
|
||||
{
|
||||
path: '/settings/network/settings',
|
||||
element: <NetworkSettings />
|
||||
},
|
||||
{ path: '/settings/network/scan', element: <WiFiNetworkScanner /> }
|
||||
],
|
||||
useLocation()
|
||||
);
|
||||
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
||||
const routerTab = useMatch('/settings/network/:tab')?.pathname || false;
|
||||
|
||||
const navigate = useNavigate();
|
||||
|
||||
@@ -64,14 +43,7 @@ const Network = () => {
|
||||
/>
|
||||
<Tab value="/settings/network/scan" label={LL.NETWORK_SCAN()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="scan" element={<WiFiNetworkScanner />} />
|
||||
<Route path="settings" element={<NetworkSettings />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate replace to="/settings/network/settings" />}
|
||||
/>
|
||||
</Routes>
|
||||
<Outlet />
|
||||
</WiFiConnectionContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,31 +1,16 @@
|
||||
import { memo } from 'react';
|
||||
import { Navigate, Route, Routes, matchRoutes, useLocation } from 'react-router';
|
||||
import { Outlet, useMatch } from 'react-router';
|
||||
|
||||
import { Tab } from '@mui/material';
|
||||
|
||||
import { RouterTabs, useLayoutTitle } from 'components';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
import ManageUsers from './ManageUsers';
|
||||
import SecuritySettings from './SecuritySettings';
|
||||
|
||||
const Security = () => {
|
||||
const { LL } = useI18nContext();
|
||||
useLayoutTitle(LL.SECURITY(0));
|
||||
|
||||
const location = useLocation();
|
||||
|
||||
const matchedRoutes = matchRoutes(
|
||||
[
|
||||
{
|
||||
path: '/settings/security/settings',
|
||||
element: <ManageUsers />
|
||||
},
|
||||
{ path: '/settings/security/users', element: <SecuritySettings /> }
|
||||
],
|
||||
location
|
||||
);
|
||||
const routerTab = matchedRoutes?.[0]?.route.path || false;
|
||||
const routerTab = useMatch('/settings/security/:tab')?.pathname || false;
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -36,14 +21,7 @@ const Security = () => {
|
||||
/>
|
||||
<Tab value="/settings/security/users" label={LL.MANAGE_USERS()} />
|
||||
</RouterTabs>
|
||||
<Routes>
|
||||
<Route path="users" element={<ManageUsers />} />
|
||||
<Route path="settings" element={<SecuritySettings />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate replace to="/settings/security/settings" />}
|
||||
/>
|
||||
</Routes>
|
||||
<Outlet />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { memo, useContext } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
import { Navigate, Outlet } from 'react-router';
|
||||
|
||||
import { AuthenticatedContext } from 'contexts/authentication';
|
||||
import type { RequiredChildrenProps } from 'utils';
|
||||
|
||||
const RequireAdmin: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const authenticatedContext = useContext(AuthenticatedContext);
|
||||
return authenticatedContext.me.admin ? (
|
||||
<>{children}</>
|
||||
) : (
|
||||
<Navigate replace to="/" />
|
||||
);
|
||||
// Layout-route guard: renders nested admin routes only for admins, otherwise
|
||||
// redirects home. Must be used inside the authenticated route subtree so that
|
||||
// AuthenticatedContext (and `me`) is available.
|
||||
const RequireAdmin = () => {
|
||||
const { me } = useContext(AuthenticatedContext);
|
||||
return me.admin ? <Outlet /> : <Navigate replace to="/" />;
|
||||
};
|
||||
|
||||
export default memo(RequireAdmin);
|
||||
|
||||
35
interface/src/components/routing/RootRedirect.tsx
Normal file
35
interface/src/components/routing/RootRedirect.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { memo, useContext, useEffect, useRef } from 'react';
|
||||
import { Navigate } from 'react-router';
|
||||
|
||||
import { toast } from 'components/toast';
|
||||
import { AuthenticationContext } from 'contexts/authentication';
|
||||
import { useI18nContext } from 'i18n/i18n-react';
|
||||
|
||||
type RootRedirectKind = 'unauthorized' | 'fileUpdated';
|
||||
|
||||
// Shows a one-shot toast and bounces back to "/". Used by the /unauthorized and
|
||||
// /fileUpdated routes. Resolves its own i18n message so it can be used directly
|
||||
// as a static route element.
|
||||
const RootRedirect = ({ kind }: { kind: RootRedirectKind }) => {
|
||||
const { LL } = useI18nContext();
|
||||
const { signOut } = useContext(AuthenticationContext);
|
||||
const hasShownToast = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Guard against StrictMode double-invoke / re-renders.
|
||||
if (hasShownToast.current) return;
|
||||
hasShownToast.current = true;
|
||||
|
||||
if (kind === 'unauthorized') {
|
||||
signOut(false);
|
||||
toast.success(LL.PLEASE_SIGNIN());
|
||||
} else {
|
||||
toast.success(LL.UPLOAD_SUCCESSFUL());
|
||||
}
|
||||
// Run once on mount.
|
||||
}, []);
|
||||
|
||||
return <Navigate to="/" replace />;
|
||||
};
|
||||
|
||||
export default memo(RootRedirect);
|
||||
@@ -2,3 +2,4 @@ export { default as RouterTabs } from './RouterTabs';
|
||||
export { default as RequireAdmin } from './RequireAdmin';
|
||||
export { default as RequireAuthenticated } from './RequireAuthenticated';
|
||||
export { default as RequireUnauthenticated } from './RequireUnauthenticated';
|
||||
export { default as RootRedirect } from './RootRedirect';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import type { FC } from 'react';
|
||||
import { redirect } from 'react-router';
|
||||
import { useNavigate } from 'react-router';
|
||||
|
||||
import { callAction } from 'api/app';
|
||||
import { ACCESS_TOKEN } from 'api/endpoints';
|
||||
@@ -18,6 +18,7 @@ import { AuthenticationContext } from './context';
|
||||
|
||||
const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
const { LL } = useI18nContext();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [me, setMe] = useState<Me>();
|
||||
@@ -60,7 +61,7 @@ const Authentication: FC<RequiredChildrenProps> = ({ children }) => {
|
||||
setMe(undefined);
|
||||
setVersions(undefined);
|
||||
if (doRedirect) {
|
||||
redirect('/');
|
||||
void navigate('/', { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { StrictMode } from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import {
|
||||
Navigate,
|
||||
Outlet,
|
||||
Route,
|
||||
RouterProvider,
|
||||
createBrowserRouter,
|
||||
@@ -9,6 +11,45 @@ import {
|
||||
} from 'react-router';
|
||||
|
||||
import App from 'App';
|
||||
import SignIn from 'SignIn';
|
||||
import Commands from 'app/main/Commands';
|
||||
import CustomEntities from 'app/main/CustomEntities';
|
||||
import Customizations from 'app/main/Customizations';
|
||||
import Dashboard from 'app/main/Dashboard';
|
||||
import Devices from 'app/main/Devices';
|
||||
import Help from 'app/main/Help';
|
||||
import Modules from 'app/main/Modules';
|
||||
import Scheduler from 'app/main/Scheduler';
|
||||
import Sensors from 'app/main/Sensors';
|
||||
import UserProfile from 'app/main/UserProfile';
|
||||
import APSettings from 'app/settings/APSettings';
|
||||
import ApplicationSettings from 'app/settings/ApplicationSettings';
|
||||
import DownloadUpload from 'app/settings/DownloadUpload';
|
||||
import MqttSettings from 'app/settings/MqttSettings';
|
||||
import NTPSettings from 'app/settings/NTPSettings';
|
||||
import Settings from 'app/settings/Settings';
|
||||
import Version from 'app/settings/Version';
|
||||
import Network from 'app/settings/network/Network';
|
||||
import NetworkSettings from 'app/settings/network/NetworkSettings';
|
||||
import WiFiNetworkScanner from 'app/settings/network/WiFiNetworkScanner';
|
||||
import ManageUsers from 'app/settings/security/ManageUsers';
|
||||
import Security from 'app/settings/security/Security';
|
||||
import SecuritySettings from 'app/settings/security/SecuritySettings';
|
||||
import APStatus from 'app/status/APStatus';
|
||||
import Activity from 'app/status/Activity';
|
||||
import HardwareStatus from 'app/status/HardwareStatus';
|
||||
import MqttStatus from 'app/status/MqttStatus';
|
||||
import NTPStatus from 'app/status/NTPStatus';
|
||||
import NetworkStatus from 'app/status/NetworkStatus';
|
||||
import Status from 'app/status/Status';
|
||||
import SystemLog from 'app/status/SystemLog';
|
||||
import {
|
||||
Layout,
|
||||
RequireAdmin,
|
||||
RequireAuthenticated,
|
||||
RequireUnauthenticated,
|
||||
RootRedirect
|
||||
} from 'components';
|
||||
|
||||
const errorPageStyles = {
|
||||
container: {
|
||||
@@ -105,7 +146,87 @@ function ErrorPage() {
|
||||
|
||||
const router = createBrowserRouter(
|
||||
createRoutesFromElements(
|
||||
<Route path="/*" element={<App />} errorElement={<ErrorPage />} />
|
||||
<Route path="/" element={<App />} errorElement={<ErrorPage />}>
|
||||
<Route
|
||||
index
|
||||
element={
|
||||
<RequireUnauthenticated>
|
||||
<SignIn />
|
||||
</RequireUnauthenticated>
|
||||
}
|
||||
/>
|
||||
<Route path="unauthorized" element={<RootRedirect kind="unauthorized" />} />
|
||||
<Route path="fileUpdated" element={<RootRedirect kind="fileUpdated" />} />
|
||||
|
||||
<Route
|
||||
element={
|
||||
<RequireAuthenticated>
|
||||
<Layout>
|
||||
<Outlet />
|
||||
</Layout>
|
||||
</RequireAuthenticated>
|
||||
}
|
||||
>
|
||||
<Route path="dashboard/*" element={<Dashboard />} />
|
||||
<Route path="devices/*" element={<Devices />} />
|
||||
<Route path="sensors/*" element={<Sensors />} />
|
||||
<Route path="help/*" element={<Help />} />
|
||||
<Route path="user/*" element={<UserProfile />} />
|
||||
|
||||
<Route path="status/*" element={<Status />} />
|
||||
<Route path="status/hardwarestatus/*" element={<HardwareStatus />} />
|
||||
<Route path="status/activity" element={<Activity />} />
|
||||
<Route path="status/log" element={<SystemLog />} />
|
||||
<Route path="status/mqtt" element={<MqttStatus />} />
|
||||
<Route path="status/ntp" element={<NTPStatus />} />
|
||||
<Route path="status/ap" element={<APStatus />} />
|
||||
<Route path="status/network" element={<NetworkStatus />} />
|
||||
|
||||
<Route element={<RequireAdmin />}>
|
||||
<Route path="settings" element={<Settings />} />
|
||||
<Route path="settings/version" element={<Version />} />
|
||||
<Route path="settings/application" element={<ApplicationSettings />} />
|
||||
<Route path="settings/mqtt" element={<MqttSettings />} />
|
||||
<Route path="settings/ntp" element={<NTPSettings />} />
|
||||
<Route path="settings/ap" element={<APSettings />} />
|
||||
<Route path="settings/modules" element={<Modules />} />
|
||||
<Route path="settings/downloadUpload" element={<DownloadUpload />} />
|
||||
|
||||
<Route path="settings/network" element={<Network />}>
|
||||
<Route
|
||||
index
|
||||
element={<Navigate replace to="/settings/network/settings" />}
|
||||
/>
|
||||
<Route path="settings" element={<NetworkSettings />} />
|
||||
<Route path="scan" element={<WiFiNetworkScanner />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate replace to="/settings/network/settings" />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="settings/security" element={<Security />}>
|
||||
<Route
|
||||
index
|
||||
element={<Navigate replace to="/settings/security/settings" />}
|
||||
/>
|
||||
<Route path="settings" element={<SecuritySettings />} />
|
||||
<Route path="users" element={<ManageUsers />} />
|
||||
<Route
|
||||
path="*"
|
||||
element={<Navigate replace to="/settings/security/settings" />}
|
||||
/>
|
||||
</Route>
|
||||
|
||||
<Route path="customizations" element={<Customizations />} />
|
||||
<Route path="commands" element={<Commands />} />
|
||||
<Route path="scheduler" element={<Scheduler />} />
|
||||
<Route path="customentities" element={<CustomEntities />} />
|
||||
</Route>
|
||||
|
||||
<Route path="*" element={<Navigate replace to="/" />} />
|
||||
</Route>
|
||||
</Route>
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user