228 lines
6.6 KiB
TypeScript
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>;
|
|
}
|
|
}
|