milestone 23 s3: convert features/live/LiveLogPage.tsx to stylex

This commit is contained in:
2026-08-12 22:36:09 +02:00
parent 4379d7599b
commit 02bf1ca310
2 changed files with 182 additions and 34 deletions
+7 -1
View File
@@ -50,8 +50,14 @@ test("streams rows, flags blocked ones, and freezes the display", () => {
expect(screen.getByText("Blocked")).toBeTruthy(); expect(screen.getByText("Blocked")).toBeTruthy();
expect(screen.getByText("blocklist:stevenblack")).toBeTruthy(); expect(screen.getByText("blocklist:stevenblack")).toBeTruthy();
expect(screen.getByText("AAAA")).toBeTruthy(); expect(screen.getByText("AAAA")).toBeTruthy();
// StyleX compiles to opaque class names, so the check is structural: a blocked
// row carries every class a plain row does, plus the ones the flag adds.
const blockedRow = screen.getByText("ads.example").closest("tr"); const blockedRow = screen.getByText("ads.example").closest("tr");
expect(blockedRow?.className).toContain("bg-red-50"); const plainRow = screen.getByText("ok.example").closest("tr");
const blockedClasses = new Set(blockedRow?.className.split(" "));
const plainClasses = plainRow?.className.split(" ") ?? [];
expect(plainClasses.every((name) => blockedClasses.has(name))).toBe(true);
expect(blockedClasses.size).toBeGreaterThan(plainClasses.length);
const freeze = screen.getByRole("button", { name: "Freeze" }); const freeze = screen.getByRole("button", { name: "Freeze" });
fireEvent.click(freeze); fireEvent.click(freeze);
+175 -33
View File
@@ -1,20 +1,164 @@
import * as stylex from "@stylexjs/stylex";
import { QueryCells, QueryTableHead } from "@/features/queries/QueryLogPage"; import { QueryCells, QueryTableHead } from "@/features/queries/QueryLogPage";
import { RING_CAPACITY } from "./ringBuffer"; import { RING_CAPACITY } from "./ringBuffer";
import { useLiveQueries, type EventSourceFactory, type StreamStatus } from "./useLiveQueries"; import { useLiveQueries, type EventSourceFactory, type StreamStatus } from "./useLiveQueries";
import { buttonClass, retryButtonClass, tableWrapClass } from "@/ui/classes"; import { styles as shared } from "@/ui/styles";
import { colors } from "@/ui/tokens.stylex";
const toolbarButtonClass = `${buttonClass} font-medium`; const DARK = "@media (prefers-color-scheme: dark)";
const styles = stylex.create({
toolbar: {
display: "flex",
flexWrap: "wrap",
alignItems: "center",
gap: "0.75rem",
},
heading: {
fontSize: "1.5rem",
lineHeight: "2rem",
fontWeight: 600,
},
toolbarButton: {
fontWeight: 500,
},
pill: {
borderRadius: "9999px",
paddingInline: "0.625rem",
paddingBlock: "0.125rem",
fontSize: "0.75rem",
lineHeight: "1rem",
fontWeight: 500,
},
/** Four stream states need four tints; only two of them map onto a token role. */
pillConnecting: {
backgroundColor: { default: "oklch(96.7% 0.001 286.375)", [DARK]: "oklch(27.4% 0.006 286.033)" },
color: { default: "oklch(37% 0.013 285.805)", [DARK]: "oklch(87.1% 0.006 286.286)" },
},
pillOpen: {
backgroundColor: { default: "oklch(96.2% 0.044 156.743)", [DARK]: "oklch(39.3% 0.095 152.535)" },
color: { default: "oklch(44.8% 0.119 151.328)", [DARK]: "oklch(92.5% 0.084 155.995)" },
},
pillRetrying: {
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(92.4% 0.12 95.746)" },
},
pillCapped: {
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)" },
},
note: {
marginTop: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
/** Informational, neither a warning nor a failure, so the blue ramp stands alone. */
resumed: {
marginTop: "0.75rem",
display: "flex",
alignItems: "center",
gap: "0.75rem",
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: { default: "oklch(80.9% 0.105 251.813)", [DARK]: "oklch(37.9% 0.146 265.522)" },
backgroundColor: { default: "oklch(97% 0.014 254.604)", [DARK]: "oklch(28.2% 0.091 267.935)" },
color: { default: "oklch(42.4% 0.199 265.638)", [DARK]: "oklch(88.2% 0.059 254.128)" },
paddingInline: "0.75rem",
paddingBlock: "0.5rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
dismiss: {
borderStyle: "none",
backgroundColor: "transparent",
padding: 0,
color: "inherit",
fontSize: "inherit",
fontWeight: 500,
textDecorationLine: "underline",
},
failureNote: {
marginTop: "0.75rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.dangerText,
},
cappedBox: {
marginTop: "1rem",
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.dangerBorder,
backgroundColor: colors.dangerSurface,
padding: "1rem",
},
cappedHeading: {
fontWeight: 600,
color: colors.dangerText,
},
cappedDetail: {
marginTop: "0.25rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.dangerText,
},
empty: {
marginTop: "1.5rem",
color: colors.textMuted,
},
tableWrap: {
marginTop: "1rem",
overflowX: "auto",
borderRadius: "0.25rem",
borderWidth: 1,
borderStyle: "solid",
borderColor: colors.border,
},
table: {
width: "100%",
fontSize: "0.875rem",
lineHeight: "1.25rem",
},
/** `divide-y`: a hairline between rows, so the first row carries none. */
row: {
borderTopWidth: { default: 1, ":first-child": 0 },
borderTopStyle: "solid",
borderTopColor: colors.border,
},
rowBlocked: {
backgroundColor: {
default: "oklch(97.1% 0.013 17.38)",
[DARK]: "oklch(25.8% 0.092 26.042 / 0.4)",
},
},
footnote: {
marginTop: "0.75rem",
fontSize: "0.875rem",
lineHeight: "1.25rem",
color: colors.textMuted,
},
});
const PILL_LABELS: Record<StreamStatus, string> = {
connecting: "Connecting…",
open: "Live",
retrying: "Reconnecting…",
capped: "Disconnected",
};
function pillStyle(status: StreamStatus) {
if (status === "open") return styles.pillOpen;
if (status === "retrying") return styles.pillRetrying;
if (status === "capped") return styles.pillCapped;
return styles.pillConnecting;
}
function StatusPill({ status }: { status: StreamStatus }) { function StatusPill({ status }: { status: StreamStatus }) {
const styles: Record<StreamStatus, [string, string]> = { const label = PILL_LABELS[status];
connecting: ["Connecting…", "bg-zinc-100 text-zinc-700 dark:bg-zinc-800 dark:text-zinc-300"],
open: ["Live", "bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200"],
retrying: ["Reconnecting…", "bg-amber-100 text-amber-800 dark:bg-amber-900 dark:text-amber-200"],
capped: ["Disconnected", "bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200"],
};
const [label, tone] = styles[status];
return ( return (
<span role="status" aria-label={label} className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${tone}`}> <span role="status" aria-label={label} {...stylex.props(styles.pill, pillStyle(status))}>
{label} {label}
</span> </span>
); );
@@ -27,58 +171,56 @@ export default function LiveLogPage({ createEventSource }: { createEventSource?:
return ( return (
<section> <section>
<div className="flex flex-wrap items-center gap-3"> <div {...stylex.props(styles.toolbar)}>
<h1 className="text-2xl font-semibold">Live</h1> <h1 {...stylex.props(styles.heading)}>Live</h1>
<StatusPill status={live.status} /> <StatusPill status={live.status} />
<button <button
type="button" type="button"
onClick={live.toggleFreeze} onClick={live.toggleFreeze}
aria-pressed={live.frozen} aria-pressed={live.frozen}
className={toolbarButtonClass} {...stylex.props(shared.button, styles.toolbarButton, shared.focusRing)}
> >
{live.frozen ? "Resume" : "Freeze"} {live.frozen ? "Resume" : "Freeze"}
</button> </button>
</div> </div>
{live.frozen && ( {live.frozen && (
<p className="mt-2 text-sm text-zinc-500" role="status"> <p {...stylex.props(styles.note)} role="status">
Display frozen new queries keep buffering ({live.liveCount} in buffer, newest {RING_CAPACITY}{" "} Display frozen new queries keep buffering ({live.liveCount} in buffer, newest {RING_CAPACITY}{" "}
kept). kept).
</p> </p>
)} )}
{live.missed !== null && ( {live.missed !== null && (
<div <div role="status" {...stylex.props(styles.resumed)}>
role="status"
className="mt-3 flex items-center gap-3 rounded border border-blue-300 bg-blue-50 px-3 py-2 text-sm text-blue-800 dark:border-blue-900 dark:bg-blue-950 dark:text-blue-200"
>
<span> <span>
Stream resumed {" "} Stream resumed {" "}
{live.missed === 0 ? "no queries missed" : `${live.missed} missed queries recovered`}. {live.missed === 0 ? "no queries missed" : `${live.missed} missed queries recovered`}.
</span> </span>
<button type="button" onClick={live.dismissMissed} className="font-medium underline"> <button
type="button"
onClick={live.dismissMissed}
{...stylex.props(styles.dismiss, shared.focusRing)}
>
Dismiss Dismiss
</button> </button>
</div> </div>
)} )}
{live.resyncFailed && ( {live.resyncFailed && (
<p role="alert" className="mt-3 text-sm text-red-700 dark:text-red-300"> <p role="alert" {...stylex.props(styles.failureNote)}>
Stream resumed, but re-syncing the gap failed some queries may be missing here. Stream resumed, but re-syncing the gap failed some queries may be missing here.
</p> </p>
)} )}
{live.status === "capped" && ( {live.status === "capped" && (
<div <div role="alert" {...stylex.props(styles.cappedBox)}>
role="alert" <h2 {...stylex.props(styles.cappedHeading)}>Live stream unavailable</h2>
className="mt-4 rounded border border-red-300 bg-red-50 p-4 dark:border-red-900 dark:bg-red-950" <p {...stylex.props(styles.cappedDetail)}>
>
<h2 className="font-semibold text-red-800 dark:text-red-200">Live stream unavailable</h2>
<p className="mt-1 text-sm text-red-700 dark:text-red-300">
The connection failed repeatedly possibly too many live viewers (the server caps streams per The connection failed repeatedly possibly too many live viewers (the server caps streams per
address), or the server is unreachable. address), or the server is unreachable.
</p> </p>
<button type="button" onClick={live.retry} className={retryButtonClass}> <button type="button" onClick={live.retry} {...stylex.props(shared.retryButton, shared.focusRing)}>
Retry Retry
</button> </button>
</div> </div>
@@ -86,25 +228,25 @@ export default function LiveLogPage({ createEventSource }: { createEventSource?:
{live.rows.length === 0 ? ( {live.rows.length === 0 ? (
live.status !== "capped" && ( live.status !== "capped" && (
<p className="mt-6 text-zinc-500"> <p {...stylex.props(styles.empty)}>
{live.status === "open" ? "Waiting for queries…" : "No queries received yet."} {live.status === "open" ? "Waiting for queries…" : "No queries received yet."}
</p> </p>
) )
) : ( ) : (
<> <>
<div className={`${tableWrapClass} rounded border border-zinc-200 dark:border-zinc-800`}> <div {...stylex.props(styles.tableWrap)}>
<table className="w-full text-sm"> <table {...stylex.props(styles.table)}>
<QueryTableHead /> <QueryTableHead />
<tbody className="divide-y divide-zinc-100 dark:divide-zinc-800"> <tbody>
{live.rows.map((row) => ( {live.rows.map((row) => (
<tr key={row.key} className={row.blocked ? "bg-red-50 dark:bg-red-950/40" : ""}> <tr key={row.key} {...stylex.props(styles.row, row.blocked && styles.rowBlocked)}>
<QueryCells row={row} /> <QueryCells row={row} />
</tr> </tr>
))} ))}
</tbody> </tbody>
</table> </table>
</div> </div>
<p className="mt-3 text-sm text-zinc-500"> <p {...stylex.props(styles.footnote)}>
Showing {live.rows.length} {live.rows.length === 1 ? "query" : "queries"} (newest first, last{" "} Showing {live.rows.length} {live.rows.length === 1 ? "query" : "queries"} (newest first, last{" "}
{RING_CAPACITY} kept). {RING_CAPACITY} kept).
</p> </p>