milestone 23 s3: convert features/queries/QueryLogPage.tsx to stylex
This commit is contained in:
@@ -1,14 +1,146 @@
|
|||||||
import { useState, type FormEvent } from "react";
|
import { useState, type FormEvent } from "react";
|
||||||
import { useInfiniteQuery } from "@tanstack/react-query";
|
import { useInfiniteQuery } from "@tanstack/react-query";
|
||||||
|
import * as stylex from "@stylexjs/stylex";
|
||||||
import * as api from "@/lib/api";
|
import * as api from "@/lib/api";
|
||||||
import { formatMicros, formatTime } from "@/lib/format";
|
import { formatMicros, formatTime } from "@/lib/format";
|
||||||
import { queriesInfiniteQuery } from "@/lib/queries";
|
import { queriesInfiniteQuery } from "@/lib/queries";
|
||||||
import type { QueriesFilter, QueryRow } from "@/lib/types";
|
import type { QueriesFilter, QueryRow } from "@/lib/types";
|
||||||
import { qtypeName } from "./qtype";
|
import { qtypeName } from "./qtype";
|
||||||
import { buttonClass, smallInputClass, tableWrapClass } from "@/ui/classes";
|
import Select from "@/ui/Select";
|
||||||
|
import { styles as shared } from "@/ui/styles";
|
||||||
|
import { colors } from "@/ui/tokens.stylex";
|
||||||
|
|
||||||
const filterInputClass = `mt-1 w-full ${smallInputClass}`;
|
const DARK = "@media (prefers-color-scheme: dark)";
|
||||||
const toolbarButtonClass = `${buttonClass} font-medium`;
|
|
||||||
|
const STATUS_OPTIONS = [
|
||||||
|
{ value: "any", label: "All" },
|
||||||
|
{ value: "blocked", label: "Blocked only" },
|
||||||
|
{ value: "allowed", label: "Allowed only" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const styles = stylex.create({
|
||||||
|
heading: {
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
lineHeight: "2rem",
|
||||||
|
fontWeight: 600,
|
||||||
|
},
|
||||||
|
/** One column on a phone, two from `sm`, five from `lg`, as before. */
|
||||||
|
filterGrid: {
|
||||||
|
marginTop: "1rem",
|
||||||
|
display: "grid",
|
||||||
|
gap: "0.75rem",
|
||||||
|
gridTemplateColumns: {
|
||||||
|
default: "repeat(1, minmax(0, 1fr))",
|
||||||
|
"@media (min-width: 640px)": "repeat(2, minmax(0, 1fr))",
|
||||||
|
"@media (min-width: 1024px)": "repeat(5, minmax(0, 1fr))",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
filterLabel: {
|
||||||
|
display: "block",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: "1.25rem",
|
||||||
|
},
|
||||||
|
filterInput: {
|
||||||
|
marginTop: "0.25rem",
|
||||||
|
width: "100%",
|
||||||
|
},
|
||||||
|
buttonRow: {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "flex-end",
|
||||||
|
gap: "0.5rem",
|
||||||
|
gridColumn: {
|
||||||
|
default: null,
|
||||||
|
"@media (min-width: 640px)": "span 2 / span 2",
|
||||||
|
"@media (min-width: 1024px)": "span 5 / span 5",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
toolbarButton: {
|
||||||
|
fontWeight: 500,
|
||||||
|
},
|
||||||
|
note: {
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: "1.25rem",
|
||||||
|
color: colors.textMuted,
|
||||||
|
},
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
/** The header tint is a shade off the ground in each scheme, not a token role. */
|
||||||
|
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.textMuted,
|
||||||
|
},
|
||||||
|
/** `divide-y`: a hairline between rows, so the first row carries none. */
|
||||||
|
row: {
|
||||||
|
borderTopWidth: { default: 1, ":first-child": 0 },
|
||||||
|
borderTopStyle: "solid",
|
||||||
|
borderTopColor: colors.border,
|
||||||
|
},
|
||||||
|
cell: {
|
||||||
|
paddingInline: "0.75rem",
|
||||||
|
paddingBlock: "0.5rem",
|
||||||
|
},
|
||||||
|
nowrap: {
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
},
|
||||||
|
breakAll: {
|
||||||
|
wordBreak: "break-all",
|
||||||
|
},
|
||||||
|
small: {
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
lineHeight: "1rem",
|
||||||
|
},
|
||||||
|
muted: {
|
||||||
|
color: colors.textMuted,
|
||||||
|
},
|
||||||
|
blockedWrap: {
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.375rem",
|
||||||
|
},
|
||||||
|
blockedBadge: {
|
||||||
|
borderRadius: "0.25rem",
|
||||||
|
paddingInline: "0.375rem",
|
||||||
|
paddingBlock: "0.125rem",
|
||||||
|
fontSize: "0.75rem",
|
||||||
|
lineHeight: "1rem",
|
||||||
|
fontWeight: 500,
|
||||||
|
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)" },
|
||||||
|
},
|
||||||
|
footer: {
|
||||||
|
marginTop: "0.75rem",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "0.75rem",
|
||||||
|
},
|
||||||
|
moreError: {
|
||||||
|
marginTop: "0.5rem",
|
||||||
|
fontSize: "0.875rem",
|
||||||
|
lineHeight: "1.25rem",
|
||||||
|
color: colors.dangerText,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
function errorMessage(error: unknown): string {
|
function errorMessage(error: unknown): string {
|
||||||
return error instanceof Error ? error.message : String(error);
|
return error instanceof Error ? error.message : String(error);
|
||||||
@@ -21,13 +153,11 @@ function datetimeLocalToUnix(value: string): number | undefined {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function BlockedCell({ row }: { row: Pick<QueryRow, "blocked" | "block_reason"> }) {
|
export function BlockedCell({ row }: { row: Pick<QueryRow, "blocked" | "block_reason"> }) {
|
||||||
if (!row.blocked) return <span className="text-zinc-400">—</span>;
|
if (!row.blocked) return <span {...stylex.props(styles.muted)}>—</span>;
|
||||||
return (
|
return (
|
||||||
<span className="inline-flex items-center gap-1.5">
|
<span {...stylex.props(styles.blockedWrap)}>
|
||||||
<span className="rounded bg-red-100 px-1.5 py-0.5 text-xs font-medium text-red-800 dark:bg-red-900 dark:text-red-200">
|
<span {...stylex.props(styles.blockedBadge)}>Blocked</span>
|
||||||
Blocked
|
{row.block_reason !== "" && <span {...stylex.props(styles.small, styles.muted)}>{row.block_reason}</span>}
|
||||||
</span>
|
|
||||||
{row.block_reason !== "" && <span className="text-xs text-zinc-500">{row.block_reason}</span>}
|
|
||||||
</span>
|
</span>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -35,37 +165,38 @@ export function BlockedCell({ row }: { row: Pick<QueryRow, "blocked" | "block_re
|
|||||||
export function QueryCells({ row }: { row: Omit<QueryRow, "id"> }) {
|
export function QueryCells({ row }: { row: Omit<QueryRow, "id"> }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<td className="px-3 py-2 whitespace-nowrap text-zinc-500">{formatTime(row.ts)}</td>
|
<td {...stylex.props(styles.cell, styles.nowrap, styles.muted)}>{formatTime(row.ts)}</td>
|
||||||
<td className="px-3 py-2 font-mono text-xs break-all">{row.domain}</td>
|
<td {...stylex.props(styles.cell, styles.small, styles.breakAll, shared.mono)}>{row.domain}</td>
|
||||||
<td className="px-3 py-2 font-mono text-xs whitespace-nowrap">{row.client_ip}</td>
|
<td {...stylex.props(styles.cell, styles.small, styles.nowrap, shared.mono)}>{row.client_ip}</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap">{qtypeName(row.qtype)}</td>
|
<td {...stylex.props(styles.cell, styles.nowrap)}>{qtypeName(row.qtype)}</td>
|
||||||
<td className="px-3 py-2">
|
<td {...stylex.props(styles.cell)}>
|
||||||
<BlockedCell row={row} />
|
<BlockedCell row={row} />
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap tabular-nums">
|
<td {...stylex.props(styles.cell, styles.nowrap, shared.tabularNums)}>
|
||||||
{row.response_time_us === null ? "—" : formatMicros(row.response_time_us)}
|
{row.response_time_us === null ? "—" : formatMicros(row.response_time_us)}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 whitespace-nowrap">
|
<td {...stylex.props(styles.cell, styles.nowrap)}>
|
||||||
{row.cache_hit === null ? "—" : row.cache_hit ? "hit" : "miss"}
|
{row.cache_hit === null ? "—" : row.cache_hit ? "hit" : "miss"}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-3 py-2 font-mono text-xs break-all">{row.upstream === "" ? "—" : row.upstream}</td>
|
<td {...stylex.props(styles.cell, styles.small, styles.breakAll, shared.mono)}>
|
||||||
|
{row.upstream === "" ? "—" : row.upstream}
|
||||||
|
</td>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QueryTableHead() {
|
export function QueryTableHead() {
|
||||||
const th = "px-3 py-2 font-medium text-zinc-600 dark:text-zinc-400";
|
|
||||||
return (
|
return (
|
||||||
<thead className="bg-zinc-50 text-left dark:bg-zinc-900">
|
<thead {...stylex.props(styles.head)}>
|
||||||
<tr>
|
<tr>
|
||||||
<th className={th}>Time</th>
|
<th {...stylex.props(styles.th)}>Time</th>
|
||||||
<th className={th}>Domain</th>
|
<th {...stylex.props(styles.th)}>Domain</th>
|
||||||
<th className={th}>Client</th>
|
<th {...stylex.props(styles.th)}>Client</th>
|
||||||
<th className={th}>Type</th>
|
<th {...stylex.props(styles.th)}>Type</th>
|
||||||
<th className={th}>Status</th>
|
<th {...stylex.props(styles.th)}>Status</th>
|
||||||
<th className={th}>Response</th>
|
<th {...stylex.props(styles.th)}>Response</th>
|
||||||
<th className={th}>Cache</th>
|
<th {...stylex.props(styles.th)}>Cache</th>
|
||||||
<th className={th}>Upstream</th>
|
<th {...stylex.props(styles.th)}>Upstream</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
);
|
);
|
||||||
@@ -124,66 +255,65 @@ export default function QueryLogPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section>
|
<section>
|
||||||
<h1 className="text-2xl font-semibold">Query Log</h1>
|
<h1 {...stylex.props(styles.heading)}>Query Log</h1>
|
||||||
|
|
||||||
<form onSubmit={applyFilters} className="mt-4 grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-5">
|
<form onSubmit={applyFilters} {...stylex.props(styles.filterGrid)}>
|
||||||
<label className="block text-sm">
|
<label {...stylex.props(styles.filterLabel)}>
|
||||||
Domain contains
|
Domain contains
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={domain}
|
value={domain}
|
||||||
onChange={(event) => setDomain(event.target.value)}
|
onChange={(event) => setDomain(event.target.value)}
|
||||||
className={filterInputClass}
|
{...stylex.props(shared.smallInput, styles.filterInput, shared.focusRing)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block text-sm">
|
<label {...stylex.props(styles.filterLabel)}>
|
||||||
Client (exact)
|
Client (exact)
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={client}
|
value={client}
|
||||||
onChange={(event) => setClient(event.target.value)}
|
onChange={(event) => setClient(event.target.value)}
|
||||||
className={filterInputClass}
|
{...stylex.props(shared.smallInput, styles.filterInput, shared.focusRing)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block text-sm">
|
<Select
|
||||||
Status
|
variant="compactField"
|
||||||
<select
|
label="Status"
|
||||||
value={blocked}
|
value={blocked}
|
||||||
onChange={(event) => setBlocked(event.target.value)}
|
onChange={setBlocked}
|
||||||
className={filterInputClass}
|
options={STATUS_OPTIONS}
|
||||||
>
|
/>
|
||||||
<option value="any">All</option>
|
<label {...stylex.props(styles.filterLabel)}>
|
||||||
<option value="blocked">Blocked only</option>
|
|
||||||
<option value="allowed">Allowed only</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label className="block text-sm">
|
|
||||||
Since
|
Since
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={since}
|
value={since}
|
||||||
onChange={(event) => setSince(event.target.value)}
|
onChange={(event) => setSince(event.target.value)}
|
||||||
className={filterInputClass}
|
{...stylex.props(shared.smallInput, styles.filterInput, shared.focusRing)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className="block text-sm">
|
<label {...stylex.props(styles.filterLabel)}>
|
||||||
Until
|
Until
|
||||||
<input
|
<input
|
||||||
type="datetime-local"
|
type="datetime-local"
|
||||||
value={until}
|
value={until}
|
||||||
onChange={(event) => setUntil(event.target.value)}
|
onChange={(event) => setUntil(event.target.value)}
|
||||||
className={filterInputClass}
|
{...stylex.props(shared.smallInput, styles.filterInput, shared.focusRing)}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="flex items-end gap-2 sm:col-span-2 lg:col-span-5">
|
<div {...stylex.props(styles.buttonRow)}>
|
||||||
<button type="submit" className={toolbarButtonClass}>
|
<button type="submit" {...stylex.props(shared.button, styles.toolbarButton, shared.focusRing)}>
|
||||||
Apply filters
|
Apply filters
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={clearFilters} className={toolbarButtonClass}>
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={clearFilters}
|
||||||
|
{...stylex.props(shared.button, styles.toolbarButton, shared.focusRing)}
|
||||||
|
>
|
||||||
Clear
|
Clear
|
||||||
</button>
|
</button>
|
||||||
{base.isFetching && (
|
{base.isFetching && (
|
||||||
<span className="text-sm text-zinc-500" role="status">
|
<span {...stylex.props(styles.note)} role="status">
|
||||||
Loading…
|
Loading…
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -191,29 +321,29 @@ export default function QueryLogPage() {
|
|||||||
</form>
|
</form>
|
||||||
|
|
||||||
{base.data === undefined ? (
|
{base.data === undefined ? (
|
||||||
<p className="mt-6 animate-pulse text-zinc-500" role="status">
|
<p {...stylex.props(styles.empty, shared.pulse)} role="status">
|
||||||
Loading query log…
|
Loading query log…
|
||||||
</p>
|
</p>
|
||||||
) : rows.length === 0 ? (
|
) : rows.length === 0 ? (
|
||||||
<p className="mt-6 text-zinc-500">
|
<p {...stylex.props(styles.empty)}>
|
||||||
{filterActive ? "No queries match the current filters." : "No queries logged yet."}
|
{filterActive ? "No queries match the current filters." : "No queries logged 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>
|
||||||
{rows.map((row) => (
|
{rows.map((row) => (
|
||||||
<tr key={row.id}>
|
<tr key={row.id} {...stylex.props(styles.row)}>
|
||||||
<QueryCells row={row} />
|
<QueryCells row={row} />
|
||||||
</tr>
|
</tr>
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 flex items-center gap-3">
|
<div {...stylex.props(styles.footer)}>
|
||||||
<p className="text-sm text-zinc-500">
|
<p {...stylex.props(styles.note)}>
|
||||||
Showing {rows.length} {rows.length === 1 ? "query" : "queries"}
|
Showing {rows.length} {rows.length === 1 ? "query" : "queries"}
|
||||||
{hasMore ? "" : " — end of log"}
|
{hasMore ? "" : " — end of log"}
|
||||||
</p>
|
</p>
|
||||||
@@ -222,14 +352,14 @@ export default function QueryLogPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={loadMore}
|
onClick={loadMore}
|
||||||
disabled={base.isFetchingNextPage || base.isPlaceholderData}
|
disabled={base.isFetchingNextPage || base.isPlaceholderData}
|
||||||
className={toolbarButtonClass}
|
{...stylex.props(shared.button, styles.toolbarButton, shared.focusRing)}
|
||||||
>
|
>
|
||||||
{base.isFetchingNextPage ? "Loading…" : "Load more"}
|
{base.isFetchingNextPage ? "Loading…" : "Load more"}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{moreError !== null && (
|
{moreError !== null && (
|
||||||
<p role="alert" className="mt-2 text-sm text-red-700 dark:text-red-300">
|
<p role="alert" {...stylex.props(styles.moreError)}>
|
||||||
Failed to load more: {moreError}
|
Failed to load more: {moreError}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user