milestone 9: react spa admin ui, frontend ci and embedded dist
This commit is contained in:
@@ -0,0 +1,218 @@
|
||||
import type { QueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
createRootRouteWithContext,
|
||||
createRoute,
|
||||
createRouter,
|
||||
lazyRouteComponent,
|
||||
useRouter,
|
||||
type ErrorComponentProps,
|
||||
type RouterHistory,
|
||||
} from "@tanstack/react-router";
|
||||
import AppShell from "@/shell/AppShell";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { createQueryClient } from "@/lib/queryClient";
|
||||
import {
|
||||
blocklistsQuery,
|
||||
clientPrefixesQuery,
|
||||
clientsQuery,
|
||||
forwardZonesQuery,
|
||||
groupsQuery,
|
||||
healthQuery,
|
||||
localRecordsQuery,
|
||||
queriesQuery,
|
||||
rulesQuery,
|
||||
settingsQuery,
|
||||
statsQuery,
|
||||
timeseriesQuery,
|
||||
upstreamHealthQuery,
|
||||
} from "@/lib/queries";
|
||||
|
||||
export interface RouterContext {
|
||||
queryClient: QueryClient;
|
||||
}
|
||||
|
||||
function RoutePending() {
|
||||
return (
|
||||
<div className="p-8 text-center text-zinc-500" role="status">
|
||||
<span className="animate-pulse">Loading…</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RouteError({ error }: ErrorComponentProps) {
|
||||
const router = useRouter();
|
||||
let title = "Something went wrong";
|
||||
let detail = error.message;
|
||||
if (error instanceof ApiError) {
|
||||
if (error.status === 503) {
|
||||
title = "Server starting or degraded";
|
||||
detail = error.message;
|
||||
} else if (error.status === 429) {
|
||||
title = "Rate limited";
|
||||
detail = error.retryAfter !== undefined ? `Try again in ${error.retryAfter}s.` : "Try again shortly.";
|
||||
} else if (error.status >= 500) {
|
||||
title = "Internal error";
|
||||
} else {
|
||||
title = `Request failed (${error.status})`;
|
||||
}
|
||||
}
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
className="m-4 rounded border border-red-300 bg-red-50 p-4 dark:border-red-900 dark:bg-red-950"
|
||||
>
|
||||
<h2 className="font-semibold text-red-800 dark:text-red-200">{title}</h2>
|
||||
<p className="mt-1 text-sm text-red-700 dark:text-red-300">{detail}</p>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void router.invalidate()}
|
||||
className="mt-3 rounded border border-red-300 px-3 py-1.5 text-sm font-medium text-red-800 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-red-800 dark:text-red-200"
|
||||
>
|
||||
Retry
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const rootRoute = createRootRouteWithContext<RouterContext>()();
|
||||
|
||||
const loginRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/login",
|
||||
validateSearch: (search: Record<string, unknown>): { redirect?: string } => ({
|
||||
redirect: typeof search["redirect"] === "string" ? search["redirect"] : undefined,
|
||||
}),
|
||||
component: lazyRouteComponent(() => import("@/auth/LoginPage")),
|
||||
});
|
||||
|
||||
const shellRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
id: "shell",
|
||||
component: AppShell,
|
||||
});
|
||||
|
||||
const dashboardRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/",
|
||||
loader: ({ context }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(statsQuery("24h")),
|
||||
context.queryClient.ensureQueryData(timeseriesQuery("24h")),
|
||||
context.queryClient.ensureQueryData(healthQuery()),
|
||||
context.queryClient.ensureQueryData(upstreamHealthQuery()),
|
||||
]),
|
||||
component: lazyRouteComponent(() => import("@/features/dashboard/DashboardPage")),
|
||||
});
|
||||
|
||||
const queriesRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/queries",
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(queriesQuery({})),
|
||||
component: lazyRouteComponent(() => import("@/features/queries/QueryLogPage")),
|
||||
});
|
||||
|
||||
const liveRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/live",
|
||||
component: lazyRouteComponent(() => import("@/features/live/LiveLogPage")),
|
||||
});
|
||||
|
||||
const clientsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/clients",
|
||||
loader: ({ context }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(clientsQuery()),
|
||||
context.queryClient.ensureQueryData(clientPrefixesQuery()),
|
||||
context.queryClient.ensureQueryData(groupsQuery()),
|
||||
]),
|
||||
component: lazyRouteComponent(() => import("@/features/clients/ClientsPage")),
|
||||
});
|
||||
|
||||
const groupsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/groups",
|
||||
loader: ({ context }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(groupsQuery()),
|
||||
context.queryClient.ensureQueryData(blocklistsQuery()),
|
||||
]),
|
||||
component: lazyRouteComponent(() => import("@/features/groups/GroupsPage")),
|
||||
});
|
||||
|
||||
const blocklistsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/blocklists",
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(blocklistsQuery()),
|
||||
component: lazyRouteComponent(() => import("@/features/blocklists/BlocklistsPage")),
|
||||
});
|
||||
|
||||
const rulesRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/rules",
|
||||
loader: ({ context }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(rulesQuery()),
|
||||
context.queryClient.ensureQueryData(groupsQuery()),
|
||||
]),
|
||||
component: lazyRouteComponent(() => import("@/features/rules/RulesPage")),
|
||||
});
|
||||
|
||||
const localDnsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/local-dns",
|
||||
loader: ({ context }) =>
|
||||
Promise.all([
|
||||
context.queryClient.ensureQueryData(localRecordsQuery()),
|
||||
context.queryClient.ensureQueryData(forwardZonesQuery()),
|
||||
]),
|
||||
component: lazyRouteComponent(() => import("@/features/local/LocalDnsPage")),
|
||||
});
|
||||
|
||||
const lookupRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/lookup",
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(groupsQuery()),
|
||||
component: lazyRouteComponent(() => import("@/features/lookup/LookupPage")),
|
||||
});
|
||||
|
||||
const settingsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/settings",
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(settingsQuery()),
|
||||
component: lazyRouteComponent(() => import("@/features/settings/SettingsPage")),
|
||||
});
|
||||
|
||||
const routeTree = rootRoute.addChildren([
|
||||
loginRoute,
|
||||
shellRoute.addChildren([
|
||||
dashboardRoute,
|
||||
queriesRoute,
|
||||
liveRoute,
|
||||
clientsRoute,
|
||||
groupsRoute,
|
||||
blocklistsRoute,
|
||||
rulesRoute,
|
||||
localDnsRoute,
|
||||
lookupRoute,
|
||||
settingsRoute,
|
||||
]),
|
||||
]);
|
||||
|
||||
export function createAppRouter(history?: RouterHistory, queryClient: QueryClient = createQueryClient()) {
|
||||
return createRouter({
|
||||
routeTree,
|
||||
history,
|
||||
context: { queryClient },
|
||||
defaultPreload: "intent",
|
||||
defaultPreloadStaleTime: 0,
|
||||
defaultPendingComponent: RoutePending,
|
||||
defaultErrorComponent: RouteError,
|
||||
});
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
interface Register {
|
||||
router: ReturnType<typeof createAppRouter>;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user