Gates / frontend (push) Successful in 1m34s
Gates / test (push) Successful in 2m3s
Gates / test-aarch64 (push) Failing after 3h13m33s
Gates / package (push) Successful in 5m20s
Gates / container (push) Successful in 15s
CI / gates (push) Failing after 6h30m45s
306 lines
9.1 KiB
TypeScript
306 lines
9.1 KiB
TypeScript
/**
|
|
* One query, explained in the order it met the pipeline.
|
|
*
|
|
* This is the body of the detail surface, with no route in it, because two
|
|
* surfaces show it: the persisted detail page, which fetched the row by id, and
|
|
* a live row, whose provenance arrived in the stream frame and which SQLite may
|
|
* not have written yet. The related actions are links into routes, so the
|
|
* caller passes them in already built.
|
|
*/
|
|
|
|
import type { ReactNode } from "react";
|
|
import * as stylex from "@stylexjs/stylex";
|
|
import { formatMicros, formatTime } from "@/lib/format";
|
|
import type { PolicyReason, Provenance } from "@/lib/types";
|
|
import { clientLabel, useClientNames } from "@/features/clients/clientNames";
|
|
import {
|
|
policyActionLabel,
|
|
policyReasonLabel,
|
|
qclassName,
|
|
rcodeName,
|
|
routeKindLabel,
|
|
} from "@/features/provenance/provenanceCopy";
|
|
import { qtypeName } from "@/features/provenance/qtype";
|
|
import { styles as shared } from "@/ui/styles";
|
|
import { colors } from "@/ui/tokens.stylex";
|
|
|
|
const styles = stylex.create({
|
|
heading: {
|
|
marginTop: "0.5rem",
|
|
fontSize: "1.5rem",
|
|
lineHeight: "2rem",
|
|
fontWeight: 600,
|
|
wordBreak: "break-all",
|
|
},
|
|
subtitle: {
|
|
marginTop: "0.25rem",
|
|
color: colors.textSecondary,
|
|
},
|
|
/** The recorded facts, fenced off from the live links below them. */
|
|
record: {
|
|
marginTop: "1rem",
|
|
maxWidth: "48rem",
|
|
borderRadius: "0.25rem",
|
|
borderWidth: 1,
|
|
borderStyle: "solid",
|
|
borderColor: colors.border,
|
|
backgroundColor: colors.surfaceRaised,
|
|
padding: "1rem",
|
|
},
|
|
recordNote: {
|
|
fontSize: "0.8125rem",
|
|
lineHeight: "1.25rem",
|
|
color: colors.textMuted,
|
|
},
|
|
section: {
|
|
marginTop: "1rem",
|
|
borderTopWidth: { default: 1, ":first-of-type": 0 },
|
|
borderTopStyle: "solid",
|
|
borderTopColor: colors.border,
|
|
paddingTop: { default: "1rem", ":first-of-type": 0 },
|
|
},
|
|
sectionHeading: {
|
|
fontSize: "0.75rem",
|
|
lineHeight: "1rem",
|
|
fontWeight: 600,
|
|
letterSpacing: "0.05em",
|
|
textTransform: "uppercase",
|
|
color: colors.textMuted,
|
|
},
|
|
facts: {
|
|
marginTop: "0.5rem",
|
|
marginBottom: 0,
|
|
display: "grid",
|
|
gap: "0.375rem 1rem",
|
|
gridTemplateColumns: {
|
|
default: "auto",
|
|
"@media (min-width: 640px)": "max-content 1fr",
|
|
},
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
term: {
|
|
color: colors.textMuted,
|
|
},
|
|
value: {
|
|
margin: 0,
|
|
wordBreak: "break-all",
|
|
},
|
|
muted: {
|
|
color: colors.textMuted,
|
|
},
|
|
related: {
|
|
marginTop: "1.5rem",
|
|
maxWidth: "48rem",
|
|
},
|
|
relatedHeading: {
|
|
fontSize: "1.125rem",
|
|
lineHeight: "1.75rem",
|
|
fontWeight: 600,
|
|
},
|
|
relatedNote: {
|
|
marginTop: "0.25rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
color: colors.textMuted,
|
|
},
|
|
relatedList: {
|
|
marginTop: "0.5rem",
|
|
display: "flex",
|
|
flexWrap: "wrap",
|
|
gap: "0.75rem",
|
|
fontSize: "0.875rem",
|
|
lineHeight: "1.25rem",
|
|
},
|
|
link: {
|
|
color: colors.primaryOnSurface,
|
|
},
|
|
});
|
|
|
|
/** The look of one related action, so every caller's links match. */
|
|
export const provenanceRelatedLink = styles.link;
|
|
|
|
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
return (
|
|
<div {...stylex.props(styles.section)}>
|
|
<h2 {...stylex.props(styles.sectionHeading)}>{title}</h2>
|
|
<dl {...stylex.props(styles.facts)}>{children}</dl>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Fact({ label, children }: { label: string; children: ReactNode }) {
|
|
return (
|
|
<>
|
|
<dt {...stylex.props(styles.term)}>{label}</dt>
|
|
<dd {...stylex.props(styles.value)}>{children}</dd>
|
|
</>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* A recorded name, in the face the rest of the interface gives to names. It
|
|
* wraps the value rather than the row, so the prose that stands in for a
|
|
* missing one is not set in the same typewriter face.
|
|
*/
|
|
function Mono({ children }: { children: string }) {
|
|
return <span {...stylex.props(shared.mono)}>{children}</span>;
|
|
}
|
|
|
|
/** An empty text field means the server recorded nothing there, never an empty value. */
|
|
function Absent({ children }: { children: string }) {
|
|
return <span {...stylex.props(styles.muted)}>{children}</span>;
|
|
}
|
|
|
|
/**
|
|
* What an empty `matched` means. `no_match` is the only reason the matcher
|
|
* itself records with nothing to show; every other empty one names a pipeline
|
|
* step that answered before filtering — a local record, a forward zone, a pause
|
|
* (see `PolicyReason` in src/storage/provenance.zig) — where "nothing matched"
|
|
* would claim an evaluation that never happened.
|
|
*/
|
|
function unmatchedLabel(reason: PolicyReason): string {
|
|
return reason === "no_match" ? "Nothing matched" : "The matcher never ran";
|
|
}
|
|
|
|
interface Props {
|
|
provenance: Provenance;
|
|
/**
|
|
* The log row this explains, or null for a frame read straight off the
|
|
* stream. Null is the same fact `QuerySummary.id` carries: the event
|
|
* precedes its own insert, so there is no row to name and none is invented.
|
|
*/
|
|
persistedId: number | null;
|
|
/** The related-action links, built by whichever surface owns the routes. */
|
|
relatedActions: ReactNode;
|
|
}
|
|
|
|
export default function ProvenanceDetail({ provenance, persistedId, relatedActions }: Props) {
|
|
const { request, group, policy, rewrites, route, response } = provenance;
|
|
const clientNames = useClientNames();
|
|
const currentClient = clientLabel(request.client, clientNames);
|
|
|
|
return (
|
|
<>
|
|
<h1 {...stylex.props(styles.heading, shared.mono)}>{request.domain}</h1>
|
|
<p {...stylex.props(styles.subtitle)}>
|
|
{formatTime(request.time)} — {policyActionLabel(policy.action)}
|
|
{/* The verdict alone reads as a success; a non-NOERROR answer says otherwise. */}
|
|
{response.rcode !== 0 && ` — ${rcodeName(response.rcode)}`}
|
|
</p>
|
|
|
|
<div {...stylex.props(styles.record)}>
|
|
<p {...stylex.props(styles.recordNote)}>
|
|
What was recorded when this query was answered. Group and blocklist names are the ones in force at
|
|
that moment; they may have been renamed or deleted since.
|
|
{persistedId === null &&
|
|
" This is the event as it was streamed; the query log may not have written it yet."}
|
|
</p>
|
|
|
|
<Section title="Request">
|
|
<Fact label="Time">{formatTime(request.time)}</Fact>
|
|
<Fact label="Domain">
|
|
<Mono>{request.domain}</Mono>
|
|
</Fact>
|
|
<Fact label="Client">
|
|
<Mono>{request.client}</Mono>
|
|
</Fact>
|
|
<Fact label="Type">{qtypeName(request.qtype)}</Fact>
|
|
<Fact label="Class">{qclassName(request.qclass)}</Fact>
|
|
</Section>
|
|
|
|
<Section title="Group">
|
|
<Fact label="Name">{group.name === "" ? <Absent>No group recorded</Absent> : group.name}</Fact>
|
|
<Fact label="Id">{group.id === null ? <Absent>—</Absent> : group.id}</Fact>
|
|
</Section>
|
|
|
|
<Section title="Policy">
|
|
<Fact label="Decision">{policyActionLabel(policy.action)}</Fact>
|
|
<Fact label="Reason">{policyReasonLabel(policy.reason)}</Fact>
|
|
<Fact label="Matched">
|
|
{policy.matched === "" ? (
|
|
<Absent>{unmatchedLabel(policy.reason)}</Absent>
|
|
) : (
|
|
<Mono>{policy.matched}</Mono>
|
|
)}
|
|
</Fact>
|
|
<Fact label="Blocklist">
|
|
{policy.source_name === "" ? (
|
|
<Absent>Not a blocklist decision</Absent>
|
|
) : policy.source_id === null ? (
|
|
policy.source_name
|
|
) : (
|
|
`${policy.source_name} (#${policy.source_id})`
|
|
)}
|
|
</Fact>
|
|
</Section>
|
|
|
|
<Section title="Rewrites">
|
|
<Fact label="CNAME target">
|
|
{rewrites.cname_target === "" ? (
|
|
<Absent>The queried name was decided directly</Absent>
|
|
) : (
|
|
<Mono>{rewrites.cname_target}</Mono>
|
|
)}
|
|
</Fact>
|
|
<Fact label="Safe search">
|
|
{rewrites.safe_search_target === "" ? (
|
|
<Absent>No rewrite</Absent>
|
|
) : (
|
|
<Mono>{rewrites.safe_search_target}</Mono>
|
|
)}
|
|
</Fact>
|
|
</Section>
|
|
|
|
<Section title="Route">
|
|
<Fact label="Answered by">{routeKindLabel(route.kind)}</Fact>
|
|
<Fact label="Forward zone">
|
|
{route.forward_zone === "" ? <Absent>—</Absent> : <Mono>{route.forward_zone}</Mono>}
|
|
</Fact>
|
|
<Fact label="Upstream">
|
|
{route.upstream === "" ? <Absent>No upstream exchange</Absent> : <Mono>{route.upstream}</Mono>}
|
|
</Fact>
|
|
</Section>
|
|
|
|
<Section title="Response">
|
|
<Fact label="Result">{rcodeName(response.rcode)}</Fact>
|
|
<Fact label="Took">
|
|
{response.duration_us === null ? (
|
|
<Absent>Not measured</Absent>
|
|
) : (
|
|
formatMicros(response.duration_us)
|
|
)}
|
|
</Fact>
|
|
</Section>
|
|
</div>
|
|
|
|
{/* A named region, because the Pause it may offer is not the only Pause
|
|
on screen: the sidebar carries one too, and the two answer different
|
|
questions. */}
|
|
<section aria-labelledby="related-actions" {...stylex.props(styles.related)}>
|
|
<h2 id="related-actions" {...stylex.props(styles.relatedHeading)}>
|
|
Related
|
|
</h2>
|
|
<p {...stylex.props(styles.relatedNote)}>
|
|
These read the current configuration, which may no longer be the one that decided this query.
|
|
</p>
|
|
{currentClient !== null && (
|
|
<p {...stylex.props(styles.relatedNote)}>
|
|
{currentClient.learned ? (
|
|
<>
|
|
Reverse DNS currently resolves <Mono>{request.client}</Mono> to{" "}
|
|
<Mono>{currentClient.text}</Mono>.
|
|
</>
|
|
) : (
|
|
<>
|
|
The client list currently names <Mono>{request.client}</Mono> “{currentClient.text}”.
|
|
</>
|
|
)}
|
|
</p>
|
|
)}
|
|
<div {...stylex.props(styles.relatedList)}>{relatedActions}</div>
|
|
</section>
|
|
</>
|
|
);
|
|
}
|