Files
atc-agents/ui/src/components/features/TrinoFederationView.tsx
T
mo 437574f0bb feat: federated query spans all 5 databases (not just 3)
The marquee panel only joined 3 region-keyed sources. Add a
"one SQL across every database" reach matrix that fans a single
Trino query out to PostgreSQL, MySQL, MongoDB, Cassandra and the
Hadoop/Iceberg lake in one UNION ALL (telemetry has no region, so
a per-source summary is used instead of a misleading join).

- New MATRIX_SQL + concurrent execution alongside the region
  scorecard so total latency stays ~ the slower query
- Federated tab shows the 5-source matrix (records + headline
  metric per engine) above the relabelled 3-source region scorecard
2026-06-28 18:51:55 +00:00

442 lines
23 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useEffect, useState } from 'react'
import {
Network,
Database,
Boxes,
RefreshCw,
Loader2,
DollarSign,
ShoppingCart,
Users,
Activity,
Layers,
ShieldCheck,
ShieldAlert,
Server,
HardDrive,
Play,
} from 'lucide-react'
import { cn } from '../../lib/utils'
type Bucket = { key: string; count: number; value?: number }
export type SubTab = 'federated' | 'lake' | 'dictionary'
const COLORS = ['#34d399', '#60a5fa', '#f472b6', '#fbbf24', '#a78bfa', '#22d3ee', '#fb7185']
function fmtNum(n?: number | string | null) {
if (n == null) return '—'
const v = typeof n === 'number' ? n : Number(n)
if (Number.isNaN(v)) return String(n)
if (Math.abs(v) >= 1e9) return `${(v / 1e9).toFixed(2)}B`
if (Math.abs(v) >= 1e6) return `${(v / 1e6).toFixed(1)}M`
if (Math.abs(v) >= 1e3) return `${(v / 1e3).toFixed(1)}K`
return String(v)
}
const fmtMoney = (n?: number | string | null) => (n == null ? '—' : `€${fmtNum(n)}`)
function Panel({ title, subtitle, icon: Icon, children }: { title: string; subtitle?: string; icon?: typeof Database; children: React.ReactNode }) {
return (
<div className="panel flex min-h-0 flex-col p-3">
<div className="mb-2 flex items-center gap-1.5">
{Icon && <Icon className="h-3.5 w-3.5 text-docker" />}
<h3 className="text-[11px] font-semibold text-foreground">{title}</h3>
{subtitle && <span className="ml-auto text-[9px] text-foreground-faint">{subtitle}</span>}
</div>
{children}
</div>
)
}
function BarsH({ data, valueKind, colorByIndex }: { data?: Bucket[]; valueKind?: 'money' | 'num'; colorByIndex?: boolean }) {
const d = data || []
const useVal = valueKind != null
const max = Math.max(1, ...d.map((x) => (useVal && x.value != null ? x.value : x.count)))
if (!d.length) return <p className="py-5 text-center text-[10px] text-foreground-faint">No data</p>
return (
<div className="space-y-1.5">
{d.map((x, i) => {
const metric = useVal && x.value != null ? x.value : x.count
const pct = Math.max(2, (metric / max) * 100)
const label = useVal && x.value != null ? (valueKind === 'money' ? fmtMoney(x.value) : fmtNum(x.value)) : fmtNum(x.count)
return (
<div key={x.key ?? i} className="flex items-center gap-2 text-[10px]">
<span className="w-28 shrink-0 truncate text-foreground-muted" title={x.key}>{x.key ?? '—'}</span>
<div className="relative h-3.5 flex-1 overflow-hidden rounded bg-surface-overlay">
<div className="h-full rounded" style={{ width: `${pct}%`, backgroundColor: colorByIndex ? COLORS[i % COLORS.length] : '#38bdf8' }} />
</div>
<span className="w-20 shrink-0 text-right font-mono text-foreground">{label}</span>
</div>
)
})}
</div>
)
}
function Donut({ data }: { data?: Bucket[] }) {
const d = data || []
const total = d.reduce((s, x) => s + x.count, 0) || 1
let acc = 0
const r = 42
const c = 2 * Math.PI * r
if (!d.length) return <p className="py-5 text-center text-[10px] text-foreground-faint">No data</p>
return (
<div className="flex items-center gap-4">
<svg viewBox="0 0 100 100" className="h-24 w-24 shrink-0 -rotate-90">
{d.map((x, i) => {
const dash = (x.count / total) * c
const seg = <circle key={x.key ?? i} cx="50" cy="50" r={r} fill="none" stroke={COLORS[i % COLORS.length]} strokeWidth="14" strokeDasharray={`${dash} ${c - dash}`} strokeDashoffset={-acc} />
acc += dash
return seg
})}
</svg>
<div className="min-w-0 flex-1 space-y-1">
{d.slice(0, 7).map((x, i) => (
<div key={x.key ?? i} className="flex items-center gap-1.5 text-[10px]">
<span className="h-2 w-2 shrink-0 rounded-full" style={{ backgroundColor: COLORS[i % COLORS.length] }} />
<span className="flex-1 truncate text-foreground-muted">{x.key ?? '—'}</span>
<span className="font-mono text-foreground">{((x.count / total) * 100).toFixed(0)}%</span>
</div>
))}
</div>
</div>
)
}
function Kpi({ icon: Icon, label, value, sub, accent }: { icon: typeof Users; label: string; value: string; sub?: string; accent: string }) {
return (
<div className="panel flex items-center gap-3 px-3 py-2.5">
<div className="flex h-9 w-9 shrink-0 items-center justify-center rounded-lg" style={{ backgroundColor: `${accent}1f`, color: accent }}>
<Icon className="h-4 w-4" />
</div>
<div className="min-w-0">
<p className="text-[9px] font-semibold uppercase tracking-wider text-foreground-muted">{label}</p>
<p className="truncate text-lg font-bold leading-tight text-foreground">{value}</p>
{sub && <p className="truncate text-[9px] text-foreground-faint">{sub}</p>}
</div>
</div>
)
}
export function TrinoFederationView({ embedded = false, activeTab }: { embedded?: boolean; activeTab?: SubTab } = {}) {
const [tabState, setTab] = useState<SubTab>('federated')
const tab = embedded ? activeTab ?? 'federated' : tabState
const [catalogs, setCatalogs] = useState<any>(null)
const [marquee, setMarquee] = useState<any>(null)
const [lake, setLake] = useState<any>(null)
const [dict, setDict] = useState<any>(null)
const [loading, setLoading] = useState(false)
const [matRunning, setMatRunning] = useState(false)
const loadFederated = useCallback(async () => {
setLoading(true)
try {
const [c, m] = await Promise.all([
fetch('/api/federated/catalogs').then((r) => (r.ok ? r.json() : null)),
fetch('/api/federated/marquee').then((r) => (r.ok ? r.json() : null)),
])
setCatalogs(c)
setMarquee(m)
} catch { /* */ } finally { setLoading(false) }
}, [])
const loadLake = useCallback(async () => {
setLoading(true)
try {
const r = await fetch('/api/federated/lake')
if (r.ok) setLake(await r.json())
const ms = await fetch('/api/federated/materialize/status').then((x) => (x.ok ? x.json() : null))
setMatRunning(!!ms?.running)
} catch { /* */ } finally { setLoading(false) }
}, [])
const loadDict = useCallback(async () => {
setLoading(true)
try {
const r = await fetch('/api/federated/dictionary')
if (r.ok) setDict(await r.json())
} catch { /* */ } finally { setLoading(false) }
}, [])
useEffect(() => {
if (tab === 'federated') loadFederated()
else if (tab === 'lake') loadLake()
else loadDict()
}, [tab, loadFederated, loadLake, loadDict])
// poll marquee while it is computing
useEffect(() => {
if (tab !== 'federated' || !marquee?.running) return
const t = setTimeout(loadFederated, 5000)
return () => clearTimeout(t)
}, [tab, marquee, loadFederated])
// poll materialize while running
useEffect(() => {
if (tab !== 'lake' || !matRunning) return
const t = setTimeout(loadLake, 6000)
return () => clearTimeout(t)
}, [tab, matRunning, loadLake])
const rebuildLake = async () => {
await fetch('/api/federated/materialize', { method: 'POST' })
setMatRunning(true)
setTimeout(loadLake, 2000)
}
const refreshMarquee = async () => {
await fetch('/api/federated/marquee/refresh', { method: 'POST' })
setTimeout(loadFederated, 1500)
}
const m = marquee?.marquee
const totals = catalogs?.source_totals || {}
const mxRows: any[] = m?.matrix?.rows || []
const mxMax = Math.max(1, ...mxRows.map((r) => Number(r.records) || 0))
const mxCatalogs: string[] = m?.matrix?.catalogs || ['postgres_sales', 'mysql_hr', 'mongodb_supplychain', 'cassandra_telemetry', 'iceberg']
const body = (
<>
{/* ───────── FEDERATED ───────── */}
{tab === 'federated' && (
<>
<div className="grid shrink-0 grid-cols-2 gap-2 md:grid-cols-4">
<Kpi icon={Layers} label="Federated catalogs" value={String(catalogs?.count ?? '—')} sub="one Trino engine" accent="#dd00a1" />
<Kpi icon={ShoppingCart} label="Orders" value={fmtNum(totals.orders)} sub="PostgreSQL (live)" accent="#fbbf24" />
<Kpi icon={Users} label="HR events" value={fmtNum(totals.hr_events)} sub="MySQL (live)" accent="#60a5fa" />
<Kpi icon={Boxes} label="Supply events" value={fmtNum(totals.supply_events)} sub="MongoDB (live)" accent="#a78bfa" />
</div>
<Panel title="Federated catalog landscape" subtitle={`${catalogs?.count ?? 0} catalogs`} icon={Database}>
<div className="flex flex-wrap gap-2">
{(catalogs?.catalogs || []).map((c: any) => (
<div key={c.catalog} className="flex min-w-[150px] flex-col gap-0.5 rounded-lg border px-3 py-2" style={{ borderColor: `${c.color}55`, backgroundColor: `${c.color}12` }}>
<span className="flex items-center gap-1.5 text-[11px] font-semibold text-foreground">
<Server className="h-3 w-3" style={{ color: c.color }} /> {c.label}
</span>
<span className="font-mono text-[9px] text-foreground-muted">{c.catalog}</span>
<span className="text-[9px] text-foreground-faint">{c.desc}</span>
{c.rows != null && <span className="mt-0.5 font-mono text-[10px] text-foreground">{fmtNum(c.rows)} rows</span>}
</div>
))}
</div>
</Panel>
<Panel
title="One SQL across every database"
subtitle={m?.matrix?.elapsed_ms != null ? `${mxCatalogs.length} databases · ${(m.matrix.elapsed_ms / 1000).toFixed(1)}s` : marquee?.running ? 'computing…' : `${mxCatalogs.length} databases`}
icon={Network}
>
<p className="mb-2 text-[10px] text-foreground-muted">
A single Trino query fanned out to <span className="text-docker">five engines at once</span> relational, document, wide-column and the Hadoop lakehouse no copies, no ETL.
</p>
<div className="mb-2 flex flex-wrap items-center gap-1.5">
{mxCatalogs.map((c) => (
<span key={c} className="rounded-full bg-docker/15 px-2 py-0.5 text-[9px] font-medium text-docker">{c}</span>
))}
<button type="button" onClick={refreshMarquee} className="ml-auto inline-flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[9px] text-foreground-muted hover:bg-surface-overlay">
{marquee?.running ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />} re-run
</button>
</div>
<pre className="mb-2 overflow-x-auto rounded-md border border-border bg-surface p-2 font-mono text-[9px] leading-relaxed text-foreground-muted">{m?.matrix?.sql}</pre>
{mxRows.length ? (
<div className="overflow-x-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="border-b border-border text-left text-foreground-muted">
<th className="py-1 pr-3">Source</th>
<th className="py-1 pr-3">Catalog · dataset</th>
<th className="py-1 pr-3">Records</th>
<th className="py-1 pr-3 text-right">Headline metric</th>
</tr>
</thead>
<tbody>
{mxRows.map((r, i) => {
const isMoney = /revenue|value/i.test(r.metric_label || '')
const pct = Math.max(3, (Number(r.records) / mxMax) * 100)
const color = COLORS[i % COLORS.length]
return (
<tr key={i} className="border-b border-border/40">
<td className="py-1 pr-3 font-medium text-foreground">
<span className="inline-flex items-center gap-1.5"><span className="h-2 w-2 rounded-full" style={{ backgroundColor: color }} />{r.source}</span>
</td>
<td className="py-1 pr-3 font-mono text-foreground-muted">{r.catalog}<span className="text-foreground-faint"> · {r.dataset}</span></td>
<td className="py-1 pr-3">
<div className="flex items-center gap-2">
<div className="relative h-2.5 w-24 overflow-hidden rounded bg-surface-overlay"><div className="h-full rounded" style={{ width: `${pct}%`, backgroundColor: color }} /></div>
<span className="font-mono text-foreground">{fmtNum(r.records)}</span>
</div>
</td>
<td className="py-1 pr-3 text-right font-mono text-emerald-400">{isMoney ? fmtMoney(r.metric) : fmtNum(r.metric)} <span className="text-foreground-faint">{r.metric_label}</span></td>
</tr>
)
})}
</tbody>
</table>
</div>
) : (
<p className="py-4 text-center text-[10px] text-foreground-faint">{marquee?.running ? <><Loader2 className="mr-1 inline h-3 w-3 animate-spin" /> Federating across all databases</> : 'No result yet — click re-run.'}</p>
)}
</Panel>
<Panel
title="Region scorecard — live join across 3 OLTP sources"
subtitle={m?.elapsed_ms != null ? `${(m.elapsed_ms / 1000).toFixed(1)}s` : marquee?.running ? 'computing…' : ''}
icon={Network}
>
<div className="mb-2 flex flex-wrap items-center gap-1.5">
{(m?.catalogs || ['postgres_sales', 'mysql_hr', 'mongodb_supplychain']).map((c: string) => (
<span key={c} className="rounded-full bg-docker/15 px-2 py-0.5 text-[9px] font-medium text-docker">{c}</span>
))}
<button type="button" onClick={refreshMarquee} className="ml-auto inline-flex items-center gap-1 rounded border border-border px-2 py-0.5 text-[9px] text-foreground-muted hover:bg-surface-overlay">
{marquee?.running ? <Loader2 className="h-3 w-3 animate-spin" /> : <RefreshCw className="h-3 w-3" />} re-run
</button>
</div>
<pre className="mb-2 overflow-x-auto rounded-md border border-border bg-surface p-2 font-mono text-[9px] leading-relaxed text-foreground-muted">{marquee?.sql || m?.sql}</pre>
{marquee?.running && !m?.rows?.length ? (
<p className="py-4 text-center text-[10px] text-foreground-muted"><Loader2 className="mr-1 inline h-3 w-3 animate-spin" /> Federating across live sources (~12 min, cached afterwards)</p>
) : m?.rows?.length ? (
<div className="overflow-x-auto">
<table className="w-full text-[10px]">
<thead>
<tr className="border-b border-border text-left text-foreground-muted">
<th className="py-1 pr-3">Region</th>
<th className="py-1 pr-3 text-right">Orders</th>
<th className="py-1 pr-3 text-right">Revenue</th>
<th className="py-1 pr-3 text-right">HR events</th>
<th className="py-1 pr-3 text-right">Supply events</th>
</tr>
</thead>
<tbody>
{m.rows.map((r: any, i: number) => (
<tr key={i} className="border-b border-border/40">
<td className="py-1 pr-3 font-medium text-foreground">{r.region}</td>
<td className="py-1 pr-3 text-right font-mono text-foreground">{fmtNum(r.orders)}</td>
<td className="py-1 pr-3 text-right font-mono text-emerald-400">{fmtMoney(r.revenue)}</td>
<td className="py-1 pr-3 text-right font-mono text-foreground">{fmtNum(r.hr_events)}</td>
<td className="py-1 pr-3 text-right font-mono text-foreground">{fmtNum(r.supply_events)}</td>
</tr>
))}
</tbody>
</table>
{m.generated_at && <p className="mt-1 text-[9px] text-foreground-faint">as of {new Date(m.generated_at).toLocaleString()} · joined live across PostgreSQL + MySQL + MongoDB</p>}
</div>
) : (
<p className="py-4 text-center text-[10px] text-foreground-faint">{m?.error || 'No result yet — click re-run.'}</p>
)}
</Panel>
</>
)}
{/* ───────── HADOOP LAKE ───────── */}
{tab === 'lake' && (
<>
<div className="flex shrink-0 flex-wrap items-center justify-between gap-2 px-1">
<p className="text-[11px] text-foreground-muted">
All federated business data materialized as <span className="text-docker">external Iceberg tables on HDFS</span> queried live (fast).
</p>
<button type="button" onClick={rebuildLake} disabled={matRunning}
className="inline-flex items-center gap-1.5 rounded-md border border-emerald-500/40 bg-emerald-500/10 px-3 py-1.5 text-[11px] font-medium text-emerald-400 hover:bg-emerald-500/20 disabled:opacity-60">
{matRunning ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <Play className="h-3.5 w-3.5" />}
{matRunning ? 'Materializing…' : 'Rebuild external tables'}
</button>
</div>
<Panel title="Hadoop external tables (iceberg.hadoop.*)" subtitle={`${fmtNum(lake?.total_rows)} rows total`} icon={HardDrive}>
<div className="grid grid-cols-2 gap-2 md:grid-cols-3 lg:grid-cols-4">
{(lake?.tables || []).map((t: any) => (
<div key={t.table} className="rounded-lg border border-border/60 bg-surface-overlay/40 px-3 py-2">
<p className="flex items-center gap-1 truncate font-mono text-[10px] text-foreground"><Boxes className="h-3 w-3 text-docker" />{t.table}</p>
<p className="font-mono text-[13px] font-bold text-foreground">{fmtNum(t.rows)}</p>
</div>
))}
{!lake?.tables?.length && <p className="col-span-full py-4 text-center text-[10px] text-foreground-faint">No external tables yet click Rebuild external tables.</p>}
</div>
</Panel>
<div className="grid shrink-0 gap-2 lg:grid-cols-2">
<Panel title="Revenue by region" icon={DollarSign}><BarsH data={lake?.orders?.by_region} valueKind="money" colorByIndex /></Panel>
<Panel title="Top customers by spend" icon={Users}><BarsH data={lake?.orders?.top_customers} valueKind="money" /></Panel>
<div className="grid gap-2 sm:grid-cols-2">
<Panel title="Orders by status"><Donut data={lake?.orders?.by_status} /></Panel>
<Panel title="Revenue by channel"><BarsH data={lake?.orders?.by_channel} valueKind="money" colorByIndex /></Panel>
</div>
<Panel title="Employees by department" icon={Users}><BarsH data={lake?.hr?.by_department} colorByIndex /></Panel>
<Panel title="Supply events by type" icon={Boxes}><BarsH data={lake?.supply?.by_type} colorByIndex /></Panel>
<Panel title="Telemetry — avg value by metric" icon={Activity}><BarsH data={lake?.telemetry?.by_metric} valueKind="num" /></Panel>
</div>
</>
)}
{/* ───────── DICTIONARY ───────── */}
{tab === 'dictionary' && (
<>
<div className="grid shrink-0 grid-cols-2 gap-2 md:grid-cols-4">
<Kpi icon={Layers} label="Tables" value={String(dict?.summary?.tables ?? '—')} accent="#5b8def" />
<Kpi icon={Database} label="Columns" value={String(dict?.summary?.columns ?? '—')} accent="#34d399" />
<Kpi icon={ShieldAlert} label="PII columns" value={String(dict?.summary?.pii_columns ?? '—')} accent="#fbbf24" />
<Kpi icon={ShieldCheck} label="Masked" value={String(dict?.summary?.masked_columns ?? '—')} sub="hidden from LLM" accent="#f472b6" />
</div>
<p className="px-1 text-[10px] text-foreground-muted">
This is exactly what the assistant knows about your data every column, its type, and whether it is <span className="text-amber-400">masked</span> or visible.
</p>
<div className="grid gap-2 lg:grid-cols-2">
{(dict?.tables || []).map((t: any) => (
<Panel key={t.fqn} title={t.fqn} subtitle={`${t.masked_count}/${t.pii_count} PII masked`} icon={Database}>
<p className="mb-1.5 text-[9px] text-foreground-faint">{t.engine} · {t.desc}</p>
<div className="flex flex-wrap gap-1">
{t.columns.map((c: any) => (
<span key={c.name}
className={cn('inline-flex items-center gap-1 rounded border px-1.5 py-0.5 font-mono text-[9px]',
c.masked ? 'border-amber-500/40 bg-amber-500/10 text-amber-300'
: c.pii ? 'border-rose-500/40 bg-rose-500/10 text-rose-300'
: 'border-border/60 text-foreground-muted')}
title={`${c.type}${c.category ? ` · ${c.category}` : ''}${c.masked ? ' · MASKED' : c.pii ? ' · PII visible' : ''}`}>
{c.masked && <ShieldCheck className="h-2.5 w-2.5" />}
{!c.masked && c.pii && <ShieldAlert className="h-2.5 w-2.5" />}
{c.name}
</span>
))}
</div>
</Panel>
))}
</div>
</>
)}
{loading && !catalogs && !lake && !dict && (
<div className="flex flex-1 items-center justify-center text-foreground-muted"><Loader2 className="h-5 w-5 animate-spin" /></div>
)}
</>
)
if (embedded) return body
return (
<div className="scrollbar-thin flex h-full min-h-0 flex-col gap-2 overflow-y-auto p-3">
<header className="panel flex shrink-0 flex-wrap items-center justify-between gap-3 px-4 py-3">
<div>
<h1 className="flex items-center gap-2 text-base font-semibold text-foreground">
<Network className="h-5 w-5 text-docker" />
Trino Federation &amp; Hadoop Lakehouse
</h1>
<p className="text-[11px] text-foreground-muted">
One SQL engine over every source federated business analytics, mirrored into Hadoop as external Iceberg tables
</p>
</div>
<div className="flex flex-wrap gap-1">
{([
{ id: 'federated', label: 'Federated', icon: Network },
{ id: 'lake', label: 'Hadoop Lake', icon: HardDrive },
{ id: 'dictionary', label: 'Data Dictionary', icon: ShieldCheck },
] as { id: SubTab; label: string; icon: typeof Network }[]).map(({ id, label, icon: Icon }) => (
<button key={id} type="button" onClick={() => setTab(id)}
className={cn('inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[11px] font-medium transition-colors', tab === id ? 'bg-docker/15 text-docker' : 'text-foreground-muted hover:bg-surface-overlay')}>
<Icon className="h-3.5 w-3.5" /> {label}
</button>
))}
</div>
</header>
{body}
</div>
)
}