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 from __future__ import annotations
import os import os
import re
import time import time
from typing import Any from typing import Any
import httpx import httpx
import psycopg2 import psycopg2
import pymysql
from fastapi import APIRouter from fastapi import APIRouter
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field 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_PORT = int(os.getenv("PG_PORT", "5432"))
PG_USER = os.getenv("PG_USER", "mo") PG_USER = os.getenv("PG_USER", "mo")
PG_PASS = os.getenv("PG_PASSWORD", "Dell2026!") PG_PASS = os.getenv("PG_PASSWORD", "Dell2026!")
PG_DB = os.getenv("PG_DATABASE", "postgres") 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_URL = os.getenv("TRINO_URL", "http://10.0.21.50:8089").rstrip("/")
TRINO_USER = os.getenv("TRINO_USER", "atc") 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": "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');"}, {"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": [ "trino": [
{"id": "tq1", "label": "Trino version", "sql": "SELECT version()"}, {"id": "tq1", "label": "Trino version", "sql": "SELECT version()"},
{"id": "tq2", "label": "Catalogs", "sql": "SHOW CATALOGS"}, {"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]: def _run_postgres(sql: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter() t0 = time.perf_counter()
conn = psycopg2.connect( 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: try:
conn.set_session(readonly=True, autocommit=True) 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) elapsed_ms = int((time.perf_counter() - t0) * 1000)
if cur.description: if cur.description:
columns = [d[0] for d in cur.description] columns = [d[0] for d in cur.description]
rows = cur.fetchmany(limit) rows = [list(r) for r in cur.fetchmany(limit)]
data = [list(r) for r in rows] return _tabular(columns, rows, elapsed_ms, truncated=len(rows) >= limit)
return {"ok": True, "columns": columns, "rows": data, "row_count": len(data), "elapsed_ms": elapsed_ms, "truncated": len(data) >= limit} return _tabular([], [], elapsed_ms, message="OK")
return {"ok": True, "columns": [], "rows": [], "row_count": 0, "elapsed_ms": elapsed_ms, "message": "OK"}
finally: finally:
conn.close() 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]: def _run_trino(sql: str, limit: int = 200) -> dict[str, Any]:
t0 = time.perf_counter() t0 = time.perf_counter()
headers = {"X-Trino-User": TRINO_USER, "Content-Type": "text/plain"} 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() data = client.get(nxt, headers=headers).json()
elapsed_ms = int((time.perf_counter() - t0) * 1000) elapsed_ms = int((time.perf_counter() - t0) * 1000)
stats = data.get("stats") or {} stats = data.get("stats") or {}
return { return _tabular(columns, rows, elapsed_ms, engine_stats_ms=stats.get("elapsedTimeMillis"), truncated=len(rows) >= limit)
"ok": True,
"columns": columns,
"rows": rows, ENGINES = ("postgres", "mysql", "mongodb", "trino")
"row_count": len(rows),
"elapsed_ms": elapsed_ms,
"engine_stats_ms": stats.get("elapsedTimeMillis"),
"truncated": len(rows) >= limit,
}
class SqlRequest(BaseModel): 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) sql: str = Field(..., min_length=1, max_length=8000)
@@ -124,51 +265,58 @@ class SqlRequest(BaseModel):
async def get_samples(engine: str): async def get_samples(engine: str):
if engine not in SAMPLES: if engine not in SAMPLES:
return JSONResponse({"error": "unknown engine"}, status_code=404) return JSONResponse({"error": "unknown engine"}, status_code=404)
return { return {"engine": engine, "samples": SAMPLES[engine], "connection": _connection_info(engine)}
"engine": engine,
"samples": SAMPLES[engine],
"connection": _connection_info(engine),
}
@router.get("/health") @router.get("/health")
async def sql_health(): async def sql_health():
pg_ok = trino_ok = False out: dict[str, Any] = {}
pg_err = trino_err = None for eng in ENGINES:
try: try:
if eng == "postgres":
_run_postgres("SELECT 1") _run_postgres("SELECT 1")
pg_ok = True out[eng] = {"ok": True, "host": DB_HOST, "user": PG_USER, "error": None}
except Exception as exc: elif eng == "mysql":
pg_err = str(exc)[:200] _run_mysql("SELECT 1")
try: 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") r = _run_trino("SELECT 1")
trino_ok = bool(r.get("ok")) out[eng] = {"ok": bool(r.get("ok")), "url": TRINO_URL, "user": TRINO_USER, "error": r.get("error")}
if not trino_ok:
trino_err = r.get("error")
except Exception as exc: except Exception as exc:
trino_err = str(exc)[:200] out[eng] = {"ok": False, "error": str(exc)[:200]}
return { return out
"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},
}
def _connection_info(engine: str) -> dict[str, str]: def _connection_info(engine: str) -> dict[str, str]:
if engine == "postgres": 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} 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") @router.post("/execute")
async def execute_sql(body: SqlRequest): async def execute_sql(body: SqlRequest):
sql = body.sql.strip().rstrip(";") sql = body.sql.strip().rstrip(";")
if not sql: if not sql:
return JSONResponse({"ok": False, "error": "Empty query"}, status_code=400) return JSONResponse({"ok": False, "error": "Empty query"}, status_code=400)
try: try:
if body.engine == "postgres": result = _dispatch(body.engine, sql)
result = _run_postgres(sql)
else:
result = _run_trino(sql)
if not result.get("ok"): if not result.get("ok"):
return JSONResponse(result, status_code=422) return JSONResponse(result, status_code=422)
return {**result, "engine": body.engine, "sql": sql} return {**result, "engine": body.engine, "sql": sql}
@@ -182,21 +330,17 @@ async def benchmark():
results: dict[str, Any] = {} results: dict[str, Any] = {}
for engine, sql in BENCHMARK_SQL.items(): for engine, sql in BENCHMARK_SQL.items():
try: try:
if engine == "postgres": results[engine] = _dispatch(engine, sql)
results[engine] = _run_postgres(sql)
else:
results[engine] = _run_trino(sql)
results[engine]["sql"] = sql results[engine]["sql"] = sql
except Exception as exc: except Exception as exc:
results[engine] = {"ok": False, "error": str(exc)[:300], "sql": sql} results[engine] = {"ok": False, "error": str(exc)[:300], "sql": sql}
pg_ms = results.get("postgres", {}).get("elapsed_ms") pg_ms = results.get("postgres", {}).get("elapsed_ms")
tr_ms = results.get("trino", {}).get("elapsed_ms") tr_ms = results.get("trino", {}).get("elapsed_ms")
faster = None faster = None
speedup = None
if pg_ms and tr_ms: if pg_ms and tr_ms:
faster = "trino" if tr_ms < pg_ms else "postgres" 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 speedup = round(pg_ms / tr_ms, 2) if tr_ms < pg_ms else round(tr_ms / pg_ms, 2)
else:
speedup = None
return { return {
"ok": True, "ok": True,
"postgres": results.get("postgres"), "postgres": results.get("postgres"),
+16 -4
View File
@@ -1,4 +1,5 @@
import { useRef, useState } from 'react' import { useRef, useState } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { useClock } from './hooks/useClock' import { useClock } from './hooks/useClock'
import { useCommandCenter } from './hooks/useCommandCenter' import { useCommandCenter } from './hooks/useCommandCenter'
import { useLiveMetrics } from './hooks/useLiveMetrics' import { useLiveMetrics } from './hooks/useLiveMetrics'
@@ -25,6 +26,7 @@ export default function App() {
const clock = useClock() const clock = useClock()
const cc = useCommandCenter() const cc = useCommandCenter()
const [gpuChatActive, setGpuChatActive] = useState(false) const [gpuChatActive, setGpuChatActive] = useState(false)
const [infraOpen, setInfraOpen] = useState(false)
const gpuBoost = gpuChatActive || cc.mainView === 'knowledge' const gpuBoost = gpuChatActive || cc.mainView === 'knowledge'
const { agentLoads, gpuLive } = useLiveMetrics(cc.agents, cc.gpu, cc.anims, gpuBoost) const { agentLoads, gpuLive } = useLiveMetrics(cc.agents, cc.gpu, cc.anims, gpuBoost)
const mainScrollRef = useRef<HTMLDivElement>(null) const mainScrollRef = useRef<HTMLDivElement>(null)
@@ -74,9 +76,9 @@ export default function App() {
onSelectZone={cc.selectNode} 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-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 && ( {isPlatform && (
<> <>
<div className="grid shrink-0 grid-cols-1 gap-2 xl:grid-cols-[1fr_auto]"> <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} /> <GpuMonitor gpu={cc.gpu} live={gpuLive} />
</div> </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 <InfraQuickAccess
workload={cc.workload} workload={cc.workload}
agents={cc.agents} agents={cc.agents}
@@ -101,10 +112,11 @@ export default function App() {
onProbe={cc.probeNodeId} onProbe={cc.probeNodeId}
onOpenTerminal={cc.openTerminal} 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' ? ( {cc.mainView === 'platform' ? (
<PlatformTopology <PlatformTopology
workload={cc.workload} workload={cc.workload}
@@ -139,7 +151,7 @@ export default function App() {
</div> </div>
{isPlatform && ( {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 <InspectorPanel
node={cc.selectedNode} node={cc.selectedNode}
nodeDetail={cc.nodeDetail} nodeDetail={cc.nodeDetail}
+4 -4
View File
@@ -36,16 +36,16 @@ function AgentCard({
type="button" type="button"
onClick={onSelect} onClick={onSelect}
className={cn( 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', 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"> <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 }}> <span className="flex h-6 w-6 shrink-0 items-center justify-center rounded-md bg-surface" style={{ color: meta.accent }}>
<Icon className="h-4 w-4" /> <Icon className="h-3.5 w-3.5" />
</span> </span>
<div className="min-w-0 flex-1"> <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> <p className="truncate text-[8px] text-foreground-muted">{meta.domain}</p>
</div> </div>
<span className={cn('h-1.5 w-1.5 shrink-0 rounded-full', busy ? 'bg-success animate-pulse' : 'bg-foreground-faint/30')} /> <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[] lines: TerminalLine[]
busy: boolean busy: boolean
onSendPrompt: (message: string, agentId?: string) => void onSendPrompt: (message: string, agentId?: string) => void
compact?: boolean
} }
const LEVEL: Record<string, string> = { const LEVEL: Record<string, string> = {
@@ -23,7 +24,7 @@ const LEVEL: Record<string, string> = {
probe: 'text-amber-300', 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 meta = getAgentMeta(agent.id)
const Icon = meta.icon const Icon = meta.icon
const bottomRef = useRef<HTMLDivElement>(null) const bottomRef = useRef<HTMLDivElement>(null)
@@ -33,49 +34,44 @@ export function AgentWorkbench({ agent, lines, busy, onSendPrompt }: Props) {
}, [lines, busy]) }, [lines, busy])
return ( return (
<div className="flex h-full min-h-0 flex-col bg-[#0a0e14]"> <div className="flex h-full min-h-0 flex-col">
<header className="flex shrink-0 items-center gap-3 border-b border-border/60 px-4 py-3"> <header className={cn('flex shrink-0 items-center gap-2 border-b border-border/60 px-2', compact ? 'py-1' : 'py-3')}>
<span <span
className="flex h-12 w-12 shrink-0 items-center justify-center rounded-xl border border-border/80 bg-surface-overlay" 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: `0 0 24px ${meta.accent}33` }} 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> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<h3 className="truncate text-base font-bold text-foreground">{agent.name}</h3> <h3 className={cn('truncate font-bold text-foreground', compact ? 'text-xs' : 'text-sm')}>{agent.name}</h3>
<p className="text-[11px] text-docker">{meta.domain} · {agent.zone}</p> {!compact && <p className="text-[10px] text-docker">{meta.domain} · {agent.zone}</p>}
<p className="truncate text-[10px] text-foreground-muted">{agent.role}</p>
</div> </div>
<div className="flex shrink-0 items-center gap-2"> <div className="flex shrink-0 items-center gap-1.5">
{busy && ( {busy && <Loader2 className="h-3 w-3 animate-spin text-success" />}
<span className="flex items-center gap-1 text-[10px] 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">
<Loader2 className="h-3 w-3 animate-spin" /> Live <Terminal className="h-3 w-3" /> Terminal
</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
</span> </span>
</div> </div>
</header> </header>
<div className="flex shrink-0 flex-wrap gap-1 border-b border-border/40 px-3 py-2"> {!compact && (
{(agent.suggested_prompts || []).slice(0, 6).map((prompt) => ( <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 <button
key={prompt} key={prompt}
type="button" type="button"
onClick={() => onSendPrompt(prompt, agent.id)} 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} {prompt}
</button> </button>
))} ))}
</div> </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 && ( {lines.length === 0 && (
<p className="py-8 text-center text-foreground-faint"> <p className="py-2 text-center text-foreground-faint">Agent terminal ready</p>
Agent terminal ready probe output and LLM responses appear here
</p>
)} )}
{lines.map((line) => ( {lines.map((line) => (
<div key={line.id} className={LEVEL[line.level] || 'text-foreground-muted'}> <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) { function nodeCoords(col: number, row: number, rows: number) {
const colW = 100 / 5 const colCount = 5
const yPad = 8 const gap = 3.2
const ySpan = 84 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 const y = yPad + ((row + 0.5) / rows) * ySpan
return { return {
inX: col * colW + colW * 0.08, inX: xCenter - nodeHalf,
outX: col * colW + colW * 0.92, outX: xCenter + nodeHalf,
y, y,
} }
} }
@@ -279,7 +283,7 @@ export function PlatformTopology({ workload, animations, selectedNodeId, onNodeC
return ( return (
<section className="panel flex h-full min-h-0 flex-1 flex-col overflow-hidden"> <section className="panel flex h-full min-h-0 flex-1 flex-col overflow-hidden">
<header <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)' }} style={{ background: 'var(--topo-header-bg)' }}
> >
<div className="flex items-center justify-between gap-2"> <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"> <div className="min-w-0">
<h2 className="truncate text-xs font-semibold text-foreground">Data Platform Topology</h2> <h2 className="truncate text-xs font-semibold text-foreground">Data Platform Topology</h2>
<p className="truncate text-[9px] text-foreground-muted"> <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> </p>
</div> </div>
</div> </div>
@@ -361,7 +365,7 @@ export function PlatformTopology({ workload, animations, selectedNodeId, onNodeC
})} })}
</svg> </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) => ( {STAGES.map((stage) => (
<div key={stage.id} className={cn('topo-stage-col', stage.accent)}> <div key={stage.id} className={cn('topo-stage-col', stage.accent)}>
<header className="mb-1 shrink-0 border-b border-white/10 pb-1"> <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 sql?: string
} }
type Engine = 'postgres' | 'mysql' | 'mongodb' | 'trino'
type Props = { 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: { postgres: {
title: 'PostgreSQL SQL Console', title: 'PostgreSQL SQL Console',
sub: 'DB Vault · 10.0.21.51:5432 · user mo', sub: 'DB Vault · 10.0.21.51:5432 · user mo',
accent: 'text-blue-400', 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: { trino: {
title: 'Trino SQL Console', 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', accent: 'text-violet-400',
queryLabel: 'SQL',
}, },
} }
export function SqlWorkbench({ engine }: Props) { export function SqlWorkbench({ engine, compact }: Props) {
const meta = ENGINE_META[engine] const meta = ENGINE_META[engine]
const [samples, setSamples] = useState<Sample[]>([]) const [samples, setSamples] = useState<Sample[]>([])
const [sql, setSql] = useState('SELECT version();') const [sql, setSql] = useState('SELECT version();')
const [result, setResult] = useState<SqlResult | null>(null) const [result, setResult] = useState<SqlResult | null>(null)
const [benchmark, setBenchmark] = useState<{ const [benchmark, setBenchmark] = useState<{
comparison?: { postgres_ms?: number; trino_ms?: number; faster?: string; speedup_factor?: number } comparison?: { postgres_ms?: number; trino_ms?: number; faster?: string; speedup_factor?: number }
postgres?: SqlResult
trino?: SqlResult
} | null>(null) } | null>(null)
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [benchLoading, setBenchLoading] = useState(false) const [benchLoading, setBenchLoading] = useState(false)
@@ -106,40 +121,41 @@ export function SqlWorkbench({ engine }: Props) {
} }
return ( return (
<div className="flex h-full min-h-0 flex-col bg-[#0a0e14] text-foreground"> <div className="flex h-full min-h-0 flex-col text-foreground">
<header className="flex shrink-0 flex-wrap items-center justify-between gap-2 border-b border-border/60 px-3 py-2"> <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> <div>
<h3 className={cn('flex items-center gap-2 text-sm font-semibold', meta.accent)}> <h3 className={cn('flex items-center gap-1.5 font-semibold', meta.accent, compact ? 'text-xs' : 'text-sm')}>
<Database className="h-4 w-4" /> <Database className={compact ? 'h-3 w-3' : 'h-4 w-4'} />
{meta.title} {meta.title}
</h3> </h3>
<p className="text-[10px] text-foreground-muted">{meta.sub}</p> <p className="text-[9px] text-foreground-muted">{meta.sub}</p>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-1.5">
<button type="button" onClick={() => run()} disabled={loading} className={cn('inline-flex items-center gap-1 rounded px-3 py-1.5 text-[11px]', subTabActive)}> <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" />} {loading ? <Loader2 className="h-3 w-3 animate-spin" /> : <Play className="h-3 w-3" />}
Run Run
</button> </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" />} {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> </button>
)}
</div> </div>
</header> </header>
<div className="flex min-h-0 flex-1 flex-col lg:flex-row"> <div className="flex min-h-0 flex-1">
<aside className="shrink-0 border-b border-border/60 p-2 lg:w-56 lg:border-b-0 lg:border-r"> <aside className={cn('shrink-0 border-r border-border/60 p-1.5', compact ? 'w-44' : 'w-52')}>
<p className="mb-1 text-[9px] font-semibold uppercase tracking-wider text-foreground-faint">10 demo commands</p> <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-32 space-y-0.5 overflow-y-auto lg:max-h-none"> <div className="scrollbar-thin max-h-full space-y-0.5 overflow-y-auto">
{samples.map((s) => ( {samples.map((s) => (
<button <button
key={s.id} key={s.id}
type="button" type="button"
onClick={() => { setSql(s.sql); run(s.sql) }} 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="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> </button>
))} ))}
</div> </div>
@@ -149,42 +165,45 @@ export function SqlWorkbench({ engine }: Props) {
<textarea <textarea
value={sql} value={sql}
onChange={(e) => setSql(e.target.value)} 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} 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 && ( {benchmark?.comparison && (
<div className="shrink-0 border-b border-amber-500/30 bg-amber-500/10 px-3 py-2 text-[11px]"> <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 (5M parallel compute): </span> <span className="font-semibold text-amber-300">Benchmark: </span>
PostgreSQL <span className="font-mono">{benchmark.comparison.postgres_ms}ms</span> PG <span className="font-mono">{benchmark.comparison.postgres_ms}ms</span>
{' · '} {' · '}Trino <span className="font-mono">{benchmark.comparison.trino_ms}ms</span>
Trino <span className="font-mono">{benchmark.comparison.trino_ms}ms</span>
{' — '} {' — '}
<span className="font-semibold text-success"> <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> </span>
</div> </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 && ( {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 {result.row_count} rows · {result.elapsed_ms}ms
</p> </p>
<table className="w-full text-left font-mono text-[10px]"> <table className="w-full text-left font-mono text-[9px]">
<thead> <thead>
<tr className="border-b border-border text-docker"> <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> </tr>
</thead> </thead>
<tbody> <tbody>
{result.rows?.map((row, i) => ( {result.rows?.map((row, i) => (
<tr key={i} className="border-b border-border/30 hover:bg-white/5"> <tr key={i} className="border-b border-border/30 hover:bg-white/5">
{row.map((cell, j) => ( {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)} {cell === null ? 'NULL' : String(cell)}
</td> </td>
))} ))}
@@ -195,7 +214,7 @@ export function SqlWorkbench({ engine }: Props) {
</> </>
)} )}
{!result && !loading && ( {!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>
</div> </div>
@@ -3,7 +3,7 @@ import { AgentWorkbench } from './AgentWorkbench'
import { SqlWorkbench } from './SqlWorkbench' import { SqlWorkbench } from './SqlWorkbench'
type Props = { type Props = {
mode: 'agent' | 'sql-postgres' | 'sql-trino' | null mode: 'agent' | 'sql-postgres' | 'sql-mysql' | 'sql-mongodb' | 'sql-trino' | null
agent: Agent | null agent: Agent | null
lines: TerminalLine[] lines: TerminalLine[]
busy: boolean busy: boolean
@@ -14,12 +14,14 @@ export function WorkbenchPanel({ mode, agent, lines, busy, onSendPrompt }: Props
if (!mode) return null if (!mode) return null
return ( 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 && ( {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-postgres' && <SqlWorkbench engine="postgres" compact />}
{mode === 'sql-trino' && <SqlWorkbench engine="trino" />} {mode === 'sql-mysql' && <SqlWorkbench engine="mysql" compact />}
{mode === 'sql-mongodb' && <SqlWorkbench engine="mongodb" compact />}
{mode === 'sql-trino' && <SqlWorkbench engine="trino" compact />}
</section> </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', postgresql: 'postgres',
'src-postgres': 'postgres', 'src-postgres': 'postgres',
mysql: 'mysql',
'src-mysql': 'mysql',
mongodb: 'mongodb',
'src-mongo': 'mongodb',
trino: 'trino', trino: 'trino',
'query-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 return SQL_NODE_ENGINES[nodeId] || SQL_NODE_ENGINES[resolveProbeId(nodeId)] || null
} }
@@ -66,7 +70,7 @@ export function useCommandCenter() {
const [nodeBusy, setNodeBusy] = useState(false) const [nodeBusy, setNodeBusy] = useState(false)
const [mainView, setMainView] = useState<'platform' | 'approvals' | 'presentation' | 'dataquality' | 'knowledge' | 'storage' | 'search'>('platform') const [mainView, setMainView] = useState<'platform' | 'approvals' | 'presentation' | 'dataquality' | 'knowledge' | 'storage' | 'search'>('platform')
const [approvalHighlight, setApprovalHighlight] = useState(false) 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 [chatExpanded, setChatExpanded] = useState(false)
const promptTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) const promptTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
const [terminalExpanded, setTerminalExpanded] = useState(true) const [terminalExpanded, setTerminalExpanded] = useState(true)
@@ -225,7 +229,7 @@ export function useCommandCenter() {
const probeId = resolveProbeId(nodeId) const probeId = resolveProbeId(nodeId)
const sqlEng = resolveSqlEngine(nodeId) const sqlEng = resolveSqlEngine(nodeId)
if (sqlEng) { if (sqlEng) {
setWorkbenchMode(sqlEng === 'postgres' ? 'sql-postgres' : 'sql-trino') setWorkbenchMode(`sql-${sqlEng}` as 'sql-postgres' | 'sql-mysql' | 'sql-mongodb' | 'sql-trino')
setSelectedAgentId(null) setSelectedAgentId(null)
} else if (options?.keepWorkbench) { } else if (options?.keepWorkbench) {
setWorkbenchMode(options.keepWorkbench) setWorkbenchMode(options.keepWorkbench)
+5 -5
View File
@@ -87,14 +87,14 @@
} }
.topo-edge-idle { .topo-edge-idle {
stroke: rgba(100, 140, 180, 0.35); stroke: rgba(100, 140, 180, 0.45);
stroke-width: 1.5; stroke-width: 2;
stroke-dasharray: 4 8; stroke-dasharray: 4 8;
fill: none; fill: none;
} }
.topo-edge-live { .topo-edge-live {
stroke-width: 2; stroke-width: 2.5;
stroke-dasharray: 8 12; stroke-dasharray: 8 12;
fill: none; fill: none;
animation: flow-dash 1.2s linear infinite; animation: flow-dash 1.2s linear infinite;
@@ -130,7 +130,7 @@
} }
.topo-node { .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); background: var(--topo-node-bg);
border-color: var(--topo-node-border); border-color: var(--topo-node-border);
box-shadow: var(--topo-node-shadow); box-shadow: var(--topo-node-shadow);
@@ -146,7 +146,7 @@
} }
.topo-stage-col { .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); border-right: 1px dashed var(--topo-stage-border);
} }