237 lines
5.7 KiB
TypeScript
237 lines
5.7 KiB
TypeScript
import {
|
|
BrowserRouter,
|
|
Routes,
|
|
Route,
|
|
NavLink,
|
|
useLocation,
|
|
} from "react-router-dom";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import { ThemeProvider } from "@mui/material/styles";
|
|
import {
|
|
AppBar,
|
|
Toolbar,
|
|
Typography,
|
|
Box,
|
|
Tabs,
|
|
Tab,
|
|
Container,
|
|
CssBaseline,
|
|
Chip,
|
|
useMediaQuery,
|
|
Button,
|
|
Stack,
|
|
Card,
|
|
CardContent,
|
|
CircularProgress,
|
|
} from "@mui/material";
|
|
import { useEffect, useMemo } from "react";
|
|
import { AuthProvider, useAuth } from "react-oidc-context";
|
|
import { Dashboard } from "./pages/Dashboard";
|
|
import { Monitoring } from "./pages/Monitoring";
|
|
import { Media } from "./pages/Media";
|
|
import { UsersPage } from "./pages/Users";
|
|
import { FileBrowser } from "./pages/FileBrowser";
|
|
import { getAppTheme } from "./theme";
|
|
import { getOidcConfig, isOidcConfigured, setAccessToken } from "./auth";
|
|
|
|
const queryClient = new QueryClient({
|
|
defaultOptions: { queries: { retry: 1, refetchOnWindowFocus: false } },
|
|
});
|
|
|
|
function Shell({
|
|
darkMode,
|
|
authLabel,
|
|
onSignOut,
|
|
}: {
|
|
darkMode: boolean;
|
|
authLabel?: string;
|
|
onSignOut?: () => void;
|
|
}) {
|
|
const location = useLocation();
|
|
const current = location.pathname;
|
|
|
|
return (
|
|
<>
|
|
<CssBaseline />
|
|
<AppBar position="sticky" color="inherit" elevation={0}>
|
|
<Toolbar sx={{ display: "flex", gap: 2, minHeight: 68 }}>
|
|
<Typography variant="h6" sx={{ mr: 2, fontWeight: 700 }}>
|
|
Media Library Viewer
|
|
</Typography>
|
|
<Tabs
|
|
value={current}
|
|
textColor="primary"
|
|
indicatorColor="primary"
|
|
sx={{ flex: 1 }}
|
|
>
|
|
<Tab value="/" label="Dashboard" component={NavLink} to="/" />
|
|
<Tab
|
|
value="/monitoring"
|
|
label="Monitoring"
|
|
component={NavLink}
|
|
to="/monitoring"
|
|
/>
|
|
<Tab value="/media" label="Media" component={NavLink} to="/media" />
|
|
<Tab value="/users" label="Users" component={NavLink} to="/users" />
|
|
<Tab
|
|
value="/files"
|
|
label="File Browser"
|
|
component={NavLink}
|
|
to="/files"
|
|
/>
|
|
</Tabs>
|
|
<Stack direction="row" spacing={1} sx={{ alignItems: "center" }}>
|
|
{authLabel && (
|
|
<Chip size="small" variant="outlined" label={authLabel} />
|
|
)}
|
|
<Chip
|
|
size="small"
|
|
variant="outlined"
|
|
label={darkMode ? "Dark" : "Light"}
|
|
/>
|
|
{onSignOut && (
|
|
<Button size="small" variant="text" onClick={onSignOut}>
|
|
Sign out
|
|
</Button>
|
|
)}
|
|
</Stack>
|
|
</Toolbar>
|
|
</AppBar>
|
|
<Container maxWidth={false} sx={{ py: 3 }}>
|
|
<Routes>
|
|
<Route path="/" element={<Dashboard />} />
|
|
<Route path="/monitoring" element={<Monitoring />} />
|
|
<Route path="/media" element={<Media />} />
|
|
<Route path="/users" element={<UsersPage />} />
|
|
<Route path="/files" element={<FileBrowser />} />
|
|
</Routes>
|
|
</Container>
|
|
</>
|
|
);
|
|
}
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
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 the media library viewer.
|
|
</Typography>
|
|
<Button variant="contained" onClick={onSignIn}>
|
|
Sign in with OIDC
|
|
</Button>
|
|
</Stack>
|
|
</CardContent>
|
|
</Card>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function AuthenticatedApp({ darkMode }: { darkMode: boolean }) {
|
|
const auth = useAuth();
|
|
useEffect(() => {
|
|
setAccessToken(auth.user?.access_token ?? null);
|
|
}, [auth.user?.access_token]);
|
|
|
|
const authLabel = useMemo(() => {
|
|
const profile = auth.user?.profile as Record<string, unknown> | undefined;
|
|
return String(
|
|
profile?.name ??
|
|
profile?.preferred_username ??
|
|
profile?.email ??
|
|
auth.user?.profile?.sub ??
|
|
"Authenticated",
|
|
);
|
|
}, [auth.user]);
|
|
|
|
if (auth.isLoading || auth.activeNavigator) {
|
|
return <LoadingScreen label="Checking sign-in…" />;
|
|
}
|
|
|
|
if (auth.error) {
|
|
return (
|
|
<LoadingScreen
|
|
label={`Authentication error: ${auth.error.message || "Unable to sign in"}`}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (!auth.isAuthenticated) {
|
|
return <SignInScreen onSignIn={() => void auth.signinRedirect()} />;
|
|
}
|
|
|
|
return (
|
|
<Box sx={{ minHeight: "100vh", bgcolor: "background.default" }}>
|
|
<BrowserRouter>
|
|
<Shell
|
|
darkMode={darkMode}
|
|
authLabel={authLabel}
|
|
onSignOut={() => void auth.signoutRedirect()}
|
|
/>
|
|
</BrowserRouter>
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
function AppInner() {
|
|
const prefersDarkMode = useMediaQuery("(prefers-color-scheme: dark)");
|
|
const theme = useMemo(
|
|
() => getAppTheme(prefersDarkMode ? "dark" : "light"),
|
|
[prefersDarkMode],
|
|
);
|
|
|
|
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>
|
|
);
|
|
}
|
|
|
|
export default function App() {
|
|
return <AppInner />;
|
|
}
|