277 lines
6.4 KiB
TypeScript
277 lines
6.4 KiB
TypeScript
import {
|
|
Component,
|
|
type ErrorInfo,
|
|
type ReactNode,
|
|
lazy,
|
|
Suspense,
|
|
useEffect,
|
|
useRef,
|
|
} from "react";
|
|
import {
|
|
Navigate,
|
|
NavLink,
|
|
Route,
|
|
Routes,
|
|
useLocation,
|
|
} from "react-router-dom";
|
|
|
|
import type { BackupToolClient, Components } from "../api/generated/client";
|
|
import type { OperatorPageProps } from "./OperatorPages";
|
|
|
|
type SessionUser = Components["schemas"]["SessionUser"];
|
|
|
|
type RouteDefinition = {
|
|
path: string;
|
|
label: string;
|
|
title: string;
|
|
component: React.LazyExoticComponent<React.ComponentType<OperatorPageProps>>;
|
|
};
|
|
|
|
const DashboardPage = lazy(() =>
|
|
import("./OperatorPages").then(({ DashboardPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const SourcesPage = lazy(() =>
|
|
import("./OperatorPages").then(({ SourcesPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const RepositoriesPage = lazy(() =>
|
|
import("./OperatorPages").then(({ RepositoriesPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const JobsPage = lazy(() =>
|
|
import("./OperatorPages").then(({ JobsPage: Page }) => ({ default: Page })),
|
|
);
|
|
const ExecutionsPage = lazy(() =>
|
|
import("./OperatorPages").then(({ ExecutionsPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const BackupsPage = lazy(() =>
|
|
import("./M13Workflows").then(({ BackupsPage: Page }) => ({ default: Page })),
|
|
);
|
|
const SecurityPage = lazy(() =>
|
|
import("./M13Workflows").then(({ SecurityPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const NotificationsPage = lazy(() =>
|
|
import("./M13Workflows").then(({ NotificationsPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
const AuditPage = lazy(() =>
|
|
import("./M13Workflows").then(({ AuditPage: Page }) => ({ default: Page })),
|
|
);
|
|
const AdministrationPage = lazy(() =>
|
|
import("./AdministrationPage").then(({ AdministrationPage: Page }) => ({
|
|
default: Page,
|
|
})),
|
|
);
|
|
|
|
const routes: RouteDefinition[] = [
|
|
{
|
|
path: "/dashboard",
|
|
label: "Dashboard",
|
|
title: "Dashboard",
|
|
component: DashboardPage,
|
|
},
|
|
{
|
|
path: "/sources",
|
|
label: "Sources",
|
|
title: "Sources",
|
|
component: SourcesPage,
|
|
},
|
|
{
|
|
path: "/repositories",
|
|
label: "Repositories",
|
|
title: "Repositories",
|
|
component: RepositoriesPage,
|
|
},
|
|
{
|
|
path: "/jobs",
|
|
label: "Jobs & schedules",
|
|
title: "Jobs & schedules",
|
|
component: JobsPage,
|
|
},
|
|
{
|
|
path: "/executions",
|
|
label: "Executions",
|
|
title: "Executions",
|
|
component: ExecutionsPage,
|
|
},
|
|
{
|
|
path: "/backups",
|
|
label: "Backups",
|
|
title: "Backups",
|
|
component: BackupsPage,
|
|
},
|
|
{
|
|
path: "/security",
|
|
label: "Security & recovery",
|
|
title: "Security & recovery",
|
|
component: SecurityPage,
|
|
},
|
|
{
|
|
path: "/notifications",
|
|
label: "Notifications",
|
|
title: "Notifications",
|
|
component: NotificationsPage,
|
|
},
|
|
{ path: "/audit", label: "Audit", title: "Audit", component: AuditPage },
|
|
{
|
|
path: "/administration",
|
|
label: "Administration",
|
|
title: "Administration",
|
|
component: AdministrationPage,
|
|
},
|
|
];
|
|
|
|
class RouteErrorBoundary extends Component<
|
|
{ children: ReactNode },
|
|
{ error?: Error }
|
|
> {
|
|
state: { error?: Error } = {};
|
|
|
|
static getDerivedStateFromError(error: Error) {
|
|
return { error };
|
|
}
|
|
|
|
componentDidCatch(_error: Error, _info: ErrorInfo) {}
|
|
|
|
componentDidUpdate(previousProps: Readonly<{ children: ReactNode }>) {
|
|
if (this.state.error && previousProps.children !== this.props.children)
|
|
this.setState({ error: undefined });
|
|
}
|
|
|
|
render() {
|
|
if (this.state.error)
|
|
return (
|
|
<section className="mx-auto max-w-6xl p-4 sm:p-6">
|
|
<h2 className="text-xl font-semibold">Page unavailable</h2>
|
|
<p className="mt-3" role="alert">
|
|
This page could not be loaded. Try another page or refresh your
|
|
browser.
|
|
</p>
|
|
</section>
|
|
);
|
|
return this.props.children;
|
|
}
|
|
}
|
|
|
|
function RouteLoading() {
|
|
return (
|
|
<section className="mx-auto max-w-6xl p-4 sm:p-6">
|
|
<p aria-live="polite" role="status">
|
|
Loading page…
|
|
</p>
|
|
</section>
|
|
);
|
|
}
|
|
|
|
function SkipLink() {
|
|
return (
|
|
<a
|
|
className="sr-only focus:not-sr-only focus:absolute focus:left-4 focus:top-4 focus:z-10 focus:rounded focus:bg-slate-100 focus:px-4 focus:py-2 focus:text-slate-950"
|
|
href="#main-content"
|
|
onClick={(event) => {
|
|
event.preventDefault();
|
|
document.getElementById("main-content")?.focus();
|
|
}}
|
|
>
|
|
Skip to main content
|
|
</a>
|
|
);
|
|
}
|
|
|
|
function PageRoute({
|
|
component: Page,
|
|
...props
|
|
}: RouteDefinition & OperatorPageProps) {
|
|
return (
|
|
<RouteErrorBoundary>
|
|
<Suspense fallback={<RouteLoading />}>
|
|
<Page {...props} />
|
|
</Suspense>
|
|
</RouteErrorBoundary>
|
|
);
|
|
}
|
|
|
|
function RouteEffects() {
|
|
const { pathname } = useLocation();
|
|
const hasMounted = useRef(false);
|
|
|
|
useEffect(() => {
|
|
const route = routes.find((item) => item.path === pathname);
|
|
document.title = `${route?.title ?? "Dashboard"} | Backup Tool`;
|
|
if (hasMounted.current) document.getElementById("main-content")?.focus();
|
|
hasMounted.current = true;
|
|
}, [pathname]);
|
|
|
|
return null;
|
|
}
|
|
|
|
export function OperatorViews({
|
|
client,
|
|
onSessionExpired,
|
|
user,
|
|
}: {
|
|
client: BackupToolClient;
|
|
onSessionExpired: () => void;
|
|
user: SessionUser;
|
|
}) {
|
|
const pageProps = { client, onSessionExpired };
|
|
return (
|
|
<div className="min-h-screen bg-slate-950 text-slate-100">
|
|
<SkipLink />
|
|
<header className="border-b border-slate-800 bg-slate-900">
|
|
<div className="mx-auto flex max-w-6xl flex-wrap items-center justify-between gap-4 px-4 py-5 sm:px-6">
|
|
<div>
|
|
<p className="text-sm font-semibold uppercase tracking-widest text-emerald-400">
|
|
Backup Tool
|
|
</p>
|
|
<h1 className="mt-1 text-2xl font-bold">Operator console</h1>
|
|
</div>
|
|
<p className="text-sm text-slate-300">
|
|
<span className="sr-only">Signed in as </span>
|
|
{user.username}
|
|
</p>
|
|
</div>
|
|
<nav
|
|
aria-label="Primary"
|
|
className="mx-auto flex max-w-6xl gap-1 overflow-x-auto px-4 pb-3 sm:px-6"
|
|
>
|
|
{routes.map((item) => (
|
|
<NavLink
|
|
className={({ isActive }) =>
|
|
`whitespace-nowrap rounded-md px-3 py-2 text-sm font-semibold hover:bg-slate-800 focus-visible:bg-slate-800 ${isActive ? "bg-slate-800 text-emerald-300" : ""}`
|
|
}
|
|
key={item.path}
|
|
to={item.path}
|
|
>
|
|
{item.label}
|
|
</NavLink>
|
|
))}
|
|
</nav>
|
|
</header>
|
|
<main id="main-content" tabIndex={-1}>
|
|
<RouteEffects />
|
|
<Routes>
|
|
<Route element={<Navigate replace to="/dashboard" />} path="/" />
|
|
{routes.map((route) => (
|
|
<Route
|
|
element={<PageRoute {...route} {...pageProps} />}
|
|
key={route.path}
|
|
path={route.path}
|
|
/>
|
|
))}
|
|
<Route element={<Navigate replace to="/dashboard" />} path="*" />
|
|
</Routes>
|
|
</main>
|
|
</div>
|
|
);
|
|
}
|