milestone 29: activity — history, live and policy simulation on one surface
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
Gates / test (push) Successful in 1m40s
Gates / package (push) Successful in 3m58s
Gates / container (push) Successful in 14s
CI / gates (push) Successful in 12m51s
Gates / frontend (push) Successful in 1m18s
Gates / test-aarch64 (push) Successful in 6m57s
query log, live and lookup merge into /activity. history filters live in the url, so a pasted link or back/forward reproduces the exact view; the result column separates servfail and nxdomain from success in the list. live is follow-by-default with freeze, and a streamed row opens its in-memory provenance detail — no correlation invented for rows sqlite has not written. lookup survives as the current policy simulation under /activity/test. investigation links carry absolute bounds, and the diagnostics page now honors since/until instead of ignoring them. the old routes are gone without aliases.
This commit is contained in:
+78
-54
@@ -12,7 +12,14 @@ import {
|
||||
import AppShell from "@/shell/AppShell";
|
||||
import { ApiError } from "@/lib/api";
|
||||
import { createQueryClient } from "@/lib/queryClient";
|
||||
import type { DiagnosticSeverity, DiagnosticState, DiagnosticsFilter, QueriesFilter } from "@/lib/types";
|
||||
import { diagnosticsFilterOf, type DiagnosticsSearch } from "@/features/diagnostics/filter";
|
||||
import {
|
||||
queriesFilterOf,
|
||||
validateActivitySearch,
|
||||
validateText,
|
||||
validateTimestamp,
|
||||
type ActivitySearch,
|
||||
} from "@/features/activity/search";
|
||||
import {
|
||||
blocklistsQuery,
|
||||
clientPrefixesQuery,
|
||||
@@ -140,46 +147,71 @@ const dashboardRoute = createRoute({
|
||||
});
|
||||
|
||||
/**
|
||||
* `domain` and `client` seed the filter form, so a detail page can link to
|
||||
* "every query for this domain". Anything else in the search object is dropped:
|
||||
* an unknown value would reach the api as a parameter the handler 400s.
|
||||
* Activity. The URL is the applied state: mode, the five filters, and nothing
|
||||
* else. Everything is validated by `activity/search.ts`, so a hand-typed or
|
||||
* stale parameter becomes `undefined` here rather than reaching the API as a
|
||||
* value it answers 400 to.
|
||||
*/
|
||||
const queriesRoute = createRoute({
|
||||
const activityRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/queries",
|
||||
validateSearch: (search: Record<string, unknown>): { domain?: string; client?: string } => {
|
||||
const domain = search["domain"];
|
||||
const client = search["client"];
|
||||
return {
|
||||
domain: typeof domain === "string" && domain !== "" ? domain : undefined,
|
||||
client: typeof client === "string" && client !== "" ? client : undefined,
|
||||
};
|
||||
},
|
||||
loaderDeps: ({ search }) => search,
|
||||
path: "/activity",
|
||||
validateSearch: validateActivitySearch,
|
||||
// An explicit projection, not `search` itself: the router hands the loader
|
||||
// whatever else the URL carried, and an unknown key would make two
|
||||
// otherwise-identical loads look like different deps.
|
||||
loaderDeps: ({ search }): ActivitySearch => ({
|
||||
mode: search.mode,
|
||||
since: search.since,
|
||||
until: search.until,
|
||||
domain: search.domain,
|
||||
client: search.client,
|
||||
blocked: search.blocked,
|
||||
}),
|
||||
/**
|
||||
* Starts the first page in parallel with the component chunk, and does not
|
||||
* wait for it. Awaiting would make every Apply a blocking navigation, which
|
||||
* throws away the `keepPreviousData` placeholder the list is built on: the
|
||||
* reader would lose the rows they were reading to a pending page instead of
|
||||
* watching them be replaced. The page owns the loading and error surfaces,
|
||||
* so the rejection is caught here only to keep it from going unhandled.
|
||||
*
|
||||
* Live mode reads the SSE stream and nothing else. Prefetching the log for
|
||||
* it would spend a request per navigation on rows the page never renders,
|
||||
* with the retained filters attached to make it look deliberate.
|
||||
*/
|
||||
loader: ({ context, deps }) => {
|
||||
const filter: QueriesFilter = {};
|
||||
if (deps.domain !== undefined) filter.domain = deps.domain;
|
||||
if (deps.client !== undefined) filter.client = deps.client;
|
||||
return context.queryClient.ensureInfiniteQueryData(queriesInfiniteQuery(filter));
|
||||
if (deps.mode !== "history") return;
|
||||
void context.queryClient.ensureInfiniteQueryData(queriesInfiniteQuery(queriesFilterOf(deps))).catch(() => {});
|
||||
},
|
||||
component: lazyRouteComponent(() => import("@/features/queries/QueryLogPage")),
|
||||
component: lazyRouteComponent(() => import("@/features/activity/ActivityPage")),
|
||||
});
|
||||
|
||||
const queryDetailRoute = createRoute({
|
||||
/**
|
||||
* One logged query. Its search is the Activity search the reader arrived from,
|
||||
* validated by the same functions, so the back link and every related action
|
||||
* restore the exact investigation instead of a default view of it.
|
||||
*/
|
||||
const activityDetailRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/queries/$id",
|
||||
path: "/activity/queries/$id",
|
||||
validateSearch: validateActivitySearch,
|
||||
// Swallowed on purpose, as the diagnostics detail route does: a row
|
||||
// retention has pruned is a 404 the page explains, with the way back to the
|
||||
// log. The whole-page error component would call it a request failure.
|
||||
loader: ({ context, params }) =>
|
||||
context.queryClient.ensureQueryData(queryDetailQuery(Number(params.id))).catch(() => undefined),
|
||||
component: lazyRouteComponent(() => import("@/features/queries/QueryDetailPage")),
|
||||
component: lazyRouteComponent(() => import("@/features/activity/ActivityDetailPage")),
|
||||
});
|
||||
|
||||
const liveRoute = createRoute({
|
||||
/** `domain` prefills and runs the simulation, so a query detail can link into it. */
|
||||
const activityTestRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/live",
|
||||
component: lazyRouteComponent(() => import("@/features/live/LiveLogPage")),
|
||||
path: "/activity/test",
|
||||
validateSearch: (search: Record<string, unknown>): { domain?: string } => ({
|
||||
domain: validateText(search["domain"]),
|
||||
}),
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(groupsQuery()),
|
||||
component: lazyRouteComponent(() => import("@/features/activity/PolicyTestPage")),
|
||||
});
|
||||
|
||||
const clientsRoute = createRoute({
|
||||
@@ -241,45 +273,38 @@ const upstreamsRoute = createRoute({
|
||||
component: lazyRouteComponent(() => import("@/features/upstreams/UpstreamsPage")),
|
||||
});
|
||||
|
||||
/** `domain` prefills and runs the lookup, so a query detail page can link into it. */
|
||||
const lookupRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/lookup",
|
||||
validateSearch: (search: Record<string, unknown>): { domain?: string } => {
|
||||
const domain = search["domain"];
|
||||
return { domain: typeof domain === "string" && domain !== "" ? domain : undefined };
|
||||
},
|
||||
loader: ({ context }) => context.queryClient.ensureQueryData(groupsQuery()),
|
||||
component: lazyRouteComponent(() => import("@/features/lookup/LookupPage")),
|
||||
});
|
||||
|
||||
/**
|
||||
* The three filters live in the url so an episode can be linked to as it was
|
||||
* read. Anything else in the search object is dropped: an unknown value would
|
||||
* reach the api as a query parameter the handler rejects with a 400.
|
||||
* The filters and the window live in the url so an episode can be linked to as
|
||||
* it was read — a query detail links here with an absolute five-minute window
|
||||
* around one query, which only means anything if the page applies it. Anything
|
||||
* else in the search object is dropped: an unknown value would reach the api as
|
||||
* a query parameter the handler rejects with a 400.
|
||||
*/
|
||||
const diagnosticsRoute = createRoute({
|
||||
getParentRoute: () => shellRoute,
|
||||
path: "/diagnostics",
|
||||
validateSearch: (
|
||||
search: Record<string, unknown>,
|
||||
): { state?: DiagnosticState; severity?: DiagnosticSeverity; component?: string } => {
|
||||
validateSearch: (search: Record<string, unknown>): DiagnosticsSearch => {
|
||||
const state = search["state"];
|
||||
const severity = search["severity"];
|
||||
const component = search["component"];
|
||||
return {
|
||||
state: state === "active" || state === "resolved" ? state : undefined,
|
||||
severity: severity === "warning" || severity === "error" ? severity : undefined,
|
||||
component: typeof component === "string" && component !== "" ? component : undefined,
|
||||
component: validateText(search["component"]),
|
||||
since: validateTimestamp(search["since"]),
|
||||
until: validateTimestamp(search["until"]),
|
||||
};
|
||||
},
|
||||
loaderDeps: ({ search }) => search,
|
||||
loaderDeps: ({ search }): DiagnosticsSearch => ({
|
||||
state: search.state,
|
||||
severity: search.severity,
|
||||
component: search.component,
|
||||
since: search.since,
|
||||
until: search.until,
|
||||
}),
|
||||
// allSettled: the two sections render their own state, and the resolved
|
||||
// history failing must not replace the active list with the error page.
|
||||
loader: ({ context, deps }) => {
|
||||
const base: DiagnosticsFilter = {};
|
||||
if (deps.severity !== undefined) base.severity = deps.severity;
|
||||
if (deps.component !== undefined) base.component = deps.component;
|
||||
const base = diagnosticsFilterOf(deps);
|
||||
return Promise.allSettled([
|
||||
context.queryClient.ensureInfiniteQueryData(diagnosticsInfiniteQuery({ ...base, state: "active" })),
|
||||
context.queryClient.ensureInfiniteQueryData(diagnosticsInfiniteQuery({ ...base, state: "resolved" })),
|
||||
@@ -310,16 +335,15 @@ const routeTree = rootRoute.addChildren([
|
||||
loginRoute,
|
||||
shellRoute.addChildren([
|
||||
dashboardRoute,
|
||||
queriesRoute,
|
||||
queryDetailRoute,
|
||||
liveRoute,
|
||||
activityRoute,
|
||||
activityDetailRoute,
|
||||
activityTestRoute,
|
||||
clientsRoute,
|
||||
groupsRoute,
|
||||
blocklistsRoute,
|
||||
rulesRoute,
|
||||
localDnsRoute,
|
||||
upstreamsRoute,
|
||||
lookupRoute,
|
||||
diagnosticsRoute,
|
||||
diagnosticDetailRoute,
|
||||
settingsRoute,
|
||||
|
||||
Reference in New Issue
Block a user