import { useState, type FormEvent } from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; import * as stylex from "@stylexjs/stylex"; import * as api from "@/lib/api"; import { formatMicros, formatTime } from "@/lib/format"; import { queriesInfiniteQuery } from "@/lib/queries"; import type { QueriesFilter, QueryRow } from "@/lib/types"; import { qtypeName } from "./qtype"; import Select from "@/ui/Select"; import { styles as shared } from "@/ui/styles"; import { colors } from "@/ui/tokens.stylex"; const DARK = "@media (prefers-color-scheme: dark)"; 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.textSecondary, }, /** `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 { return error instanceof Error ? error.message : String(error); } function datetimeLocalToUnix(value: string): number | undefined { if (value === "") return undefined; const ms = new Date(value).getTime(); return Number.isFinite(ms) ? Math.floor(ms / 1000) : undefined; } export function BlockedCell({ row }: { row: Pick }) { if (!row.blocked) return ; return ( Blocked {row.block_reason !== "" && {row.block_reason}} ); } export function QueryCells({ row }: { row: Omit }) { return ( <> {formatTime(row.ts)} {row.domain} {row.client_ip} {qtypeName(row.qtype)} {row.response_time_us === null ? "—" : formatMicros(row.response_time_us)} {row.cache_hit === null ? "—" : row.cache_hit ? "hit" : "miss"} {row.upstream === "" ? "—" : row.upstream} ); } export function QueryTableHead() { return ( Time Domain Client Type Status Response Cache Upstream ); } export default function QueryLogPage() { const [domain, setDomain] = useState(""); const [client, setClient] = useState(""); const [blocked, setBlocked] = useState("any"); const [since, setSince] = useState(""); const [until, setUntil] = useState(""); const [applied, setApplied] = useState({}); const base = useInfiniteQuery(queriesInfiniteQuery(applied)); const pages = base.data?.pages ?? []; const rows: QueryRow[] = pages.flatMap((page) => page.queries); const filterActive = Object.keys(applied).length > 0; // `base.hasNextPage` reads the query state, which is empty while placeholder // data stands in for a filter change; derive the cursor from what is on // screen so the button keeps its place instead of flashing "end of log". const lastPage = pages[pages.length - 1]; const hasMore = lastPage !== undefined && lastPage.next_before !== null; // A 401 is already redirecting via the cache-level handleUnauthorized. const isUnauthorized = base.error instanceof api.ApiError && base.error.status === 401; const moreError = base.isFetchNextPageError && !isUnauthorized ? errorMessage(base.error) : null; function applyFilters(event: FormEvent) { event.preventDefault(); const filter: QueriesFilter = {}; if (domain.trim() !== "") filter.domain = domain.trim(); if (client.trim() !== "") filter.client = client.trim(); if (blocked === "blocked") filter.blocked = true; if (blocked === "allowed") filter.blocked = false; const sinceTs = datetimeLocalToUnix(since); if (sinceTs !== undefined) filter.since = sinceTs; const untilTs = datetimeLocalToUnix(until); if (untilTs !== undefined) filter.until = untilTs; setApplied(filter); } function clearFilters() { setDomain(""); setClient(""); setBlocked("any"); setSince(""); setUntil(""); setApplied({}); } function loadMore() { if (!hasMore || base.isFetchingNextPage || base.isPlaceholderData) return; void base.fetchNextPage(); } return (

Query Log

setSince(event.target.value)} {...stylex.props(shared.smallInput, styles.filterInput, shared.focusRing)} />
{base.isFetching && ( Loading… )}
{base.data === undefined ? (

Loading query log…

) : rows.length === 0 ? (

{filterActive ? "No queries match the current filters." : "No queries logged yet."}

) : ( <>
{rows.map((row) => ( ))}

Showing {rows.length} {rows.length === 1 ? "query" : "queries"} {hasMore ? "" : " — end of log"}

{hasMore && ( )}
{moreError !== null && (

Failed to load more: {moreError}

)} )}
); }