/** * The seven columns of the Activity table: Time, Domain, Client, Type, Result, * Route and Duration. * * The labels here are the compact forms a scanned table needs. The detail page * keeps `provenanceCopy`'s long forms, which spell out the same facts with room * for the rcode number and the "answered by" phrasing. * * The cells render both a stored row and a streamed event, so they know nothing * about routes: the caller renders the Domain cell's contents and decides what, * if anything, a row opens. A streamed row has no id — the frame precedes its * own insert — and only the caller knows whether it has a surface for one. */ import type { ReactNode } from "react"; import * as stylex from "@stylexjs/stylex"; import { formatMicros, formatTime } from "@/lib/format"; import type { RouteKind } from "@/lib/types"; import { ClientName, type ClientNames } from "@/features/clients/clientNames"; import { rcodeShortName } from "@/features/provenance/provenanceCopy"; import { qtypeName } from "@/features/provenance/qtype"; import type { QuerySummary } from "@/features/provenance/querySummary"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; const DARK = "@media (prefers-color-scheme: dark)"; const styles = stylex.create({ head: { backgroundColor: { default: "oklch(98.5% 0 none)", [DARK]: "oklch(21% 0.006 285.885)" }, textAlign: "left", }, th: { paddingInline: "0.75rem", paddingBlock: "0.5rem", fontWeight: 500, color: colors.textSecondary, }, cell: { paddingInline: "0.75rem", paddingBlock: "0.5rem", }, nowrap: { whiteSpace: "nowrap", }, breakAll: { wordBreak: "break-all", }, small: { fontSize: "0.75rem", lineHeight: "1rem", }, muted: { color: colors.textMuted, }, domainLink: { color: colors.primaryOnSurface, textDecorationLine: "none", }, /** * The badge shape and its weight are the signal; the tint only says which * kind of unhappy answer this was. A monochrome or colour-blind reading of * the table still separates a blocked or failed row from a plain NOERROR * one, which a hue alone would not. */ badge: { display: "inline-block", borderRadius: "0.25rem", paddingInline: "0.375rem", paddingBlock: "0.125rem", fontSize: "0.75rem", lineHeight: "1rem", fontWeight: 600, }, blockedBadge: { backgroundColor: { default: "oklch(93.6% 0.032 17.717)", [DARK]: "oklch(39.6% 0.141 25.723)" }, color: { default: "oklch(44.4% 0.177 26.899)", [DARK]: "oklch(88.5% 0.062 18.334)" }, }, faultBadge: { backgroundColor: { default: "oklch(96.2% 0.059 95.617)", [DARK]: "oklch(41.4% 0.112 45.904)" }, color: { default: "oklch(47.3% 0.137 46.201)", [DARK]: "oklch(90.1% 0.076 70.697)" }, }, }); const ROUTE_LABELS: Record = { blocked: "Blocked", local: "Local", forward_zone: "Forward zone", upstream: "Upstream", cache: "Cache", rejected: "Rejected", }; /** The compact Route label. `Record` over the union, so a new kind fails `tsc`. */ export function routeLabel(kind: RouteKind): string { return ROUTE_LABELS[kind]; } /** * The compact Result label. A block is the answer the operator asked nxdns for, * so it wins over the rcode it was delivered as — a blocked name answered with * NOERROR and a zero address is still "Blocked". Everything else reads as the * code the client saw. */ export function resultLabel(row: Pick): string { return row.blocked ? "Blocked" : rcodeShortName(row.rcode); } /** * What a row's domain is wrapped in. The caller owns the routes, so it builds * the element; the look stays here, as `activityDomainLink`, which the caller * spreads onto the control itself — a link's own colour beats one inherited * from a wrapper. `id` is null for a streamed row, which history has no surface * for and live opens from memory. */ export type DomainRenderer = (id: number | null, children: ReactNode) => ReactNode; export const activityDomainLink = styles.domainLink; export function ResultCellContent({ row }: { row: Pick }) { const label = resultLabel(row); if (row.blocked) return {label}; if (row.rcode !== 0) return {label}; return {label}; } export function ActivityCells({ row, clientNames, renderDomain, }: { row: QuerySummary; clientNames: ClientNames; renderDomain: DomainRenderer; }) { return ( <> {formatTime(row.ts)} {renderDomain(row.id, row.domain)} {qtypeName(row.qtype)} {routeLabel(row.route_kind)} {row.response_time_us === null ? "—" : formatMicros(row.response_time_us)} ); } export const ACTIVITY_COLUMNS = ["Time", "Domain", "Client", "Type", "Result", "Route", "Duration"] as const; export function ActivityTableHead() { return ( {ACTIVITY_COLUMNS.map((column) => ( {column} ))} ); }