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 (
Loading…
); } 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 (

{title}

{detail}

); } const rootRoute = createRootRouteWithContext()(); const loginRoute = createRoute({ getParentRoute: () => rootRoute, path: "/login", validateSearch: (search: Record): { 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; } }