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
172 lines
5.6 KiB
TypeScript
172 lines
5.6 KiB
TypeScript
/**
|
|
* 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<RouteKind, string> = {
|
|
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<QuerySummary, "blocked" | "rcode">): 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<QuerySummary, "blocked" | "rcode"> }) {
|
|
const label = resultLabel(row);
|
|
if (row.blocked) return <span {...stylex.props(styles.badge, styles.blockedBadge)}>{label}</span>;
|
|
if (row.rcode !== 0) return <span {...stylex.props(styles.badge, styles.faultBadge)}>{label}</span>;
|
|
return <span {...stylex.props(styles.small, styles.muted)}>{label}</span>;
|
|
}
|
|
|
|
export function ActivityCells({
|
|
row,
|
|
clientNames,
|
|
renderDomain,
|
|
}: {
|
|
row: QuerySummary;
|
|
clientNames: ClientNames;
|
|
renderDomain: DomainRenderer;
|
|
}) {
|
|
return (
|
|
<>
|
|
<td {...stylex.props(styles.cell, styles.nowrap, styles.muted)}>{formatTime(row.ts)}</td>
|
|
<td {...stylex.props(styles.cell, styles.small, styles.breakAll, shared.mono)}>
|
|
{renderDomain(row.id, row.domain)}
|
|
</td>
|
|
<td {...stylex.props(styles.cell, styles.small, styles.nowrap)}>
|
|
<ClientName ip={row.client_ip} names={clientNames} />
|
|
</td>
|
|
<td {...stylex.props(styles.cell, styles.nowrap)}>{qtypeName(row.qtype)}</td>
|
|
<td {...stylex.props(styles.cell, styles.nowrap)}>
|
|
<ResultCellContent row={row} />
|
|
</td>
|
|
<td {...stylex.props(styles.cell, styles.nowrap)}>{routeLabel(row.route_kind)}</td>
|
|
<td {...stylex.props(styles.cell, styles.nowrap, shared.tabularNums)}>
|
|
{row.response_time_us === null ? "—" : formatMicros(row.response_time_us)}
|
|
</td>
|
|
</>
|
|
);
|
|
}
|
|
|
|
export const ACTIVITY_COLUMNS = ["Time", "Domain", "Client", "Type", "Result", "Route", "Duration"] as const;
|
|
|
|
export function ActivityTableHead() {
|
|
return (
|
|
<thead {...stylex.props(styles.head)}>
|
|
<tr>
|
|
{ACTIVITY_COLUMNS.map((column) => (
|
|
<th key={column} {...stylex.props(styles.th)}>
|
|
{column}
|
|
</th>
|
|
))}
|
|
</tr>
|
|
</thead>
|
|
);
|
|
}
|