admin: tables fit a phone, their first cell carrying the hidden columns, and no positioned label widens the page

This commit is contained in:
2026-10-21 10:21:17 +02:00
parent 9a99890aea
commit f26ee523c2
29 changed files with 765 additions and 156 deletions
+58 -11
View File
@@ -20,6 +20,7 @@ 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 FactLine from "@/ui/FactLine";
import { styles as shared } from "@/ui/styles";
import { colors, metrics } from "@/ui/tokens.stylex";
@@ -28,6 +29,15 @@ const DARK = "@media (prefers-color-scheme: dark)";
/** Wide enough for an ordinary hostname, narrow enough to leave the six other columns room. */
const DOMAIN_MAX_WIDTH = "24rem";
/**
* Narrower than the seven columns need with ordinary data, so the table keeps
* two: Domain, which takes the other columns' facts as lines under the name,
* and Result. Hiding cells keeps a native table; re-laying its rows out as
* blocks would put the table semantics at the mercy of each browser engine.
* The wrapper that scrolls is the container.
*/
const COMPACT = "@container (max-width: 50rem)";
const styles = stylex.create({
head: {
backgroundColor: { default: "oklch(98.5% 0 none)", [DARK]: "oklch(21% 0.006 285.885)" },
@@ -52,6 +62,16 @@ const styles = stylex.create({
domainCell: {
maxWidth: DOMAIN_MAX_WIDTH,
},
wide: {
display: { default: null, [COMPACT]: "none" },
},
/** A name can be one unbroken label as wide as the table; `anywhere` also lowers the cell's minimum width. */
compact: {
display: { default: "none", [COMPACT]: "block" },
marginTop: "0.125rem",
color: colors.textMuted,
overflowWrap: "anywhere",
},
small: {
fontSize: "0.75rem",
lineHeight: "1rem",
@@ -93,6 +113,7 @@ const styles = stylex.create({
*/
badge: {
display: "inline-block",
whiteSpace: "nowrap",
borderRadius: metrics.radius,
paddingInline: "0.375rem",
paddingBlock: "0.125rem",
@@ -152,6 +173,29 @@ export function ResultCellContent({ row }: { row: Pick<QuerySummary, "blocked" |
return <span {...stylex.props(styles.small, styles.muted)}>{label}</span>;
}
/** The facts the hidden columns hold, in their column order, for the compact Domain cell. */
function CompactFacts({ row, clientNames }: { row: QuerySummary; clientNames: ClientNames }) {
// The label stands in for the hidden Type header: a screen reader says a bare "A" as the article.
const qtype = (
<>
<span {...stylex.props(shared.srOnly)}>Type </span>
{qtypeName(row.qtype)}
</>
);
const facts: ReactNode[] = [formatTime(row.ts), qtype, routeLabel(row.route_kind)];
if (row.response_time_us !== null) facts.push(formatMicros(row.response_time_us));
return (
<>
<span {...stylex.props(styles.compact)}>
<ClientName ip={row.client_ip} names={clientNames} />
</span>
<span {...stylex.props(styles.compact, shared.tabularNums)}>
<FactLine facts={facts} />
</span>
</>
);
}
export function ActivityCells({
row,
clientNames,
@@ -163,22 +207,23 @@ export function ActivityCells({
}) {
return (
<>
<td {...stylex.props(styles.cell, styles.nowrap, styles.muted)}>{formatTime(row.ts)}</td>
<td
title={row.domain}
{...stylex.props(styles.cell, styles.small, styles.breakAll, styles.domainCell, shared.mono)}
>
{renderDomain(row.id, <span {...stylex.props(styles.domainClamp)}>{row.domain}</span>)}
<td {...stylex.props(styles.cell, styles.nowrap, styles.muted, styles.wide)}>{formatTime(row.ts)}</td>
<td title={row.domain} {...stylex.props(styles.cell, styles.small, styles.domainCell)}>
{renderDomain(
row.id,
<span {...stylex.props(styles.domainClamp, styles.breakAll, shared.mono)}>{row.domain}</span>,
)}
<CompactFacts row={row} clientNames={clientNames} />
</td>
<td {...stylex.props(styles.cell, styles.small, styles.nowrap)}>
<td {...stylex.props(styles.cell, styles.small, styles.nowrap, styles.wide)}>
<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, styles.wide)}>{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)}>
<td {...stylex.props(styles.cell, styles.nowrap, styles.wide)}>{routeLabel(row.route_kind)}</td>
<td {...stylex.props(styles.cell, styles.nowrap, shared.tabularNums, styles.wide)}>
{row.response_time_us === null ? "—" : formatMicros(row.response_time_us)}
</td>
</>
@@ -187,12 +232,14 @@ export function ActivityCells({
export const ACTIVITY_COLUMNS = ["Time", "Domain", "Client", "Type", "Result", "Route", "Duration"] as const;
const COMPACT_COLUMNS: ReadonlySet<string> = new Set(["Domain", "Result"]);
export function ActivityTableHead() {
return (
<thead {...stylex.props(styles.head)}>
<tr>
{ACTIVITY_COLUMNS.map((column) => (
<th key={column} {...stylex.props(styles.th)}>
<th key={column} {...stylex.props(styles.th, !COMPACT_COLUMNS.has(column) && styles.wide)}>
{column}
</th>
))}