2026-06-25 01:10:57 +00:00
|
|
|
|
import { useCallback, useEffect, useState } from 'react'
|
|
|
|
|
|
import { Database, Loader2, Play, Zap } from 'lucide-react'
|
2026-06-27 15:42:37 +00:00
|
|
|
|
import type { SourceEngine } from '../../lib/dataSourceCatalog'
|
2026-06-25 01:10:57 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-27 15:42:37 +00:00
|
|
|
|
type Engine = SourceEngine | 'trino'
|
2026-06-25 01:33:48 +00:00
|
|
|
|
|
2026-06-25 01:10:57 +00:00
|
|
|
|
type Props = {
|
2026-06-25 01:33:48 +00:00
|
|
|
|
engine: Engine
|
|
|
|
|
|
compact?: boolean
|
2026-06-25 01:10:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 01:33:48 +00:00
|
|
|
|
const ENGINE_META: Record<Engine, { title: string; sub: string; accent: string; queryLabel: string }> = {
|
2026-06-25 01:10:57 +00:00
|
|
|
|
postgres: {
|
|
|
|
|
|
title: 'PostgreSQL SQL Console',
|
|
|
|
|
|
sub: 'DB Vault · 10.0.21.51:5432 · user mo',
|
2026-06-27 15:42:37 +00:00
|
|
|
|
accent: 'text-sky-400',
|
2026-06-25 01:33:48 +00:00
|
|
|
|
queryLabel: 'SQL',
|
|
|
|
|
|
},
|
|
|
|
|
|
mysql: {
|
|
|
|
|
|
title: 'MySQL SQL Console',
|
|
|
|
|
|
sub: 'DB Vault · 10.0.21.51:3306 · database hr · user mo',
|
|
|
|
|
|
accent: 'text-amber-400',
|
|
|
|
|
|
queryLabel: 'SQL',
|
|
|
|
|
|
},
|
|
|
|
|
|
mongodb: {
|
|
|
|
|
|
title: 'MongoDB Query Console',
|
|
|
|
|
|
sub: 'DB Vault · 10.0.21.51:27017 · supplychain',
|
|
|
|
|
|
accent: 'text-emerald-400',
|
|
|
|
|
|
queryLabel: 'Command',
|
2026-06-25 01:10:57 +00:00
|
|
|
|
},
|
2026-06-27 15:42:37 +00:00
|
|
|
|
cassandra: {
|
|
|
|
|
|
title: 'Cassandra CQL Console',
|
|
|
|
|
|
sub: 'DB Vault · 10.0.21.51:9042 · keyspace telemetry',
|
|
|
|
|
|
accent: 'text-cyan-400',
|
|
|
|
|
|
queryLabel: 'CQL',
|
|
|
|
|
|
},
|
|
|
|
|
|
neo4j: {
|
|
|
|
|
|
title: 'Neo4j Cypher Console',
|
|
|
|
|
|
sub: 'DB Vault · 10.0.21.51:7687 · graph database',
|
|
|
|
|
|
accent: 'text-pink-400',
|
|
|
|
|
|
queryLabel: 'Cypher',
|
|
|
|
|
|
},
|
2026-06-25 01:10:57 +00:00
|
|
|
|
trino: {
|
|
|
|
|
|
title: 'Trino SQL Console',
|
2026-06-25 01:33:48 +00:00
|
|
|
|
sub: 'Lakehouse · 10.0.21.50:8089 · federated queries',
|
2026-06-25 01:10:57 +00:00
|
|
|
|
accent: 'text-violet-400',
|
2026-06-25 01:33:48 +00:00
|
|
|
|
queryLabel: 'SQL',
|
2026-06-25 01:10:57 +00:00
|
|
|
|
},
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-25 01:33:48 +00:00
|
|
|
|
export function SqlWorkbench({ engine, compact }: Props) {
|
2026-06-25 01:10:57 +00:00
|
|
|
|
const meta = ENGINE_META[engine]
|
|
|
|
|
|
const [samples, setSamples] = useState<Sample[]>([])
|
2026-06-27 15:42:37 +00:00
|
|
|
|
const [sql, setSql] = useState('')
|
2026-06-25 01:10:57 +00:00
|
|
|
|
const [result, setResult] = useState<SqlResult | null>(null)
|
|
|
|
|
|
const [benchmark, setBenchmark] = useState<{
|
|
|
|
|
|
comparison?: { postgres_ms?: number; trino_ms?: number; faster?: string; speedup_factor?: number }
|
|
|
|
|
|
} | 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 (
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<div className="flex h-full min-h-0 flex-col text-foreground">
|
|
|
|
|
|
<header className={cn('flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-border/60', compact ? 'px-2 py-1' : 'px-3 py-2')}>
|
2026-06-25 01:10:57 +00:00
|
|
|
|
<div>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<h3 className={cn('flex items-center gap-1.5 font-semibold', meta.accent, compact ? 'text-xs' : 'text-sm')}>
|
|
|
|
|
|
<Database className={compact ? 'h-3 w-3' : 'h-4 w-4'} />
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{meta.title}
|
|
|
|
|
|
</h3>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<p className="text-[9px] text-foreground-muted">{meta.sub}</p>
|
2026-06-25 01:10:57 +00:00
|
|
|
|
</div>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<div className="flex gap-1.5">
|
|
|
|
|
|
<button type="button" onClick={() => run()} disabled={loading} className={cn('inline-flex items-center gap-1 rounded px-2 py-1 text-[10px]', subTabActive)}>
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
|
|
|
|
|
|
Run
|
|
|
|
|
|
</button>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
{(engine === 'postgres' || engine === 'trino') && (
|
|
|
|
|
|
<button type="button" onClick={runBenchmark} disabled={benchLoading} className={cn('inline-flex items-center gap-1 rounded px-2 py-1 text-[10px]', subTabIdle)}>
|
|
|
|
|
|
{benchLoading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Zap className="h-3 w-3 text-amber-400" />}
|
|
|
|
|
|
PG vs Trino
|
|
|
|
|
|
</button>
|
|
|
|
|
|
)}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
</div>
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
2026-06-25 01:42:25 +00:00
|
|
|
|
<div className="flex min-h-0 flex-1 overflow-hidden">
|
|
|
|
|
|
<aside className={cn('flex min-h-0 shrink-0 flex-col border-r border-border/60 p-1.5', compact ? 'w-44' : 'w-52')}>
|
2026-06-27 15:42:37 +00:00
|
|
|
|
<p className="mb-0.5 text-[8px] font-semibold uppercase tracking-wider text-foreground-faint">Demo queries</p>
|
2026-06-25 01:42:25 +00:00
|
|
|
|
<div className="scrollbar-thin min-h-0 flex-1 space-y-0.5 overflow-y-auto">
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{samples.map((s) => (
|
|
|
|
|
|
<button
|
|
|
|
|
|
key={s.id}
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
onClick={() => { setSql(s.sql); run(s.sql) }}
|
2026-06-25 01:33:48 +00:00
|
|
|
|
className="block w-full rounded border border-transparent px-1.5 py-0.5 text-left text-[9px] hover:border-docker/30 hover:bg-docker/5"
|
2026-06-25 01:10:57 +00:00
|
|
|
|
>
|
|
|
|
|
|
<span className="font-medium text-foreground">{s.label}</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)}
|
2026-06-25 01:33:48 +00:00
|
|
|
|
className={cn(
|
2026-06-25 01:42:25 +00:00
|
|
|
|
'shrink-0 resize-y border-b border-border/60 bg-[#0d1117] p-1.5 font-mono text-[10px] text-emerald-100 outline-none focus:ring-1 focus:ring-docker/40',
|
2026-06-27 15:42:37 +00:00
|
|
|
|
compact ? 'min-h-[48px] max-h-[96px]' : 'min-h-[72px] max-h-[140px]',
|
2026-06-25 01:33:48 +00:00
|
|
|
|
)}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
spellCheck={false}
|
2026-06-25 01:33:48 +00:00
|
|
|
|
placeholder={`${meta.queryLabel}…`}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
/>
|
|
|
|
|
|
|
2026-06-25 01:33:48 +00:00
|
|
|
|
{error && <p className="shrink-0 px-2 py-0.5 text-[10px] text-danger">{error}</p>}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
|
|
|
|
|
|
{benchmark?.comparison && (
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<div className="shrink-0 border-b border-amber-500/30 bg-amber-500/10 px-2 py-1 text-[10px]">
|
|
|
|
|
|
<span className="font-semibold text-amber-300">Benchmark: </span>
|
|
|
|
|
|
PG <span className="font-mono">{benchmark.comparison.postgres_ms}ms</span>
|
|
|
|
|
|
{' · '}Trino <span className="font-mono">{benchmark.comparison.trino_ms}ms</span>
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{' — '}
|
|
|
|
|
|
<span className="font-semibold text-success">
|
2026-06-25 01:33:48 +00:00
|
|
|
|
{benchmark.comparison.faster} {benchmark.comparison.speedup_factor ? `${benchmark.comparison.speedup_factor}×` : ''}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<div className="scrollbar-thin min-h-0 flex-1 overflow-auto p-1.5">
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{result?.ok && result.columns && (
|
|
|
|
|
|
<>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<p className="mb-0.5 font-mono text-[8px] text-foreground-faint">
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{result.row_count} rows · {result.elapsed_ms}ms
|
|
|
|
|
|
</p>
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<table className="w-full text-left font-mono text-[9px]">
|
2026-06-25 01:10:57 +00:00
|
|
|
|
<thead>
|
|
|
|
|
|
<tr className="border-b border-border text-docker">
|
2026-06-25 01:33:48 +00:00
|
|
|
|
{result.columns.map((c) => <th key={c} className="px-1 py-0.5">{c}</th>)}
|
2026-06-25 01:10:57 +00:00
|
|
|
|
</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) => (
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<td key={j} className="max-w-[160px] truncate px-1 py-0.5 text-foreground-muted">
|
2026-06-25 01:10:57 +00:00
|
|
|
|
{cell === null ? 'NULL' : String(cell)}
|
|
|
|
|
|
</td>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tr>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</tbody>
|
|
|
|
|
|
</table>
|
|
|
|
|
|
</>
|
|
|
|
|
|
)}
|
|
|
|
|
|
{!result && !loading && (
|
2026-06-25 01:33:48 +00:00
|
|
|
|
<p className="py-3 text-center text-[10px] text-foreground-faint">Pick a command or write {meta.queryLabel}</p>
|
2026-06-25 01:10:57 +00:00
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|