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:
@@ -337,3 +337,38 @@ test("an episode links to its own detail page", async () => {
|
||||
const link = await screen.findByRole("link", { name: "Blocklist source failed to update" });
|
||||
expect(link.getAttribute("href")).toBe("/diagnostics/42");
|
||||
});
|
||||
|
||||
test("an absolute window reaches both requests and is stated on the page", async () => {
|
||||
const bounded = "since=1699999700&until=1700000300";
|
||||
responses[`/api/diagnostics?${bounded}&state=active`] = ACTIVE;
|
||||
responses[`/api/diagnostics?${bounded}&state=resolved`] = RESOLVED;
|
||||
renderRoute(`/diagnostics?${bounded}`);
|
||||
|
||||
await screen.findByRole("heading", { name: "Active" });
|
||||
expect(requested).toContain(`/api/diagnostics?${bounded}&state=active`);
|
||||
expect(requested).toContain(`/api/diagnostics?${bounded}&state=resolved`);
|
||||
// An empty section inside a five-minute window means something different
|
||||
// from an empty section over the whole history, so the page has to say so.
|
||||
expect(screen.getByText(/Showing events that overlap/)).toBeTruthy();
|
||||
});
|
||||
|
||||
test("clearing the range drops both bounds from the url", async () => {
|
||||
const bounded = "since=1699999700&until=1700000300";
|
||||
responses[`/api/diagnostics?${bounded}&state=active`] = ACTIVE;
|
||||
responses[`/api/diagnostics?${bounded}&state=resolved`] = RESOLVED;
|
||||
const router = renderRoute(`/diagnostics?${bounded}`);
|
||||
|
||||
fireEvent.click(await screen.findByRole("button", { name: "Clear the time range" }));
|
||||
await waitFor(() => {
|
||||
expect(router.state.location.search).not.toContain("since");
|
||||
});
|
||||
expect(router.state.location.search).not.toContain("until");
|
||||
});
|
||||
|
||||
test("a bound that is not a whole second is dropped, leaving the page unbounded", async () => {
|
||||
renderRoute("/diagnostics?since=1.5&until=Infinity");
|
||||
|
||||
await screen.findByRole("heading", { name: "Active" });
|
||||
expect(requested).toContain("/api/diagnostics?state=active");
|
||||
expect(screen.queryByText(/Showing events that overlap/)).toBeNull();
|
||||
});
|
||||
|
||||
@@ -12,18 +12,13 @@ import * as api from "@/lib/api";
|
||||
import InlineError from "@/lib/InlineError";
|
||||
import { formatDuration, formatTime } from "@/lib/format";
|
||||
import { diagnosticPurgeMutation, diagnosticsInfiniteQuery, diagnosticsPurgeResolvedMutation } from "@/lib/queries";
|
||||
import type {
|
||||
DiagnosticEvent,
|
||||
DiagnosticSeverity,
|
||||
DiagnosticState,
|
||||
DiagnosticsFilter,
|
||||
DiagnosticsPage as Page,
|
||||
} from "@/lib/types";
|
||||
import type { DiagnosticEvent, DiagnosticSeverity, DiagnosticState, DiagnosticsPage as Page } from "@/lib/types";
|
||||
import ConfirmDialog from "@/ui/ConfirmDialog";
|
||||
import Select from "@/ui/Select";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
import SeverityBadge from "./SeverityBadge";
|
||||
import { diagnosticsFilterOf } from "./filter";
|
||||
import { DIAGNOSTIC_COMPONENTS, componentLabel, copyFor } from "./eventCopy";
|
||||
|
||||
const DARK = "@media (prefers-color-scheme: dark)";
|
||||
@@ -138,6 +133,19 @@ const styles = stylex.create({
|
||||
lineHeight: "1rem",
|
||||
color: colors.textMuted,
|
||||
},
|
||||
rangeNotice: {
|
||||
marginTop: "0.75rem",
|
||||
borderRadius: "0.25rem",
|
||||
borderWidth: 1,
|
||||
borderStyle: "solid",
|
||||
borderColor: colors.border,
|
||||
backgroundColor: colors.surfaceHover,
|
||||
paddingInline: "0.75rem",
|
||||
paddingBlock: "0.5rem",
|
||||
fontSize: "0.875rem",
|
||||
lineHeight: "1.25rem",
|
||||
color: colors.textSecondary,
|
||||
},
|
||||
tableWrap: {
|
||||
marginTop: "0.75rem",
|
||||
overflowX: "auto",
|
||||
@@ -263,6 +271,33 @@ function MoreButton({ section }: { section: Section }) {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The window the page is bounded to, whenever it is bounded.
|
||||
*
|
||||
* A link from a query detail arrives with an absolute five-minute window, and
|
||||
* an empty Active section inside it means something very different from an
|
||||
* empty Active section over the whole history. The page has to say which it is
|
||||
* showing, and offer the way out of it.
|
||||
*/
|
||||
function RangeNotice({ since, until }: { since?: number; until?: number }) {
|
||||
const navigate = useNavigate({ from: "/diagnostics" });
|
||||
if (since === undefined && until === undefined) return null;
|
||||
const from = since === undefined ? "the start of the history" : formatTime(since);
|
||||
const to = until === undefined ? "now" : formatTime(until);
|
||||
return (
|
||||
<p role="status" {...stylex.props(styles.rangeNotice)}>
|
||||
Showing events that overlap {from} to {to}.{" "}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void navigate({ search: (prev) => ({ ...prev, since: undefined, until: undefined }) })}
|
||||
{...stylex.props(shared.linkButton, shared.focusRing)}
|
||||
>
|
||||
Clear the time range
|
||||
</button>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
function ActiveCard({ event, now }: { event: DiagnosticEvent; now: number }) {
|
||||
const copy = copyFor(event.code);
|
||||
return (
|
||||
@@ -327,9 +362,7 @@ export default function DiagnosticsPage() {
|
||||
const navigate = useNavigate({ from: "/diagnostics" });
|
||||
const state = search.state ?? "all";
|
||||
|
||||
const base: DiagnosticsFilter = {};
|
||||
if (search.severity !== undefined) base.severity = search.severity;
|
||||
if (search.component !== undefined) base.component = search.component;
|
||||
const base = diagnosticsFilterOf(search);
|
||||
|
||||
const active = useInfiniteQuery(diagnosticsInfiniteQuery({ ...base, state: "active" }, state !== "resolved"));
|
||||
const history = useInfiniteQuery(diagnosticsInfiniteQuery({ ...base, state: "resolved" }, state !== "active"));
|
||||
@@ -368,6 +401,8 @@ export default function DiagnosticsPage() {
|
||||
repeats, and closes when the subject recovers.
|
||||
</p>
|
||||
|
||||
<RangeNotice since={search.since} until={search.until} />
|
||||
|
||||
<div {...stylex.props(styles.filterGrid)}>
|
||||
<Select
|
||||
variant="compactField"
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* The Diagnostics search parameters and the API filter they build.
|
||||
*
|
||||
* The route, the page and the two infinite queries all have to agree on what
|
||||
* the URL asked for — the page renders the same window the loader prefetched —
|
||||
* so the projection lives in one place rather than being spelled out at each.
|
||||
*/
|
||||
|
||||
import type { DiagnosticSeverity, DiagnosticState, DiagnosticsFilter } from "@/lib/types";
|
||||
|
||||
export interface DiagnosticsSearch {
|
||||
state?: DiagnosticState;
|
||||
severity?: DiagnosticSeverity;
|
||||
component?: string;
|
||||
/** Unix seconds, inclusive. An episode qualifies when its interval overlaps. */
|
||||
since?: number;
|
||||
until?: number;
|
||||
}
|
||||
|
||||
/** Field by field, so an unset filter is an absent key rather than `undefined`. */
|
||||
export function diagnosticsFilterOf(search: DiagnosticsSearch): DiagnosticsFilter {
|
||||
const filter: DiagnosticsFilter = {};
|
||||
if (search.severity !== undefined) filter.severity = search.severity;
|
||||
if (search.component !== undefined) filter.component = search.component;
|
||||
if (search.since !== undefined) filter.since = search.since;
|
||||
if (search.until !== undefined) filter.until = search.until;
|
||||
return filter;
|
||||
}
|
||||
Reference in New Issue
Block a user