Files
atc-agents/ui/src/components/features/SqlWorkbench.tsx
T
mo 170eb2418b Add agent workbench terminal and live PostgreSQL/Trino SQL consoles.
Clicking agents opens a dedicated terminal panel; topology PostgreSQL/Trino nodes open SQL workbench with ten demo queries and Postgres vs Trino benchmark.
2026-06-25 01:10:57 +00:00

206 lines
7.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import { Database, Loader2, Play, Zap } from 'lucide-react'
import { cn } from '../../lib/utils'
import { subTabActive, subTabIdle } from '../../lib/tabActive'
type Sample = { id: string; label: string; sql: string }
type SqlResult = {
ok: boolean
columns?: string[]
rows?: unknown[][]
row_count?: number
elapsed_ms?: number
error?: string
sql?: string
}
type Props = {
engine: 'postgres' | 'trino'
}
const ENGINE_META = {
postgres: {
title: 'PostgreSQL SQL Console',
sub: 'DB Vault · 10.0.21.51:5432 · user mo',
accent: 'text-blue-400',
},
trino: {
title: 'Trino SQL Console',
sub: 'Lakehouse · 10.0.21.50:8089 · federated lakehouse queries',
accent: 'text-violet-400',
},
}
export function SqlWorkbench({ engine }: Props) {
const meta = ENGINE_META[engine]
const [samples, setSamples] = useState<Sample[]>([])
const [sql, setSql] = useState('SELECT version();')
const [result, setResult] = useState<SqlResult | null>(null)
const [benchmark, setBenchmark] = useState<{
comparison?: { postgres_ms?: number; trino_ms?: number; faster?: string; speedup_factor?: number }
postgres?: SqlResult
trino?: SqlResult
} | null>(null)
const [loading, setLoading] = useState(false)
const [benchLoading, setBenchLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const loadSamples = useCallback(async () => {
const r = await fetch(`/api/sql/samples/${engine}`)
if (r.ok) {
const j = await r.json()
setSamples(j.samples || [])
if (j.samples?.[0]) setSql(j.samples[0].sql)
}
}, [engine])
useEffect(() => {
loadSamples()
setResult(null)
setBenchmark(null)
setError(null)
}, [engine, loadSamples])
const run = async (query?: string) => {
const q = (query ?? sql).trim()
if (!q) return
setLoading(true)
setError(null)
try {
const r = await fetch('/api/sql/execute', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ engine, sql: q }),
})
const j = await r.json()
if (!r.ok || !j.ok) {
setError(j.error || 'Query failed')
setResult(null)
return
}
setSql(q)
setResult(j)
} catch {
setError('SQL API unavailable')
} finally {
setLoading(false)
}
}
const runBenchmark = async () => {
setBenchLoading(true)
setError(null)
try {
const r = await fetch('/api/sql/benchmark', { method: 'POST' })
const j = await r.json()
if (!r.ok) {
setError(j.error || 'Benchmark failed')
return
}
setBenchmark(j)
} catch {
setError('Benchmark failed')
} finally {
setBenchLoading(false)
}
}
return (
<div className="flex h-full min-h-0 flex-col bg-[#0a0e14] text-foreground">
<header className="flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-border/60 px-3 py-2">
<div>
<h3 className={cn('flex items-center gap-2 text-sm font-semibold', meta.accent)}>
<Database className="h-4 w-4" />
{meta.title}
</h3>
<p className="text-[10px] text-foreground-muted">{meta.sub}</p>
</div>
<div className="flex gap-2">
<button type="button" onClick={() => run()} disabled={loading} className={cn('inline-flex items-center gap-1 rounded px-3 py-1.5 text-[11px]', subTabActive)}>
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
Run
</button>
<button type="button" onClick={runBenchmark} disabled={benchLoading} className={cn('inline-flex items-center gap-1 rounded px-3 py-1.5 text-[11px]', subTabIdle)}>
{benchLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Zap className="h-3 w-3 text-amber-400" />}
Postgres vs Trino
</button>
</div>
</header>
<div className="flex min-h-0 flex-1 flex-col lg:flex-row">
<aside className="shrink-0 border-b border-border/60 p-2 lg:w-56 lg:border-b-0 lg:border-r">
<p className="mb-1 text-[9px] font-semibold uppercase tracking-wider text-foreground-faint">10 demo commands</p>
<div className="scrollbar-thin max-h-32 space-y-0.5 overflow-y-auto lg:max-h-none">
{samples.map((s) => (
<button
key={s.id}
type="button"
onClick={() => { setSql(s.sql); run(s.sql) }}
className="block w-full rounded border border-transparent px-2 py-1 text-left text-[10px] hover:border-docker/30 hover:bg-docker/5"
>
<span className="font-medium text-foreground">{s.label}</span>
<span className="mt-0.5 block truncate font-mono text-[8px] text-foreground-faint">{s.sql}</span>
</button>
))}
</div>
</aside>
<div className="flex min-h-0 min-w-0 flex-1 flex-col">
<textarea
value={sql}
onChange={(e) => setSql(e.target.value)}
className="min-h-[72px] shrink-0 resize-none border-b border-border/60 bg-black/40 p-2 font-mono text-[11px] text-emerald-100 outline-none focus:ring-1 focus:ring-docker/40"
spellCheck={false}
/>
{error && <p className="shrink-0 px-3 py-1 text-[11px] text-danger">{error}</p>}
{benchmark?.comparison && (
<div className="shrink-0 border-b border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px]">
<span className="font-semibold text-amber-300">Benchmark (2M rows): </span>
PostgreSQL <span className="font-mono">{benchmark.comparison.postgres_ms}ms</span>
{' · '}
Trino <span className="font-mono">{benchmark.comparison.trino_ms}ms</span>
{' — '}
<span className="font-semibold text-success">
{benchmark.comparison.faster} {benchmark.comparison.speedup_factor ? `${benchmark.comparison.speedup_factor}× faster` : ''}
</span>
</div>
)}
<div className="scrollbar-thin min-h-0 flex-1 overflow-auto p-2">
{result?.ok && result.columns && (
<>
<p className="mb-1 font-mono text-[9px] text-foreground-faint">
{result.row_count} rows · {result.elapsed_ms}ms
</p>
<table className="w-full text-left font-mono text-[10px]">
<thead>
<tr className="border-b border-border text-docker">
{result.columns.map((c) => <th key={c} className="px-2 py-1">{c}</th>)}
</tr>
</thead>
<tbody>
{result.rows?.map((row, i) => (
<tr key={i} className="border-b border-border/30 hover:bg-white/5">
{row.map((cell, j) => (
<td key={j} className="max-w-[200px] truncate px-2 py-1 text-foreground-muted">
{cell === null ? 'NULL' : String(cell)}
</td>
))}
</tr>
))}
</tbody>
</table>
</>
)}
{!result && !loading && (
<p className="py-6 text-center text-[11px] text-foreground-faint">Pick a command or write SQL · click Run</p>
)}
</div>
</div>
</div>
</div>
)
}