QueryHistory
Search, inspect, save, copy, and rerun recent SQL with execution status and timing.
QueryHistory keeps recent SQL close to the runner without turning history into another editor. Search covers SQL, branch, database, and command. Filters separate saved and failed work. Each row keeps its status, duration, row count, and execution target visible; opening a row reveals the exact statement and any Postgres error.
Query history
5 recorded
5 queries recorded.
Behavior
- The full SQL statement expands in place with PostgreSQL syntax highlighting. Only one row stays open at a time by default.
All,Saved, andFailedfilters update immediately. Text search narrows the active filter.- Save is a quiet toggle. Copy confirms in the same button. Rerun returns the complete entry to the consumer and holds a visible running state until its promise settles.
- Save, copy, and rerun icons have concise tooltips on hover and keyboard focus.
- Successful, failed, and cancelled rows use distinct icons and text announced to screen readers; color is supporting information.
- Loading, empty, no-match, and fetch-error states keep the component’s outer size contract intact.
- Query text and long errors scroll inside the component with
.neon-scroll-fade; the page never gains horizontal overflow.
Install
npx shadcn@latest add https://ui.neon.com/r/query-history.json
Usage
import { QueryHistory } from "@/components/neon-ui/query-history";
<QueryHistory
entries={history}
onRerun={(entry) => openRunner(entry.query)}
onSavedChange={(entry, saved) => saveQuery(entry.id, saved)}
/>;
Wiring to Neon
Postgres does not expose per-user application history as a UI-ready feed. Record each SQLRunner execution in your server endpoint, then return those rows from /api/query-history. The component never stores SQL on its own.
import { neon } from "@neondatabase/serverless";
const sql = neon(process.env.DATABASE_URL!);
export async function GET() {
const rows = await sql`
select id, query, status, executed_at, duration_ms, row_count,
command, database_name, branch_name, error_message, saved
from app_query_history
order by executed_at desc
limit 100
`;
return Response.json({ entries: rows });
}
The included useQueryHistory() hook loads that endpoint with request cancellation and exposes entries, isLoading, error, refresh, and setEntries. Save changes can use PATCH /api/query-history/:id. Rerun should pass entry.query back to SQLRunner or navigate to your runner route with the query in application state.
Entry shape
interface QueryHistoryEntry {
id: string;
query: string;
status: "success" | "error" | "cancelled";
timestamp: string;
executedAt?: string;
durationMs?: number;
rowCount?: number;
command?: string;
database?: string;
branch?: string;
error?: string;
saved?: boolean;
}
timestamp is display-ready, so the application controls relative-time wording and locale. Set executedAt to the ISO timestamp for the semantic <time> element.
Props
entriesQueryHistoryEntry[]
Recorded queries, newest first.
QueryHistoryEntry[]query?string
Controlled text search.
stringdefaultQuery?string
Initial search for uncontrolled usage.
string""onQueryChange?(query: string) => void
Fires when text search changes.
(query: string) => voidfilter?"all" | "saved" | "failed"
Controlled history filter.
"all" | "saved" | "failed"defaultFilter?"all" | "saved" | "failed"
Initial filter for uncontrolled usage.
"all" | "saved" | "failed""all"expandedId?string | null
Controlled open row. Pass null to close all rows.
string | nullonRerun?(entry: QueryHistoryEntry) => void | Promise<void>
Fires with the exact saved SQL. A returned promise drives the built-in running state.
(entry: QueryHistoryEntry) => void | Promise<void>runningId?string | null
Controlled id for a query currently rerunning.
string | nullonSavedChange?(entry: QueryHistoryEntry, saved: boolean) => void
Fires when a query is saved or removed from saved queries.
(entry: QueryHistoryEntry, saved: boolean) => voidonCopy?(entry: QueryHistoryEntry) => void
Optional callback after the built-in copy action runs.
(entry: QueryHistoryEntry) => voidisLoading?boolean
Render the skeleton state.
booleanfalseerror?Error | string | null
Render the fetch-error state.
Error | string | nullnullAccessibility
Search uses a native labelled input. Filters use pressed buttons inside a fieldset. Each query disclosure is a real button with aria-expanded and aria-controls; save exposes aria-pressed. Copy feedback and filtered result counts use a polite live region. Rerunning sets aria-busy, disables repeat presses, and labels the spinner as Running query. Every action has a focus-visible tooltip and ring. Disclosure height, position, and opacity settle together in 180ms; reduced motion changes the row instantly.