Files
nxdns/web/src/routes.tsx
T
mokhtar 6c507992e4
CI / test (push) Successful in 1m46s
CI / test-aarch64 (push) Successful in 5m30s
CI / frontend (push) Successful in 46s
CI / cross (push) Successful in 8m12s
CI / docker (push) Successful in 3m46s
milestone 19: hygiene sweep - dead ecs surface, single-source constants, tls classification, frontend state hazards, docker smoke network fix
2026-08-07 20:39:27 +02:00

228 lines
6.6 KiB
TypeScript

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,
queriesInfiniteQuery,
rulesQuery,
settingsQuery,
statsQuery,
timeseriesQuery,
upstreamHealthQuery,
upstreamsQuery,
} from "@/lib/queries";
import { retryButtonClass } from "@/ui/classes";
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={retryButtonClass}>
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: "/",
// allSettled, not all: DashboardPage reads these with useQuery so each widget
// can render its own error. A rejecting loader would replace the whole page
// with RouteError and take the three healthy widgets down with the failed one.
loader: ({ context }) =>
Promise.allSettled([
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.ensureInfiniteQueryData(queriesInfiniteQuery({})),
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 upstreamsRoute = createRoute({
getParentRoute: () => shellRoute,
path: "/upstreams",
loader: ({ context }) => context.queryClient.ensureQueryData(upstreamsQuery()),
component: lazyRouteComponent(() => import("@/features/upstreams/UpstreamsPage")),
});
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,
upstreamsRoute,
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>;
}
}