Add MySQL/MongoDB consoles, fit topology without scroll, longer flow lines.

Compact workbench panel, collapsible infra bar, narrower topology nodes with wider inter-stage gaps for visible connection lines.
This commit is contained in:
mo
2026-06-25 01:33:48 +00:00
parent 4e1cf9ef6c
commit 7da2dd60f0
9 changed files with 343 additions and 162 deletions
+4 -4
View File
@@ -36,16 +36,16 @@ function AgentCard({
type="button"
onClick={onSelect}
className={cn(
'flex h-[136px] w-[168px] shrink-0 flex-col gap-1 rounded-lg border bg-surface-raised p-1.5 text-left shadow-sm dark:bg-surface-overlay',
'flex h-[112px] w-[142px] shrink-0 flex-col gap-1 rounded-lg border bg-surface-raised p-1.5 text-left shadow-sm dark:bg-surface-overlay',
selected ? 'border-docker ring-2 ring-docker/30 shadow-[0_0_20px_rgba(13,183,237,0.15)]' : 'border-border hover:border-docker/30',
)}
>
<div className="flex items-center gap-1.5">
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-md bg-surface" style={{ color: meta.accent }}>
<Icon className="h-4 w-4" />
<span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-surface" style={{ color: meta.accent }}>
<Icon className="h-3.5 w-3.5" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-[12px] font-semibold text-foreground">{agent.name.split(' ·')[0]}</p>
<p className="truncate text-[11px] font-semibold text-foreground">{agent.name.split(' ·')[0]}</p>
<p className="truncate text-[8px] text-foreground-muted">{meta.domain}</p>
</div>
<span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', busy ? 'bg-success animate-pulse' : 'bg-foreground-faint/30')} />
+29 -33
View File
@@ -10,6 +10,7 @@ type Props = {
lines: TerminalLine[]
busy: boolean
onSendPrompt: (message: string, agentId?: string) => void
compact?: boolean
}
const LEVEL: Record<string, string> = {
@@ -23,7 +24,7 @@ const LEVEL: Record<string, string> = {
probe: 'text-amber-300',
}
export function AgentWorkbench({ agent, lines, busy, onSendPrompt }: Props) {
export function AgentWorkbench({ agent, lines, busy, onSendPrompt, compact }: Props) {
const meta = getAgentMeta(agent.id)
const Icon = meta.icon
const bottomRef = useRef<HTMLDivElement>(null)
@@ -33,49 +34,44 @@ export function AgentWorkbench({ agent, lines, busy, onSendPrompt }: Props) {
}, [lines, busy])
return (
<div className="flex h-full min-h-0 flex-col bg-[#0a0e14]">
<header className="flex shrink-0 items-center gap-3 border-b border-border/60 px-4 py-3">
<div className="flex h-full min-h-0 flex-col">
<header className={cn('flex shrink-0 items-center gap-2 border-b border-border/60 px-2', compact ? 'py-1' : 'py-3')}>
<span
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border border-border/80 bg-surface-overlay"
style={{ color: meta.accent, boxShadow: `0 0 24px ${meta.accent}33` }}
className={cn('flex shrink-0 items-center justify-center rounded-lg border border-border/80 bg-surface-overlay', compact ? 'h-7 w-7' : 'h-10 w-10')}
style={{ color: meta.accent, boxShadow: compact ? undefined : `0 0 24px ${meta.accent}33` }}
>
<Icon className="h-6 w-6" />
<Icon className={compact ? 'h-3.5 w-3.5' : 'h-5 w-5'} />
</span>
<div className="min-w-0 flex-1">
<h3 className="truncate text-base font-bold text-foreground">{agent.name}</h3>
<p className="text-[11px] text-docker">{meta.domain} · {agent.zone}</p>
<p className="truncate text-[10px] text-foreground-muted">{agent.role}</p>
<h3 className={cn('truncate font-bold text-foreground', compact ? 'text-xs' : 'text-sm')}>{agent.name}</h3>
{!compact && <p className="text-[10px] text-docker">{meta.domain} · {agent.zone}</p>}
</div>
<div className="flex shrink-0 items-center gap-2">
{busy && (
<span className="flex items-center gap-1 text-[10px] text-success">
<Loader2 className="h-3 w-3 animate-spin" /> Live
</span>
)}
<span className="flex items-center gap-1 rounded border border-emerald-500/40 bg-emerald-500/10 px-2 py-1 text-[10px] text-emerald-300">
<Terminal className="h-3 w-3" /> Agent terminal
<div className="flex shrink-0 items-center gap-1.5">
{busy && <Loader2 className="h-3 w-3 animate-spin text-success" />}
<span className="flex items-center gap-1 rounded border border-emerald-500/40 bg-emerald-500/10 px-1.5 py-0.5 text-[9px] text-emerald-300">
<Terminal className="h-3 w-3" /> Terminal
</span>
</div>
</header>
<div className="flex shrink-0 flex-wrap gap-1 border-b border-border/40 px-3 py-2">
{(agent.suggested_prompts || []).slice(0, 6).map((prompt) => (
<button
key={prompt}
type="button"
onClick={() => onSendPrompt(prompt, agent.id)}
className={cn('rounded border border-border/60 px-2 py-1 text-[10px] text-foreground-muted hover:border-docker/40 hover:text-docker', subTabIdle)}
>
{prompt}
</button>
))}
</div>
{!compact && (
<div className="flex shrink-0 flex-wrap gap-1 border-b border-border/40 px-2 py-1">
{(agent.suggested_prompts || []).slice(0, 4).map((prompt) => (
<button
key={prompt}
type="button"
onClick={() => onSendPrompt(prompt, agent.id)}
className={cn('rounded border border-border/60 px-1.5 py-0.5 text-[9px] text-foreground-muted hover:border-docker/40 hover:text-docker', subTabIdle)}
>
{prompt}
</button>
))}
</div>
)}
<div className="scrollbar-thin min-h-0 flex-1 overflow-y-auto px-4 py-2 font-mono text-[11px] leading-relaxed">
<div className={cn('scrollbar-thin min-h-0 flex-1 overflow-y-auto font-mono leading-relaxed', compact ? 'px-2 py-1 text-[10px]' : 'px-3 py-2 text-[11px]')}>
{lines.length === 0 && (
<p className="py-8 text-center text-foreground-faint">
Agent terminal ready probe output and LLM responses appear here
</p>
<p className="py-2 text-center text-foreground-faint">Agent terminal ready</p>
)}
{lines.map((line) => (
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}>
@@ -153,13 +153,17 @@ STAGES.forEach((stage, col) => {
})
function nodeCoords(col: number, row: number, rows: number) {
const colW = 100 / 5
const yPad = 8
const ySpan = 84
const colCount = 5
const gap = 3.2
const colW = (100 - gap * (colCount - 1)) / colCount
const xCenter = col * (colW + gap) + colW / 2
const nodeHalf = colW * 0.34
const yPad = 5
const ySpan = 90
const y = yPad + ((row + 0.5) / rows) * ySpan
return {
inX: col * colW + colW * 0.08,
outX: col * colW + colW * 0.92,
inX: xCenter - nodeHalf,
outX: xCenter + nodeHalf,
y,
}
}
@@ -279,7 +283,7 @@ export function PlatformTopology({ workload, animations, selectedNodeId, onNodeC
return (
<section className="panel flex h-full min-h-0 flex-1 flex-col overflow-hidden">
<header
className="flex shrink-0 flex-col gap-1 border-b border-border px-3 py-1.5"
className="flex shrink-0 flex-col gap-0.5 border-b border-border px-2 py-1"
style={{ background: 'var(--topo-header-bg)' }}
>
<div className="flex items-center justify-between gap-2">
@@ -290,7 +294,7 @@ export function PlatformTopology({ workload, animations, selectedNodeId, onNodeC
<div className="min-w-0">
<h2 className="truncate text-xs font-semibold text-foreground">Data Platform Topology</h2>
<p className="truncate text-[9px] text-foreground-muted">
Click PostgreSQL or Trino live SQL console · agents terminal below
Click PostgreSQL / MySQL / MongoDB / Trino live console · agents terminal below
</p>
</div>
</div>
@@ -361,7 +365,7 @@ export function PlatformTopology({ workload, animations, selectedNodeId, onNodeC
})}
</svg>
<div className="relative z-10 flex h-full min-h-0 w-full overflow-x-auto">
<div className="relative z-10 flex h-full min-h-0 w-full">
{STAGES.map((stage) => (
<div key={stage.id} className={cn('topo-stage-col', stage.accent)}>
<header className="mb-1 shrink-0 border-b border-white/10 pb-1">
+56 -37
View File
@@ -14,32 +14,47 @@ type SqlResult = {
sql?: string
}
type Engine = 'postgres' | 'mysql' | 'mongodb' | 'trino'
type Props = {
engine: 'postgres' | 'trino'
engine: Engine
compact?: boolean
}
const ENGINE_META = {
const ENGINE_META: Record<Engine, { title: string; sub: string; accent: string; queryLabel: string }> = {
postgres: {
title: 'PostgreSQL SQL Console',
sub: 'DB Vault · 10.0.21.51:5432 · user mo',
accent: 'text-blue-400',
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',
},
trino: {
title: 'Trino SQL Console',
sub: 'Lakehouse · 10.0.21.50:8089 · federated lakehouse queries',
sub: 'Lakehouse · 10.0.21.50:8089 · federated queries',
accent: 'text-violet-400',
queryLabel: 'SQL',
},
}
export function SqlWorkbench({ engine }: Props) {
export function SqlWorkbench({ engine, compact }: 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)
@@ -106,40 +121,41 @@ export function SqlWorkbench({ engine }: Props) {
}
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 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')}>
<div>
<h3 className={cn('flex items-center gap-2 text-sm font-semibold', meta.accent)}>
<Database className="h-4 w-4" />
<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'} />
{meta.title}
</h3>
<p className="text-[10px] text-foreground-muted">{meta.sub}</p>
<p className="text-[9px] 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)}>
<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)}>
{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>
{(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>
)}
</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">
<div className="flex min-h-0 flex-1">
<aside className={cn('shrink-0 border-r border-border/60 p-1.5', compact ? 'w-44' : 'w-52')}>
<p className="mb-0.5 text-[8px] font-semibold uppercase tracking-wider text-foreground-faint">10 demo commands</p>
<div className="scrollbar-thin max-h-full space-y-0.5 overflow-y-auto">
{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"
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"
>
<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>
@@ -149,42 +165,45 @@ export function SqlWorkbench({ engine }: Props) {
<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"
className={cn(
'shrink-0 resize-none border-b border-border/60 bg-black/40 p-1.5 font-mono text-[10px] text-emerald-100 outline-none focus:ring-1 focus:ring-docker/40',
compact ? 'min-h-[40px]' : 'min-h-[56px]',
)}
spellCheck={false}
placeholder={`${meta.queryLabel}`}
/>
{error && <p className="shrink-0 px-3 py-1 text-[11px] text-danger">{error}</p>}
{error && <p className="shrink-0 px-2 py-0.5 text-[10px] 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 (5M parallel compute): </span>
PostgreSQL <span className="font-mono">{benchmark.comparison.postgres_ms}ms</span>
{' · '}
Trino <span className="font-mono">{benchmark.comparison.trino_ms}ms</span>
<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>
{' — '}
<span className="font-semibold text-success">
{benchmark.comparison.faster} {benchmark.comparison.speedup_factor ? `${benchmark.comparison.speedup_factor}× faster` : ''}
{benchmark.comparison.faster} {benchmark.comparison.speedup_factor ? `${benchmark.comparison.speedup_factor}×` : ''}
</span>
</div>
)}
<div className="scrollbar-thin min-h-0 flex-1 overflow-auto p-2">
<div className="scrollbar-thin min-h-0 flex-1 overflow-auto p-1.5">
{result?.ok && result.columns && (
<>
<p className="mb-1 font-mono text-[9px] text-foreground-faint">
<p className="mb-0.5 font-mono text-[8px] text-foreground-faint">
{result.row_count} rows · {result.elapsed_ms}ms
</p>
<table className="w-full text-left font-mono text-[10px]">
<table className="w-full text-left font-mono text-[9px]">
<thead>
<tr className="border-b border-border text-docker">
{result.columns.map((c) => <th key={c} className="px-2 py-1">{c}</th>)}
{result.columns.map((c) => <th key={c} className="px-1 py-0.5">{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">
<td key={j} className="max-w-[160px] truncate px-1 py-0.5 text-foreground-muted">
{cell === null ? 'NULL' : String(cell)}
</td>
))}
@@ -195,7 +214,7 @@ export function SqlWorkbench({ engine }: Props) {
</>
)}
{!result && !loading && (
<p className="py-6 text-center text-[11px] text-foreground-faint">Pick a command or write SQL · click Run</p>
<p className="py-3 text-center text-[10px] text-foreground-faint">Pick a command or write {meta.queryLabel}</p>
)}
</div>
</div>
@@ -3,7 +3,7 @@ import { AgentWorkbench } from './AgentWorkbench'
import { SqlWorkbench } from './SqlWorkbench'
type Props = {
mode: 'agent' | 'sql-postgres' | 'sql-trino' | null
mode: 'agent' | 'sql-postgres' | 'sql-mysql' | 'sql-mongodb' | 'sql-trino' | null
agent: Agent | null
lines: TerminalLine[]
busy: boolean
@@ -14,12 +14,14 @@ export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props
if (!mode) return null
return (
<section className="flex h-[min(42vh,380px)] min-h-[260px] shrink-0 flex-col border-t-2 border-docker/30 shadow-[0_-8px_32px_rgba(0,0,0,0.35)]">
<section className="flex h-[190px] max-h-[26vh] shrink-0 flex-col border-t border-docker/25 bg-[#0a0e14] shadow-[0_-4px_20px_rgba(0,0,0,0.25)]">
{mode === 'agent' && agent && (
<AgentWorkbench agent={agent} lines={lines} busy={busy} onSendPrompt={onSendPrompt} />
<AgentWorkbench agent={agent} lines={lines} busy={busy} onSendPrompt={onSendPrompt} compact />
)}
{mode === 'sql-postgres' && <SqlWorkbench engine="postgres" />}
{mode === 'sql-trino' && <SqlWorkbench engine="trino" />}
{mode === 'sql-postgres' && <SqlWorkbench engine="postgres" compact />}
{mode === 'sql-mysql' && <SqlWorkbench engine="mysql" compact />}
{mode === 'sql-mongodb' && <SqlWorkbench engine="mongodb" compact />}
{mode === 'sql-trino' && <SqlWorkbench engine="trino" compact />}
</section>
)
}