Skip to content
Neon UI
Esc
navigateopen⌘Jpreview
On this page

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

Filter query history

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, and Failed filters 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

PropType
entriesQueryHistoryEntry[]

Recorded queries, newest first.

TypeQueryHistoryEntry[]
query?string

Controlled text search.

Typestring
defaultQuery?string

Initial search for uncontrolled usage.

Typestring
Default""
onQueryChange?(query: string) => void

Fires when text search changes.

Type(query: string) => void
filter?"all" | "saved" | "failed"

Controlled history filter.

Type"all" | "saved" | "failed"
defaultFilter?"all" | "saved" | "failed"

Initial filter for uncontrolled usage.

Type"all" | "saved" | "failed"
Default"all"
expandedId?string | null

Controlled open row. Pass null to close all rows.

Typestring | null
onRerun?(entry: QueryHistoryEntry) => void | Promise<void>

Fires with the exact saved SQL. A returned promise drives the built-in running state.

Type(entry: QueryHistoryEntry) => void | Promise<void>
runningId?string | null

Controlled id for a query currently rerunning.

Typestring | null
onSavedChange?(entry: QueryHistoryEntry, saved: boolean) => void

Fires when a query is saved or removed from saved queries.

Type(entry: QueryHistoryEntry, saved: boolean) => void
onCopy?(entry: QueryHistoryEntry) => void

Optional callback after the built-in copy action runs.

Type(entry: QueryHistoryEntry) => void
isLoading?boolean

Render the skeleton state.

Typeboolean
Defaultfalse
error?Error | string | null

Render the fetch-error state.

TypeError | string | null
Defaultnull

Accessibility

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.