milestone 23 s3: convert features/live/LiveLogPage.tsx to stylex
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user