Build new shell layout with left sidebar (el-3po)
This commit is contained in:
+371
-262
@@ -4,30 +4,14 @@ import {
|
||||
Route,
|
||||
NavLink,
|
||||
useLocation,
|
||||
Outlet,
|
||||
} from "react-router-dom";
|
||||
import {
|
||||
QueryClient,
|
||||
QueryClientProvider,
|
||||
useQuery,
|
||||
} from "@tanstack/react-query";
|
||||
import { ThemeProvider } from "@mui/material/styles";
|
||||
import {
|
||||
AppBar,
|
||||
Typography,
|
||||
Box,
|
||||
Tabs,
|
||||
Tab,
|
||||
Container,
|
||||
CssBaseline,
|
||||
Chip,
|
||||
useMediaQuery,
|
||||
Button,
|
||||
Stack,
|
||||
Card,
|
||||
CardContent,
|
||||
CircularProgress,
|
||||
} from "@mui/material";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { AuthProvider, useAuth } from "react-oidc-context";
|
||||
import { Dashboard } from "./pages/Dashboard";
|
||||
import { Monitoring } from "./pages/Monitoring";
|
||||
@@ -37,27 +21,209 @@ import { UsersPage } from "./pages/Users";
|
||||
import { FileBrowser } from "./pages/FileBrowser";
|
||||
import { Actions } from "./pages/Actions";
|
||||
import BackupsPage from "./components/BackupsPage";
|
||||
import { getAppTheme } from "./theme";
|
||||
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
||||
import { fetchAppVersion } from "./api/client";
|
||||
import { FRONTEND_VERSION_LABEL } from "./version";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipProvider,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetTrigger,
|
||||
} from "@/components/ui/sheet";
|
||||
import {
|
||||
LayoutDashboard,
|
||||
Activity,
|
||||
Monitor,
|
||||
Users,
|
||||
Zap,
|
||||
FolderOpen,
|
||||
Settings as SettingsIcon,
|
||||
Menu,
|
||||
Sun,
|
||||
Moon,
|
||||
LogOut,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
} from "lucide-react";
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
||||
});
|
||||
|
||||
function Shell({
|
||||
// Navigation items for sidebar
|
||||
const navItems = [
|
||||
{ path: "/", label: "Dashboard", icon: LayoutDashboard },
|
||||
{ path: "/monitoring", label: "Monitoring", icon: Activity },
|
||||
{ path: "/applications", label: "Media", icon: Monitor },
|
||||
{ path: "/files", label: "Files", icon: FolderOpen },
|
||||
{ path: "/users", label: "Users", icon: Users },
|
||||
{ path: "/actions", label: "Actions", icon: Zap },
|
||||
{ path: "/settings", label: "Settings", icon: SettingsIcon },
|
||||
];
|
||||
|
||||
function Sidebar({
|
||||
collapsed,
|
||||
onToggle,
|
||||
isMobile,
|
||||
}: {
|
||||
collapsed: boolean;
|
||||
onToggle: () => void;
|
||||
isMobile: boolean;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
|
||||
if (isMobile) return null;
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={`fixed left-0 top-0 z-40 h-screen border-r border-border bg-card transition-all duration-300 ${
|
||||
collapsed ? "w-16" : "w-60"
|
||||
}`}
|
||||
>
|
||||
<div className="flex h-full flex-col">
|
||||
{/* Logo area */}
|
||||
<div className="flex h-14 items-center justify-between border-b border-border px-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground">
|
||||
M
|
||||
</div>
|
||||
{!collapsed && (
|
||||
<span className="font-semibold">Manage</span>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={onToggle}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 space-y-1 p-2">
|
||||
<TooltipProvider delayDuration={0}>
|
||||
{navItems.map((item) => {
|
||||
const isItemActive = location.pathname === item.path;
|
||||
const Icon = item.icon;
|
||||
|
||||
return collapsed ? (
|
||||
<Tooltip key={item.path}>
|
||||
<TooltipTrigger asChild>
|
||||
<NavLink
|
||||
to={item.path}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center justify-center rounded-md transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5" />
|
||||
</NavLink>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">
|
||||
{item.label}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center gap-3 rounded-md px-3 transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</TooltipProvider>
|
||||
</nav>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
|
||||
function MobileDrawer() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const location = useLocation();
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="md:hidden">
|
||||
<Menu className="h-5 w-5" />
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left" className="w-60 p-0">
|
||||
<SheetHeader className="border-b border-border p-4">
|
||||
<SheetTitle className="flex items-center gap-2">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-lg bg-primary font-bold text-primary-foreground">
|
||||
M
|
||||
</div>
|
||||
<span>Manage</span>
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<nav className="space-y-1 p-2">
|
||||
{navItems.map((item) => {
|
||||
const isItemActive = location.pathname === item.path;
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
onClick={() => setOpen(false)}
|
||||
className={() =>
|
||||
`flex h-10 w-full items-center gap-3 rounded-md px-3 transition-colors ${
|
||||
isItemActive
|
||||
? "bg-primary text-primary-foreground"
|
||||
: "text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
}`
|
||||
}
|
||||
>
|
||||
<Icon className="h-5 w-5 shrink-0" />
|
||||
<span className="text-sm font-medium">{item.label}</span>
|
||||
</NavLink>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function TopBar({
|
||||
darkMode,
|
||||
authLabel,
|
||||
onSignOut,
|
||||
onToggleDarkMode,
|
||||
}: {
|
||||
darkMode: boolean;
|
||||
authLabel?: string;
|
||||
onSignOut?: () => void;
|
||||
onToggleDarkMode: () => void;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const current = location.pathname;
|
||||
const isMobile = useMediaQuery("(max-width: 900px)");
|
||||
const { data: appVersion } = useQuery({
|
||||
queryKey: ["app-version"],
|
||||
queryFn: fetchAppVersion,
|
||||
@@ -65,238 +231,148 @@ function Shell({
|
||||
});
|
||||
const backendLabel = appVersion?.backend_label || "…";
|
||||
|
||||
const pageTitle =
|
||||
navItems.find((item) => item.path === location.pathname)?.label ||
|
||||
"Dashboard";
|
||||
|
||||
return (
|
||||
<>
|
||||
<CssBaseline />
|
||||
<AppBar
|
||||
position="sticky"
|
||||
color="inherit"
|
||||
elevation={0}
|
||||
sx={{
|
||||
borderBottom: 1,
|
||||
borderColor: "divider",
|
||||
backgroundImage: "none",
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: { xs: "column", md: "row" },
|
||||
alignItems: { xs: "stretch", md: "center" },
|
||||
justifyContent: "space-between",
|
||||
gap: 1.5,
|
||||
px: { xs: 1.5, sm: 2.5, md: 3 },
|
||||
py: { xs: 1, md: 1.25 },
|
||||
}}
|
||||
<header className="sticky top-0 z-30 flex h-14 items-center justify-between border-b border-border bg-background/80 backdrop-blur-md px-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<MobileDrawer />
|
||||
<h1 className="text-lg font-semibold">{pageTitle}</h1>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="hidden sm:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium">
|
||||
FE {FRONTEND_VERSION_LABEL}
|
||||
</span>
|
||||
<span className="hidden sm:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium">
|
||||
BE {backendLabel}
|
||||
</span>
|
||||
{authLabel && (
|
||||
<span className="hidden md:inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-medium max-w-[180px] truncate">
|
||||
{authLabel}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={onToggleDarkMode}
|
||||
className="h-8 w-8"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 1.5,
|
||||
minWidth: 0,
|
||||
}}
|
||||
{darkMode ? (
|
||||
<Sun className="h-4 w-4" />
|
||||
) : (
|
||||
<Moon className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
{onSignOut && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={onSignOut}
|
||||
className="gap-2"
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
width: 42,
|
||||
height: 42,
|
||||
borderRadius: 2,
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
fontWeight: 800,
|
||||
fontSize: 20,
|
||||
color: "primary.contrastText",
|
||||
bgcolor: "primary.main",
|
||||
backgroundImage:
|
||||
"linear-gradient(135deg, rgba(255,255,255,0.22), rgba(255,255,255,0))",
|
||||
boxShadow: 1,
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
M
|
||||
</Box>
|
||||
<Box sx={{ minWidth: 0 }}>
|
||||
<Typography
|
||||
variant="h6"
|
||||
sx={{ fontWeight: 800, lineHeight: 1.05 }}
|
||||
>
|
||||
Manage
|
||||
</Typography>
|
||||
<Typography variant="caption" color="text.secondary" noWrap>
|
||||
Media operations dashboard
|
||||
</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
<LogOut className="h-4 w-4" />
|
||||
<span className="hidden sm:inline">Logout</span>
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={1}
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: { xs: "flex-start", md: "flex-end" },
|
||||
width: { xs: "100%", md: "auto" },
|
||||
}}
|
||||
>
|
||||
<Chip
|
||||
label={`FE ${FRONTEND_VERSION_LABEL}`}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
<Chip
|
||||
label={`BE ${backendLabel}`}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
{authLabel && (
|
||||
<Chip
|
||||
label={authLabel}
|
||||
variant="outlined"
|
||||
sx={{ maxWidth: { xs: "100%", sm: 220 } }}
|
||||
/>
|
||||
)}
|
||||
<Chip label={darkMode ? "Dark" : "Light"} variant="outlined" />
|
||||
{onSignOut && (
|
||||
<Button
|
||||
variant="outlined"
|
||||
onClick={onSignOut}
|
||||
sx={{ whiteSpace: "nowrap" }}
|
||||
>
|
||||
Logout
|
||||
</Button>
|
||||
)}
|
||||
</Stack>
|
||||
</Box>
|
||||
function ShellLayout({
|
||||
darkMode,
|
||||
authLabel,
|
||||
onSignOut,
|
||||
onToggleDarkMode,
|
||||
}: {
|
||||
darkMode: boolean;
|
||||
authLabel?: string;
|
||||
onSignOut?: () => void;
|
||||
onToggleDarkMode: () => void;
|
||||
}) {
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(() =>
|
||||
window.matchMedia("(max-width: 768px)").matches,
|
||||
);
|
||||
|
||||
<Box sx={{ borderTop: 1, borderColor: "divider" }}>
|
||||
<Tabs
|
||||
value={current}
|
||||
textColor="primary"
|
||||
indicatorColor="primary"
|
||||
variant={isMobile ? "scrollable" : "standard"}
|
||||
scrollButtons="auto"
|
||||
allowScrollButtonsMobile
|
||||
sx={{
|
||||
px: { xs: 0.75, sm: 1.5, md: 3 },
|
||||
minHeight: 44,
|
||||
"& .MuiTab-root": {
|
||||
minHeight: 44,
|
||||
py: 1,
|
||||
px: 1.5,
|
||||
minWidth: { xs: 88, md: 108 },
|
||||
textTransform: "none",
|
||||
fontWeight: 600,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Tab value="/" label="Dashboard" component={NavLink} to="/" />
|
||||
<Tab
|
||||
value="/monitoring"
|
||||
label="Monitoring"
|
||||
component={NavLink}
|
||||
to="/monitoring"
|
||||
/>
|
||||
<Tab
|
||||
value="/applications"
|
||||
label="Applications"
|
||||
component={NavLink}
|
||||
to="/applications"
|
||||
/>
|
||||
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
||||
<Tab
|
||||
value="/actions"
|
||||
label="Actions"
|
||||
component={NavLink}
|
||||
to="/actions"
|
||||
/>
|
||||
<Tab value="/files" label="Files" component={NavLink} to="/files" />
|
||||
<Tab
|
||||
value="/backups"
|
||||
label="Backups"
|
||||
component={NavLink}
|
||||
to="/backups"
|
||||
/>
|
||||
<Tab
|
||||
value="/settings"
|
||||
label="Settings"
|
||||
component={NavLink}
|
||||
to="/settings"
|
||||
/>
|
||||
</Tabs>
|
||||
</Box>
|
||||
</AppBar>
|
||||
<Container
|
||||
maxWidth={false}
|
||||
sx={{ py: { xs: 1.5, md: 2 }, px: { xs: 1.25, sm: 2, md: 2.5 } }}
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(max-width: 768px)");
|
||||
const handler = (e: MediaQueryListEvent) => setIsMobile(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
<Sidebar
|
||||
collapsed={sidebarCollapsed}
|
||||
onToggle={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
isMobile={isMobile}
|
||||
/>
|
||||
<div
|
||||
className={`transition-all duration-300 ${
|
||||
isMobile ? "" : sidebarCollapsed ? "ml-16" : "ml-60"
|
||||
}`}
|
||||
>
|
||||
<Routes>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Routes>
|
||||
</Container>
|
||||
</>
|
||||
<TopBar
|
||||
darkMode={darkMode}
|
||||
authLabel={authLabel}
|
||||
onSignOut={onSignOut}
|
||||
onToggleDarkMode={onToggleDarkMode}
|
||||
/>
|
||||
<main className="p-4 md:p-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingScreen({ label }: { label: string }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Card variant="outlined" sx={{ maxWidth: 420, width: "100%" }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2} sx={{ alignItems: "center", textAlign: "center" }}>
|
||||
<CircularProgress />
|
||||
<Typography variant="h6">{label}</Typography>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
<div className="min-h-screen grid place-items-center p-4">
|
||||
<div className="w-full max-w-md rounded-xl border bg-card p-6 shadow-sm">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<div className="h-8 w-8 animate-spin rounded-full border-4 border-primary border-t-transparent" />
|
||||
<h2 className="text-lg font-semibold">{label}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SignInScreen({ onSignIn }: { onSignIn: () => void }) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "grid",
|
||||
placeItems: "center",
|
||||
p: 2,
|
||||
}}
|
||||
>
|
||||
<Card variant="outlined" sx={{ maxWidth: 460, width: "100%" }}>
|
||||
<CardContent>
|
||||
<Stack spacing={2} sx={{ alignItems: "center", textAlign: "center" }}>
|
||||
<Typography variant="h5">Sign in required</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
Use your Authentik account to access Manage.
|
||||
</Typography>
|
||||
<Button variant="contained" onClick={onSignIn}>
|
||||
Sign in with OIDC
|
||||
</Button>
|
||||
</Stack>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
<div className="min-h-screen grid place-items-center p-4">
|
||||
<div className="w-full max-w-md rounded-xl border bg-card p-6 shadow-sm">
|
||||
<div className="flex flex-col items-center gap-3 text-center">
|
||||
<h2 className="text-xl font-semibold">Sign in required</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Use your Authentik account to access Manage.
|
||||
</p>
|
||||
<Button onClick={onSignIn}>Sign in with OIDC</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function AuthenticatedApp({ darkMode }: { darkMode: boolean }) {
|
||||
function AuthenticatedApp() {
|
||||
const auth = useAuth();
|
||||
const [darkMode, setDarkMode] = useState(() =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = (e: MediaQueryListEvent) => setDarkMode(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
setAccessToken(auth.user?.access_token ?? null);
|
||||
}, [auth.user?.access_token]);
|
||||
@@ -329,41 +405,74 @@ function AuthenticatedApp({ darkMode }: { darkMode: boolean }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
||||
<BrowserRouter>
|
||||
<Shell
|
||||
darkMode={darkMode}
|
||||
authLabel={authLabel}
|
||||
onSignOut={() => void auth.signoutRedirect()}
|
||||
/>
|
||||
</BrowserRouter>
|
||||
</Box>
|
||||
<ShellLayout
|
||||
darkMode={darkMode}
|
||||
authLabel={authLabel}
|
||||
onSignOut={() => void auth.signoutRedirect()}
|
||||
onToggleDarkMode={() => setDarkMode(!darkMode)}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function AppInner() {
|
||||
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
||||
const theme = useMemo(
|
||||
() => getAppTheme(prefersDarkMode ? "dark" : "light"),
|
||||
[prefersDarkMode],
|
||||
const [prefersDarkMode, setPrefersDarkMode] = useState(() =>
|
||||
window.matchMedia("(prefers-color-scheme: dark)").matches,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const mql = window.matchMedia("(prefers-color-scheme: dark)");
|
||||
const handler = (e: MediaQueryListEvent) => setPrefersDarkMode(e.matches);
|
||||
mql.addEventListener("change", handler);
|
||||
return () => mql.removeEventListener("change", handler);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ThemeProvider theme={theme}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{isOidcConfigured() ? (
|
||||
<AuthProvider {...getOidcConfig()}>
|
||||
<AuthenticatedApp darkMode={prefersDarkMode} />
|
||||
</AuthProvider>
|
||||
) : (
|
||||
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
||||
<BrowserRouter>
|
||||
<Shell darkMode={prefersDarkMode} />
|
||||
</BrowserRouter>
|
||||
</Box>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
</ThemeProvider>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{isOidcConfigured() ? (
|
||||
<AuthProvider {...getOidcConfig()}>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<AuthenticatedApp />}>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
</AuthProvider>
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<ShellLayout
|
||||
darkMode={prefersDarkMode}
|
||||
onToggleDarkMode={() =>
|
||||
setPrefersDarkMode(!prefersDarkMode)
|
||||
}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Route path="/" element={<Dashboard />} />
|
||||
<Route path="/monitoring" element={<Monitoring />} />
|
||||
<Route path="/applications" element={<Applications />} />
|
||||
<Route path="/media" element={<Applications />} />
|
||||
<Route path="/users" element={<UsersPage />} />
|
||||
<Route path="/actions" element={<Actions />} />
|
||||
<Route path="/files" element={<FileBrowser />} />
|
||||
<Route path="/backups" element={<BackupsPage />} />
|
||||
<Route path="/settings" element={<Settings />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user