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:
@@ -19,7 +19,6 @@ import { ChangesView } from './components/features/ChangesView'
|
|||||||
import { DataFlowView } from './components/features/DataFlowView'
|
import { DataFlowView } from './components/features/DataFlowView'
|
||||||
import { SearchView } from './components/features/SearchView'
|
import { SearchView } from './components/features/SearchView'
|
||||||
import { DataExplorerView } from './components/features/DataExplorerView'
|
import { DataExplorerView } from './components/features/DataExplorerView'
|
||||||
import { TrinoFederationView } from './components/features/TrinoFederationView'
|
|
||||||
import { DataHubView } from './components/features/DataHubView'
|
import { DataHubView } from './components/features/DataHubView'
|
||||||
import { SshTerminal } from './components/features/SshTerminal'
|
import { SshTerminal } from './components/features/SshTerminal'
|
||||||
import { TerminalDock } from './components/features/TerminalDock'
|
import { TerminalDock } from './components/features/TerminalDock'
|
||||||
@@ -145,8 +144,6 @@ export default function App() {
|
|||||||
<SearchView />
|
<SearchView />
|
||||||
) : cc.mainView === 'dataexplorer' ? (
|
) : cc.mainView === 'dataexplorer' ? (
|
||||||
<DataExplorerView />
|
<DataExplorerView />
|
||||||
) : cc.mainView === 'trino' ? (
|
|
||||||
<TrinoFederationView />
|
|
||||||
) : (
|
) : (
|
||||||
<ApprovalInbox agents={cc.agents} livePending={cc.approvals} onDecide={cc.decide} />
|
<ApprovalInbox agents={cc.agents} livePending={cc.approvals} onDecide={cc.decide} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,9 +11,22 @@ import {
|
|||||||
Search,
|
Search,
|
||||||
Loader2,
|
Loader2,
|
||||||
TrendingUp,
|
TrendingUp,
|
||||||
ExternalLink,
|
BarChart3,
|
||||||
|
Network,
|
||||||
|
HardDrive,
|
||||||
|
ShieldCheck,
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
import { cn } from '../../lib/utils'
|
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 Bucket = { key: string; count: number; value?: number }
|
||||||
type Business = {
|
type Business = {
|
||||||
@@ -195,6 +208,7 @@ export function DataExplorerView() {
|
|||||||
const [region, setRegion] = useState('')
|
const [region, setRegion] = useState('')
|
||||||
const [qInput, setQInput] = useState('')
|
const [qInput, setQInput] = useState('')
|
||||||
const [q, setQ] = useState('')
|
const [q, setQ] = useState('')
|
||||||
|
const [view, setView] = useState<ExplorerTab>('business')
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
@@ -213,20 +227,20 @@ export function DataExplorerView() {
|
|||||||
|
|
||||||
const k = data?.kpis
|
const k = data?.kpis
|
||||||
const regions = useMemo(() => data?.regions || [], [data])
|
const regions = useMemo(() => data?.regions || [], [data])
|
||||||
|
const activeTab = TABS.find((t) => t.id === view) || TABS[0]
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="scrollbar-thin flex h-full min-h-0 flex-col gap-2 overflow-y-auto p-3">
|
<div className="scrollbar-thin flex h-full min-h-0 flex-col gap-2 overflow-y-auto p-3">
|
||||||
{/* header */}
|
{/* header */}
|
||||||
<header className="panel flex shrink-0 flex-wrap items-center justify-between gap-3 px-4 py-3">
|
<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">
|
<h1 className="flex items-center gap-2 text-base font-semibold text-foreground">
|
||||||
<TrendingUp className="h-5 w-5 text-docker" />
|
<TrendingUp className="h-5 w-5 text-docker" />
|
||||||
Data Explorer
|
Data Explorer
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-[11px] text-foreground-muted">
|
<p className="text-[11px] text-foreground-muted">{activeTab.hint}</p>
|
||||||
Your actual business data — customers, orders, employees, supply chain & telemetry across all sources
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
{view === 'business' && (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<form
|
<form
|
||||||
onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()) }}
|
onSubmit={(e) => { e.preventDefault(); setQ(qInput.trim()) }}
|
||||||
@@ -244,8 +258,32 @@ export function DataExplorerView() {
|
|||||||
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />} Refresh
|
{loading ? <Loader2 className="h-3.5 w-3.5 animate-spin" /> : <RefreshCw className="h-3.5 w-3.5" />} Refresh
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</header>
|
</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 */}
|
{/* region filters */}
|
||||||
<div className="flex shrink-0 flex-wrap items-center gap-1.5 px-1 text-[10px]">
|
<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>
|
<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="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>
|
<Panel title="Telemetry — avg value by metric" icon={Activity}><BarsH data={data?.telemetry.by_metric || []} valueKind="num" /></Panel>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import { cn } from '../../lib/utils'
|
import { cn } from '../../lib/utils'
|
||||||
|
|
||||||
type Bucket = { key: string; count: number; value?: number }
|
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']
|
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() {
|
export function TrinoFederationView({ embedded = false, activeTab }: { embedded?: boolean; activeTab?: SubTab } = {}) {
|
||||||
const [tab, setTab] = useState<SubTab>('federated')
|
const [tabState, setTab] = useState<SubTab>('federated')
|
||||||
|
const tab = embedded ? activeTab ?? 'federated' : tabState
|
||||||
const [catalogs, setCatalogs] = useState<any>(null)
|
const [catalogs, setCatalogs] = useState<any>(null)
|
||||||
const [marquee, setMarquee] = useState<any>(null)
|
const [marquee, setMarquee] = useState<any>(null)
|
||||||
const [lake, setLake] = useState<any>(null)
|
const [lake, setLake] = useState<any>(null)
|
||||||
@@ -189,32 +190,8 @@ export function TrinoFederationView() {
|
|||||||
const m = marquee?.marquee
|
const m = marquee?.marquee
|
||||||
const totals = catalogs?.source_totals || {}
|
const totals = catalogs?.source_totals || {}
|
||||||
|
|
||||||
return (
|
const body = (
|
||||||
<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 & 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>
|
|
||||||
|
|
||||||
{/* ───────── FEDERATED ───────── */}
|
{/* ───────── FEDERATED ───────── */}
|
||||||
{tab === 'federated' && (
|
{tab === 'federated' && (
|
||||||
<>
|
<>
|
||||||
@@ -368,6 +345,37 @@ export function TrinoFederationView() {
|
|||||||
{loading && !catalogs && !lake && !dict && (
|
{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>
|
<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 & 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>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { useEffect, useState } from 'react'
|
import { Database, DatabaseZap, HardDrive, Search, LayoutDashboard, MessageSquare, TerminalSquare, Activity, GitBranch, LineChart } from 'lucide-react'
|
||||||
import { Database, DatabaseZap, HardDrive, Search, LayoutDashboard, MessageSquare, Server, TerminalSquare, Activity, GitBranch, ExternalLink, LineChart, Network } from 'lucide-react'
|
|
||||||
import type { GpuStatus, WorkloadData } from '../../types'
|
import type { GpuStatus, WorkloadData } from '../../types'
|
||||||
import type { GpuLiveMetrics } from '../../hooks/useLiveMetrics'
|
import type { GpuLiveMetrics } from '../../hooks/useLiveMetrics'
|
||||||
import { cn } from '../../lib/utils'
|
import { cn } from '../../lib/utils'
|
||||||
@@ -7,7 +6,7 @@ import { viewTabActive, viewTabIdle } from '../../lib/tabActive'
|
|||||||
import { GpuMatrixPanel } from '../features/GpuMatrixPanel'
|
import { GpuMatrixPanel } from '../features/GpuMatrixPanel'
|
||||||
import { LabHealthPanel } from '../features/LabHealthPanel'
|
import { LabHealthPanel } from '../features/LabHealthPanel'
|
||||||
|
|
||||||
type MainView = 'platform' | 'dataquality' | 'knowledge' | 'storage' | 'hdfs' | 'search' | 'approvals' | 'changes' | 'dataflow' | 'datasources' | 'dataexplorer' | 'trino'
|
type MainView = 'platform' | 'dataquality' | 'knowledge' | 'storage' | 'hdfs' | 'search' | 'approvals' | 'changes' | 'dataflow' | 'datasources' | 'dataexplorer'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
workload: WorkloadData | null
|
workload: WorkloadData | null
|
||||||
@@ -27,7 +26,6 @@ const VIEWS: { id: MainView; label: string; icon: typeof LayoutDashboard }[] = [
|
|||||||
{ id: 'platform', label: 'Data Platform', icon: LayoutDashboard },
|
{ id: 'platform', label: 'Data Platform', icon: LayoutDashboard },
|
||||||
{ id: 'datasources', label: 'Data Hub', icon: Database },
|
{ id: 'datasources', label: 'Data Hub', icon: Database },
|
||||||
{ id: 'dataexplorer', label: 'Data Explorer', icon: LineChart },
|
{ id: 'dataexplorer', label: 'Data Explorer', icon: LineChart },
|
||||||
{ id: 'trino', label: 'Trino Federation', icon: Network },
|
|
||||||
{ id: 'changes', label: 'Live Changes', icon: Activity },
|
{ id: 'changes', label: 'Live Changes', icon: Activity },
|
||||||
{ id: 'dataflow', label: 'Data Flow', icon: GitBranch },
|
{ id: 'dataflow', label: 'Data Flow', icon: GitBranch },
|
||||||
{ id: 'dataquality', label: 'Data Quality', icon: DatabaseZap },
|
{ id: 'dataquality', label: 'Data Quality', icon: DatabaseZap },
|
||||||
@@ -50,13 +48,6 @@ export function SideNav({
|
|||||||
onOpenSsh,
|
onOpenSsh,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const matrixBoost = gpuBoost || mainView === 'knowledge'
|
const matrixBoost = gpuBoost || mainView === 'knowledge'
|
||||||
const [kibanaUrl, setKibanaUrl] = useState<string | null>(null)
|
|
||||||
useEffect(() => {
|
|
||||||
fetch('/api/search/health')
|
|
||||||
.then((r) => (r.ok ? r.json() : null))
|
|
||||||
.then((j) => { if (j?.kibana?.ui_url) setKibanaUrl(j.kibana.ui_url) })
|
|
||||||
.catch(() => {})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<nav className="flex h-full min-h-0 w-[240px] shrink-0 flex-col border-r border-border bg-surface-raised">
|
<nav className="flex h-full min-h-0 w-[240px] shrink-0 flex-col border-r border-border bg-surface-raised">
|
||||||
@@ -85,18 +76,6 @@ export function SideNav({
|
|||||||
<TerminalSquare className="h-4 w-4 text-emerald-400" />
|
<TerminalSquare className="h-4 w-4 text-emerald-400" />
|
||||||
<span className="text-[11px] font-medium text-foreground">SSH Terminal</span>
|
<span className="text-[11px] font-medium text-foreground">SSH Terminal</span>
|
||||||
</button>
|
</button>
|
||||||
{kibanaUrl && (
|
|
||||||
<a
|
|
||||||
href={`${kibanaUrl}/app/dashboards#/view/atc-data-overview`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
className={cn('flex w-full items-center gap-2 rounded-md px-2.5 py-2 text-left transition-all', viewTabIdle)}
|
|
||||||
>
|
|
||||||
<ExternalLink className="h-4 w-4 text-amber-400" />
|
|
||||||
<span className="flex-1 text-[11px] font-medium text-foreground">Kibana Dashboards</span>
|
|
||||||
<ExternalLink className="h-3 w-3 text-foreground-faint" />
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function useCommandCenter() {
|
|||||||
const [selectedNode, setSelectedNode] = useState<TopologyNode | null>(null)
|
const [selectedNode, setSelectedNode] = useState<TopologyNode | null>(null)
|
||||||
const [nodeDetail, setNodeDetail] = useState<NodeDetail | null>(null)
|
const [nodeDetail, setNodeDetail] = useState<NodeDetail | null>(null)
|
||||||
const [nodeBusy, setNodeBusy] = useState(false)
|
const [nodeBusy, setNodeBusy] = useState(false)
|
||||||
const [mainView, setMainView] = useState<'platform' | 'approvals' | 'dataquality' | 'knowledge' | 'storage' | 'hdfs' | 'search' | 'changes' | 'dataflow' | 'datasources' | 'dataexplorer' | 'trino'>('platform')
|
const [mainView, setMainView] = useState<'platform' | 'approvals' | 'dataquality' | 'knowledge' | 'storage' | 'hdfs' | 'search' | 'changes' | 'dataflow' | 'datasources' | 'dataexplorer'>('platform')
|
||||||
const [changes, setChanges] = useState<CdcChange[]>([])
|
const [changes, setChanges] = useState<CdcChange[]>([])
|
||||||
const [genPulse, setGenPulse] = useState(false)
|
const [genPulse, setGenPulse] = useState(false)
|
||||||
const genPulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const genPulseTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|||||||
Reference in New Issue
Block a user