feat: fold Trino federation into Data Explorer tabs; tidy sidebar

- Data Explorer now hosts Business Overview, Federated (Trino),
  Hadoop Lake and Data Dictionary as tabs instead of a separate
  top-level view (richer, less stacked layout)
- TrinoFederationView supports an embedded mode driven by an
  external active tab
- Remove the standalone Trino Federation sidebar entry/route
- Remove the Kibana shortcut from the sidebar (it already lives in
  the Elasticsearch view)
This commit is contained in:
mo
2026-06-28 18:33:16 +00:00
parent ea3e59cf9c
commit 28bdbaf13a
5 changed files with 103 additions and 79 deletions
+62 -22
View File
@@ -11,9 +11,22 @@ import {
Search,
Loader2,
TrendingUp,
ExternalLink,
BarChart3,
Network,
HardDrive,
ShieldCheck,
} from 'lucide-react'
import { cn } from '../../lib/utils'
import { TrinoFederationView, type SubTab } from './TrinoFederationView'
type ExplorerTab = 'business' | SubTab
const TABS: { id: ExplorerTab; label: string; icon: typeof Users; hint: string }[] = [
{ id: 'business', label: 'Business Overview', icon: BarChart3, hint: 'Customers, orders, workforce, supply chain & telemetry across every source' },
{ id: 'federated', label: 'Federated (Trino)', icon: Network, hint: 'One SQL engine joining PostgreSQL, MySQL & MongoDB live — region scorecard' },
{ id: 'lake', label: 'Hadoop Lake', icon: HardDrive, hint: 'All business data mirrored as external Iceberg tables on HDFS' },
{ id: 'dictionary', label: 'Data Dictionary', icon: ShieldCheck, hint: 'Every table & column with PII / masking status — exactly what the assistant sees' },
]
type Bucket = { key: string; count: number; value?: number }
type Business = {
@@ -195,6 +208,7 @@ export function DataExplorerView() {
const [region, setRegion] = useState('')
const [qInput, setQInput] = useState('')
const [q, setQ] = useState('')
const [view, setView] = useState<ExplorerTab>('business')
const load = useCallback(async () => {
setLoading(true)
@@ -213,39 +227,63 @@ export function DataExplorerView() {
const k = data?.kpis
const regions = useMemo(() => data?.regions || [], [data])
const activeTab = TABS.find((t) => t.id === view) || TABS[0]
return (
<div className="scrollbar-thin flex h-full min-h-0 flex-col gap-2 overflow-y-auto p-3">
{/* header */}
<header className="panel flex shrink-0 flex-wrap items-center justify-between gap-3 px-4 py-3">
<div>
<div className="min-w-0">
<h1 className="flex items-center gap-2 text-base font-semibold text-foreground">
<TrendingUp className="h-5 w-5 text-docker" />
Data Explorer
</h1>
<p className="text-[11px] text-foreground-muted">
Your actual business data customers, orders, employees, supply chain &amp; telemetry across all sources
</p>
</div>
<div className="flex items-center gap-2">
<form
onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()) }}
className="flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1"
>
<Search className="h-3.5 w-3.5 text-foreground-muted" />
<input
value={qInput}
onChange={(e) => setQInput(e.target.value)}
placeholder="Filter by name, id, region…"
className="w-48 bg-transparent text-[11px] text-foreground outline-none placeholder:text-foreground-faint"
/>
</form>
<button type="button" onClick={load} className="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-[11px] text-foreground-muted hover:bg-surface-overlay">
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />} Refresh
</button>
<p className="text-[11px] text-foreground-muted">{activeTab.hint}</p>
</div>
{view === 'business' && (
<div className="flex items-center gap-2">
<form
onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()) }}
className="flex items-center gap-1 rounded-md border border-border bg-surface px-2 py-1"
>
<Search className="h-3.5 w-3.5 text-foreground-muted" />
<input
value={qInput}
onChange={(e) => setQInput(e.target.value)}
placeholder="Filter by name, id, region…"
className="w-48 bg-transparent text-[11px] text-foreground outline-none placeholder:text-foreground-faint"
/>
</form>
<button type="button" onClick={load} className="inline-flex items-center gap-1.5 rounded-md border border-border px-3 py-1.5 text-[11px] text-foreground-muted hover:bg-surface-overlay">
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />} Refresh
</button>
</div>
)}
</header>
{/* tab bar */}
<div className="flex shrink-0 flex-wrap gap-1 px-1">
{TABS.map(({ id, label, icon: Icon }) => (
<button
key={id}
type="button"
onClick={() => setView(id)}
className={cn(
'inline-flex items-center gap-1.5 rounded-md px-3 py-1.5 text-[11px] font-medium transition-colors',
view === id ? 'bg-docker/15 text-docker' : 'text-foreground-muted hover:bg-surface-overlay',
)}
>
<Icon className="h-3.5 w-3.5" /> {label}
</button>
))}
</div>
{/* Trino federation / lake / dictionary tabs */}
{view !== 'business' && <TrinoFederationView embedded activeTab={view} />}
{/* ───────── BUSINESS OVERVIEW ───────── */}
{view === 'business' && (
<>
{/* region filters */}
<div className="flex shrink-0 flex-wrap items-center gap-1.5 px-1 text-[10px]">
<span className="mr-1 font-semibold uppercase tracking-wider text-foreground-faint">Region</span>
@@ -318,6 +356,8 @@ export function DataExplorerView() {
<Panel title="Supply events by region"><Donut data={data?.supply.by_region || []} /></Panel>
<Panel title="Telemetry — avg value by metric" icon={Activity}><BarsH data={data?.telemetry.by_metric || []} valueKind="num" /></Panel>
</div>
</>
)}
</div>
)
}
@@ -19,7 +19,7 @@ import {
import { cn } from '../../lib/utils'
type Bucket = { key: string; count: number; value?: number }
type SubTab = 'federated' | 'lake' | 'dictionary'
export type SubTab = 'federated' | 'lake' | 'dictionary'
const COLORS = ['#34d399', '#60a5fa', '#f472b6', '#fbbf24', '#a78bfa', '#22d3ee', '#fb7185']
@@ -117,8 +117,9 @@ function Kpi({ icon: Icon, label, value, sub, accent }: { icon: typeof Users; la
)
}
export function TrinoFederationView() {
const [tab, setTab] = useState<SubTab>('federated')
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)
@@ -189,32 +190,8 @@ export function TrinoFederationView() {
const m = marquee?.marquee
const totals = catalogs?.source_totals || {}
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>
const body = (
<>
{/* ───────── FEDERATED ───────── */}
{tab === 'federated' && (
<>
@@ -368,6 +345,37 @@ export function TrinoFederationView() {
{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>
)
}