import { useState, type FormEvent } from "react"; import { useInfiniteQuery } from "@tanstack/react-query"; 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"; const inputClass = "mt-1 w-full rounded border border-zinc-300 bg-white px-2 py-1.5 text-sm dark:border-zinc-700 dark:bg-zinc-900"; const buttonClass = "rounded border border-zinc-300 px-3 py-1.5 text-sm font-medium focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-blue-600 dark:border-zinc-700"; 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() { const th = "px-3 py-2 font-medium text-zinc-600 dark:text-zinc-400"; 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

{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}

)} )}
); }