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
+192 -48
View File
@@ -1,23 +1,35 @@
"""Live SQL console — PostgreSQL + Trino with benchmark."""
"""Live SQL console — PostgreSQL, MySQL, MongoDB + Trino with benchmark."""
from __future__ import annotations
import os
import re
import time
from typing import Any
import httpx
import psycopg2
import pymysql
from fastapi import APIRouter
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
from pymongo import MongoClient
PG_HOST = os.getenv("DB_VAULT_HOST", "10.0.21.51")
DB_HOST = os.getenv("DB_VAULT_HOST", "10.0.21.51")
PG_PORT = int(os.getenv("PG_PORT", "5432"))
PG_USER = os.getenv("PG_USER", "mo")
PG_PASS = os.getenv("PG_PASSWORD", "Dell2026!")
PG_DB = os.getenv("PG_DATABASE", "postgres")
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
MYSQL_USER = os.getenv("MYSQL_USER", "mo")
MYSQL_PASS = os.getenv("MYSQL_PASSWORD", "Dell2026!")
MYSQL_DB = os.getenv("MYSQL_DATABASE", "hr")
MONGO_HOST = os.getenv("MONGO_HOST", DB_HOST)
MONGO_PORT = int(os.getenv("MONGO_PORT", "27017"))
MONGO_DB = os.getenv("MONGO_DATABASE", "supplychain")
TRINO_URL = os.getenv("TRINO_URL", "http://10.0.21.50:8089").rstrip("/")
TRINO_USER = os.getenv("TRINO_USER", "atc")
@@ -36,6 +48,30 @@ SAMPLES: dict[str, list[dict[str, str]]] = {
{"id": "pg9", "label": "Explain scan 100k", "sql": "EXPLAIN ANALYZE SELECT count(*) FROM generate_series(1, 100000);"},
{"id": "pg10", "label": "Memory settings", "sql": "SELECT name, setting, unit FROM pg_settings WHERE name IN ('max_connections','shared_buffers','work_mem','effective_cache_size');"},
],
"mysql": [
{"id": "my1", "label": "Server version", "sql": "SELECT VERSION() AS version;"},
{"id": "my2", "label": "Current session", "sql": "SELECT DATABASE() AS db, USER() AS user, NOW() AS now_ts;"},
{"id": "my3", "label": "HR tables", "sql": "SHOW TABLES;"},
{"id": "my4", "label": "Table sizes", "sql": "SELECT table_name, table_rows, ROUND((data_length+index_length)/1024/1024,1) AS mb FROM information_schema.tables WHERE table_schema='hr' ORDER BY (data_length+index_length) DESC;"},
{"id": "my5", "label": "Employee events sample", "sql": "SELECT event_type, COUNT(*) AS cnt FROM employee_events GROUP BY event_type ORDER BY cnt DESC LIMIT 10;"},
{"id": "my6", "label": "Recent events", "sql": "SELECT employee_id, event_type, event_date FROM employee_events ORDER BY event_date DESC LIMIT 10;"},
{"id": "my7", "label": "Active connections", "sql": "SHOW STATUS LIKE 'Threads_connected';"},
{"id": "my8", "label": "InnoDB buffer pool", "sql": "SHOW STATUS LIKE 'Innodb_buffer_pool%';"},
{"id": "my9", "label": "Explain count scan", "sql": "EXPLAIN SELECT COUNT(*) FROM employee_events;"},
{"id": "my10", "label": "Departments overview", "sql": "SELECT department, COUNT(*) AS employees FROM employees GROUP BY department ORDER BY employees DESC LIMIT 10;"},
],
"mongodb": [
{"id": "mg1", "label": "List databases", "sql": "SHOW DATABASES"},
{"id": "mg2", "label": "Supplychain collections", "sql": "SHOW COLLECTIONS supplychain"},
{"id": "mg3", "label": "Count events", "sql": "COUNT supplychain.events"},
{"id": "mg4", "label": "Sample document", "sql": "FIND supplychain.events LIMIT 3"},
{"id": "mg5", "label": "Events by type", "sql": "AGGREGATE supplychain.events GROUP type TOP 10"},
{"id": "mg6", "label": "Events by region", "sql": "AGGREGATE supplychain.events GROUP region TOP 10"},
{"id": "mg7", "label": "Distinct event types", "sql": "DISTINCT supplychain.events type"},
{"id": "mg8", "label": "Server status", "sql": "SERVER STATUS"},
{"id": "mg9", "label": "Indexes on events", "sql": "INDEXES supplychain.events"},
{"id": "mg10", "label": "Recent events", "sql": "FIND supplychain.events SORT timestamp DESC LIMIT 5"},
],
"trino": [
{"id": "tq1", "label": "Trino version", "sql": "SELECT version()"},
{"id": "tq2", "label": "Catalogs", "sql": "SHOW CATALOGS"},
@@ -56,10 +92,22 @@ BENCHMARK_SQL = {
}
def _tabular(columns: list[str], rows: list[list[Any]], elapsed_ms: int, **extra: Any) -> dict[str, Any]:
return {
"ok": True,
"columns": columns,
"rows": rows,
"row_count": len(rows),
"elapsed_ms": elapsed_ms,
"truncated": extra.pop("truncated", False),
**extra,
}
def _run_postgres(sql: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter()
conn = psycopg2.connect(
host=PG_HOST, port=PG_PORT, user=PG_USER, password=PG_PASS, dbname=PG_DB, connect_timeout=8,
host=DB_HOST, port=PG_PORT, user=PG_USER, password=PG_PASS, dbname=PG_DB, connect_timeout=8,
)
try:
conn.set_session(readonly=True, autocommit=True)
@@ -68,14 +116,112 @@ def _run_postgres(sql: str, limit: int = 200) -> dict[str, Any]:
elapsed_ms = int((time.perf_counter() - t0) * 1000)
if cur.description:
columns = [d[0] for d in cur.description]
rows = cur.fetchmany(limit)
data = [list(r) for r in rows]
return {"ok": True, "columns": columns, "rows": data, "row_count": len(data), "elapsed_ms": elapsed_ms, "truncated": len(data) >= limit}
return {"ok": True, "columns": [], "rows": [], "row_count": 0, "elapsed_ms": elapsed_ms, "message": "OK"}
rows = [list(r) for r in cur.fetchmany(limit)]
return _tabular(columns, rows, elapsed_ms, truncated=len(rows) >= limit)
return _tabular([], [], elapsed_ms, message="OK")
finally:
conn.close()
def _run_mysql(sql: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter()
conn = pymysql.connect(
host=DB_HOST, port=MYSQL_PORT, user=MYSQL_USER, password=MYSQL_PASS,
database=MYSQL_DB, connect_timeout=8, read_timeout=30,
)
try:
cur = conn.cursor()
cur.execute(sql)
elapsed_ms = int((time.perf_counter() - t0) * 1000)
if cur.description:
columns = [d[0] for d in cur.description]
rows = [list(r) for r in cur.fetchmany(limit)]
return _tabular(columns, rows, elapsed_ms, truncated=len(rows) >= limit)
return _tabular([], [], elapsed_ms, message="OK")
finally:
conn.close()
def _mongo_client() -> MongoClient:
return MongoClient(f"mongodb://{MONGO_HOST}:{MONGO_PORT}/", serverSelectionTimeoutMS=8000)
def _fmt_mongo_value(val: Any) -> Any:
if val is None:
return None
if isinstance(val, (str, int, float, bool)):
return val
return str(val)[:200]
def _run_mongo(query: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter()
q = query.strip()
client = _mongo_client()
try:
columns: list[str] = []
rows: list[list[Any]] = []
if q == "SHOW DATABASES":
columns = ["database"]
rows = [[name] for name in client.list_database_names()]
elif m := re.match(r"^SHOW COLLECTIONS(?:\s+(\w+))?$", q, re.I):
db_name = m.group(1) or MONGO_DB
columns = ["collection"]
rows = [[name] for name in client[db_name].list_collection_names()]
elif m := re.match(r"^COUNT\s+(\w+)\.(\w+)$", q, re.I):
db_name, coll = m.group(1), m.group(2)
columns = ["count"]
rows = [[client[db_name][coll].estimated_document_count()]]
elif m := re.match(r"^FIND\s+(\w+)\.(\w+)(?:\s+SORT\s+(\w+)\s+DESC)?(?:\s+LIMIT\s+(\d+))?$", q, re.I):
db_name, coll, sort_field, lim = m.group(1), m.group(2), m.group(3), m.group(4)
n = min(int(lim) if lim else 10, limit)
cursor = client[db_name][coll].find({})
if sort_field:
cursor = cursor.sort(sort_field, -1)
docs = list(cursor.limit(n))
if not docs:
columns = ["result"]
rows = [["(empty)"]]
else:
columns = sorted({k for d in docs for k in d if k != "_id"})
rows = [[_fmt_mongo_value(d.get(c)) for c in columns] for d in docs]
elif m := re.match(r"^DISTINCT\s+(\w+)\.(\w+)\s+(\w+)$", q, re.I):
db_name, coll, field = m.group(1), m.group(2), m.group(3)
columns = [field]
vals = client[db_name][coll].distinct(field)[:limit]
rows = [[_fmt_mongo_value(v)] for v in vals]
elif m := re.match(r"^AGGREGATE\s+(\w+)\.(\w+)\s+GROUP\s+(\w+)\s+TOP\s+(\d+)$", q, re.I):
db_name, coll, field, top_n = m.group(1), m.group(2), m.group(3), int(m.group(4))
pipe = [
{"$group": {"_id": f"${field}", "count": {"$sum": 1}}},
{"$sort": {"count": -1}},
{"$limit": min(top_n, limit)},
]
columns = [field, "count"]
rows = [[r.get("_id"), r.get("count")] for r in client[db_name][coll].aggregate(pipe, maxTimeMS=15000)]
elif m := re.match(r"^INDEXES\s+(\w+)\.(\w+)$", q, re.I):
db_name, coll = m.group(1), m.group(2)
columns = ["name", "keys"]
rows = [[i.get("name"), str(i.get("key"))] for i in client[db_name][coll].list_indexes()]
elif q.upper() == "SERVER STATUS":
status = client.admin.command("serverStatus")
columns = ["metric", "value"]
rows = [
["version", status.get("version")],
["uptime_secs", status.get("uptime")],
["connections_current", status.get("connections", {}).get("current")],
["mem_resident_mb", status.get("mem", {}).get("resident")],
]
else:
return {"ok": False, "error": f"Unknown MongoDB command: {q[:120]}"}
elapsed_ms = int((time.perf_counter() - t0) * 1000)
return _tabular(columns, rows[:limit], elapsed_ms, truncated=len(rows) > limit)
finally:
client.close()
def _run_trino(sql: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter()
headers = {"X-Trino-User": TRINO_USER, "Content-Type": "text/plain"}
@@ -104,19 +250,14 @@ def _run_trino(sql: str, limit: int = 200) -> dict[str, Any]:
data = client.get(nxt, headers=headers).json()
elapsed_ms = int((time.perf_counter() - t0) * 1000)
stats = data.get("stats") or {}
return {
"ok": True,
"columns": columns,
"rows": rows,
"row_count": len(rows),
"elapsed_ms": elapsed_ms,
"engine_stats_ms": stats.get("elapsedTimeMillis"),
"truncated": len(rows) >= limit,
}
return _tabular(columns, rows, elapsed_ms, engine_stats_ms=stats.get("elapsedTimeMillis"), truncated=len(rows) >= limit)
ENGINES = ("postgres", "mysql", "mongodb", "trino")
class SqlRequest(BaseModel):
engine: str = Field(..., pattern="^(postgres|trino)$")
engine: str = Field(..., pattern="^(postgres|mysql|mongodb|trino)$")
sql: str = Field(..., min_length=1, max_length=8000)
@@ -124,51 +265,58 @@ class SqlRequest(BaseModel):
async def get_samples(engine: str):
if engine not in SAMPLES:
return JSONResponse({"error": "unknown engine"}, status_code=404)
return {
"engine": engine,
"samples": SAMPLES[engine],
"connection": _connection_info(engine),
}
return {"engine": engine, "samples": SAMPLES[engine], "connection": _connection_info(engine)}
@router.get("/health")
async def sql_health():
pg_ok = trino_ok = False
pg_err = trino_err = None
out: dict[str, Any] = {}
for eng in ENGINES:
try:
if eng == "postgres":
_run_postgres("SELECT 1")
pg_ok = True
except Exception as exc:
pg_err = str(exc)[:200]
try:
out[eng] = {"ok": True, "host": DB_HOST, "user": PG_USER, "error": None}
elif eng == "mysql":
_run_mysql("SELECT 1")
out[eng] = {"ok": True, "host": DB_HOST, "database": MYSQL_DB, "user": MYSQL_USER, "error": None}
elif eng == "mongodb":
_run_mongo("SHOW DATABASES")
out[eng] = {"ok": True, "host": MONGO_HOST, "database": MONGO_DB, "error": None}
else:
r = _run_trino("SELECT 1")
trino_ok = bool(r.get("ok"))
if not trino_ok:
trino_err = r.get("error")
out[eng] = {"ok": bool(r.get("ok")), "url": TRINO_URL, "user": TRINO_USER, "error": r.get("error")}
except Exception as exc:
trino_err = str(exc)[:200]
return {
"postgres": {"ok": pg_ok, "host": PG_HOST, "user": PG_USER, "error": pg_err},
"trino": {"ok": trino_ok, "url": TRINO_URL, "user": TRINO_USER, "error": trino_err},
}
out[eng] = {"ok": False, "error": str(exc)[:200]}
return out
def _connection_info(engine: str) -> dict[str, str]:
if engine == "postgres":
return {"host": PG_HOST, "port": str(PG_PORT), "database": PG_DB, "user": PG_USER}
return {"host": DB_HOST, "port": str(PG_PORT), "database": PG_DB, "user": PG_USER}
if engine == "mysql":
return {"host": DB_HOST, "port": str(MYSQL_PORT), "database": MYSQL_DB, "user": MYSQL_USER}
if engine == "mongodb":
return {"host": MONGO_HOST, "port": str(MONGO_PORT), "database": MONGO_DB}
return {"url": TRINO_URL, "user": TRINO_USER}
def _dispatch(engine: str, sql: str) -> dict[str, Any]:
if engine == "postgres":
return _run_postgres(sql)
if engine == "mysql":
return _run_mysql(sql)
if engine == "mongodb":
return _run_mongo(sql)
return _run_trino(sql)
@router.post("/execute")
async def execute_sql(body: SqlRequest):
sql = body.sql.strip().rstrip(";")
if not sql:
return JSONResponse({"ok": False, "error": "Empty query"}, status_code=400)
try:
if body.engine == "postgres":
result = _run_postgres(sql)
else:
result = _run_trino(sql)
result = _dispatch(body.engine, sql)
if not result.get("ok"):
return JSONResponse(result, status_code=422)
return {**result, "engine": body.engine, "sql": sql}
@@ -182,21 +330,17 @@ async def benchmark():
results: dict[str, Any] = {}
for engine, sql in BENCHMARK_SQL.items():
try:
if engine == "postgres":
results[engine] = _run_postgres(sql)
else:
results[engine] = _run_trino(sql)
results[engine] = _dispatch(engine, sql)
results[engine]["sql"] = sql
except Exception as exc:
results[engine] = {"ok": False, "error": str(exc)[:300], "sql": sql}
pg_ms = results.get("postgres", {}).get("elapsed_ms")
tr_ms = results.get("trino", {}).get("elapsed_ms")
faster = None
speedup = None
if pg_ms and tr_ms:
faster = "trino" if tr_ms < pg_ms else "postgres"
speedup = round(pg_ms / tr_ms, 2) if tr_ms and tr_ms < pg_ms else round(tr_ms / pg_ms, 2) if pg_ms else None
else:
speedup = None
speedup = round(pg_ms / tr_ms, 2) if tr_ms < pg_ms else round(tr_ms / pg_ms, 2)
return {
"ok": True,
"postgres": results.get("postgres"),
+16 -4
View File
@@ -1,4 +1,5 @@
import { useRef, useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useClock } from './hooks/useClock'
import { useCommandCenter } from './hooks/useCommandCenter'
import { useLiveMetrics } from './hooks/useLiveMetrics'
@@ -25,6 +26,7 @@ export default function App() {
const clock = useClock()
const cc = useCommandCenter()
const [gpuChatActive, setGpuChatActive] = useState(false)
const [infraOpen, setInfraOpen] = useState(false)
const gpuBoost = gpuChatActive || cc.mainView === 'knowledge'
const { agentLoads, gpuLive } = useLiveMetrics(cc.agents, cc.gpu, cc.anims, gpuBoost)
const mainScrollRef = useRef<HTMLDivElement>(null)
@@ -74,9 +76,9 @@ export default function App() {
onSelectZone={cc.selectNode}
/>
<div ref={mainScrollRef} className="scrollbar-thin flex min-h-0 min-w-0 flex-1 flex-col overflow-y-auto bg-surface">
<div ref={mainScrollRef} className={cn('flex min-h-0 min-w-0 flex-1 flex-col bg-surface', isPlatform ? 'overflow-hidden' : 'scrollbar-thin overflow-y-auto')}>
<div className={cn('flex min-h-0 flex-1', isPlatform ? '' : 'flex-col')}>
<div className={cn('flex min-w-0 flex-1 flex-col gap-2', isPlatform ? 'p-2' : 'min-h-0 p-3')}>
<div className={cn('flex min-h-0 min-w-0 flex-1 flex-col', isPlatform ? 'gap-1 overflow-hidden p-1.5' : 'min-h-0 gap-2 p-3')}>
{isPlatform && (
<>
<div className="grid shrink-0 grid-cols-1 gap-2 xl:grid-cols-[1fr_auto]">
@@ -91,6 +93,15 @@ export default function App() {
/>
<GpuMonitor gpu={cc.gpu} live={gpuLive} />
</div>
<button
type="button"
onClick={() => setInfraOpen((v) => !v)}
className="flex shrink-0 items-center gap-1 self-start rounded border border-border/60 px-2 py-0.5 text-[9px] text-foreground-muted hover:border-docker/30 hover:text-docker"
>
{infraOpen ? <ChevronUp className="h-3 w-3" /> : <ChevronDown className="h-3 w-3" />}
Infrastructure & Apps
</button>
{infraOpen && (
<InfraQuickAccess
workload={cc.workload}
agents={cc.agents}
@@ -101,10 +112,11 @@ export default function App() {
onProbe={cc.probeNodeId}
onOpenTerminal={cc.openTerminal}
/>
)}
</>
)}
<div className={cn('flex min-h-0 flex-col', isPlatform ? 'min-h-[300px] flex-1' : 'min-h-0 flex-1')}>
<div className={cn('flex min-h-0 flex-col', isPlatform ? 'min-h-0 flex-1 overflow-hidden' : 'min-h-0 flex-1')}>
{cc.mainView === 'platform' ? (
<PlatformTopology
workload={cc.workload}
@@ -139,7 +151,7 @@ export default function App() {
</div>
{isPlatform && (
<div className="flex w-[340px] shrink-0 flex-col border-l border-border">
<div className="flex w-[280px] shrink-0 flex-col border-l border-border">
<InspectorPanel
node={cc.selectedNode}
nodeDetail={cc.nodeDetail}
+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')} />
+20 -24
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) => (
{!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-2 py-1 text-[10px] text-foreground-muted hover:border-docker/40 hover:text-docker', subTabIdle)}
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">
+54 -35
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)}>
{(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" />}
Postgres vs Trino
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>
)
}
+8 -4
View File
@@ -36,14 +36,18 @@ function resolveProbeId(nodeId: string) {
}
const SQL_NODE_ENGINES: Record<string, 'postgres' | 'trino'> = {
const SQL_NODE_ENGINES: Record<string, 'postgres' | 'mysql' | 'mongodb' | 'trino'> = {
postgresql: 'postgres',
'src-postgres': 'postgres',
mysql: 'mysql',
'src-mysql': 'mysql',
mongodb: 'mongodb',
'src-mongo': 'mongodb',
trino: 'trino',
'query-trino': 'trino',
}
function resolveSqlEngine(nodeId: string): 'postgres' | 'trino' | null {
function resolveSqlEngine(nodeId: string): 'postgres' | 'mysql' | 'mongodb' | 'trino' | null {
return SQL_NODE_ENGINES[nodeId] || SQL_NODE_ENGINES[resolveProbeId(nodeId)] || null
}
@@ -66,7 +70,7 @@ export function useCommandCenter() {
const [nodeBusy, setNodeBusy] = useState(false)
const [mainView, setMainView] = useState<'platform' | 'approvals' | 'presentation' | 'dataquality' | 'knowledge' | 'storage' | 'search'>('platform')
const [approvalHighlight, setApprovalHighlight] = useState(false)
const [workbenchMode, setWorkbenchMode] = useState<'agent' | 'sql-postgres' | 'sql-trino' | null>(null)
const [workbenchMode, setWorkbenchMode] = useState<'agent' | 'sql-postgres' | 'sql-mysql' | 'sql-mongodb' | 'sql-trino' | null>(null)
const [chatExpanded, setChatExpanded] = useState(false)
const promptTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [terminalExpanded, setTerminalExpanded] = useState(true)
@@ -225,7 +229,7 @@ export function useCommandCenter() {
const probeId = resolveProbeId(nodeId)
const sqlEng = resolveSqlEngine(nodeId)
if (sqlEng) {
setWorkbenchMode(sqlEng === 'postgres' ? 'sql-postgres' : 'sql-trino')
setWorkbenchMode(`sql-${sqlEng}` as 'sql-postgres' | 'sql-mysql' | 'sql-mongodb' | 'sql-trino')
setSelectedAgentId(null)
} else if (options?.keepWorkbench) {
setWorkbenchMode(options.keepWorkbench)
+5 -5
View File
@@ -87,14 +87,14 @@
}
.topo-edge-idle {
stroke: rgba(100, 140, 180, 0.35);
stroke-width: 1.5;
stroke: rgba(100, 140, 180, 0.45);
stroke-width: 2;
stroke-dasharray: 4 8;
fill: none;
}
.topo-edge-live {
stroke-width: 2;
stroke-width: 2.5;
stroke-dasharray: 8 12;
fill: none;
animation: flow-dash 1.2s linear infinite;
@@ -130,7 +130,7 @@
}
.topo-node {
@apply w-full rounded-md border px-2 py-1.5 text-left transition-all;
@apply mx-auto w-[84%] rounded-md border px-1.5 py-1 text-left transition-all;
background: var(--topo-node-bg);
border-color: var(--topo-node-border);
box-shadow: var(--topo-node-shadow);
@@ -146,7 +146,7 @@
}
.topo-stage-col {
@apply flex min-w-[130px] flex-1 flex-col px-1.5 py-2 last:border-r-0;
@apply flex min-w-0 flex-1 flex-col px-0.5 py-1 last:border-r-0;
border-right: 1px dashed var(--topo-stage-border);
}