import { useCallback, useEffect, useRef, useState } from 'react' import { Activity, Cpu, Database, ExternalLink, Gauge, HardDrive, Layers, Loader2, Play, Plus, RefreshCw, Save, Server, Square, Table2, Trash2, Zap, } from 'lucide-react' import type { SparkLive, SparkRun, SparkRunStats } from '../../types' import { cancelSparkRun, createSparkRun, fetchSparkCatalogs, fetchSparkColumns, fetchSparkLive, fetchSparkRun, fetchSparkRuns, fetchSparkSchemas, fetchSparkTables, } from '../../lib/api' import { cn } from '../../lib/utils' import { subTabActive, subTabIdle } from '../../lib/tabActive' type Tab = 'workbench' | 'cluster' | 'runs' | 'ui' type Operation = 'preview' | 'filter' | 'aggregate' | 'profile' | 'join' | 'sql' type Column = { name: string; type: string } type Metric = { fn: string; col: string; alias?: string } const OPERATIONS: { id: Operation; label: string; desc: string }[] = [ { id: 'preview', label: 'Preview', desc: 'Sample rows from a table' }, { id: 'filter', label: 'Filter', desc: 'WHERE predicate on a table' }, { id: 'aggregate', label: 'Aggregate', desc: 'Group by + sum/avg/count/min/max' }, { id: 'profile', label: 'Profile', desc: 'Row count, distinct & non-null per column' }, { id: 'join', label: 'Join', desc: 'Join two tables on keys' }, { id: 'sql', label: 'SQL', desc: 'Run arbitrary distributed SQL' }, ] const AGG_FNS = ['count', 'sum', 'avg', 'min', 'max', 'approx_distinct', 'count_distinct'] function fmtNum(n?: number | null) { if (n == null) return '—' if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B` if (n >= 1e6) return `${(n / 1e6).toFixed(2)}M` if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K` return String(n) } function fmtBytes(n?: number | null) { if (n == null) return '—' let v = n for (const u of ['B', 'KB', 'MB', 'GB', 'TB']) { if (v < 1024) return `${v.toFixed(u === 'B' ? 0 : 1)} ${u}` v /= 1024 } return `${v.toFixed(1)} PB` } function fmtMs(n?: number | null) { if (n == null) return '—' if (n < 1000) return `${n} ms` if (n < 60000) return `${(n / 1000).toFixed(1)} s` return `${(n / 60000).toFixed(1)} m` } const STATE_COLOR: Record = { QUEUED: 'text-amber-300 bg-amber-500/15', RUNNING: 'text-sky-300 bg-sky-500/15', FINISHED: 'text-emerald-300 bg-emerald-500/15', FAILED: 'text-rose-300 bg-rose-500/15', CANCELED: 'text-foreground-muted bg-surface-overlay', } export function SparkView({ embedded }: { embedded?: boolean }) { const [tab, setTab] = useState('workbench') const [live, setLive] = useState(null) const loadLive = useCallback(async () => { const l = await fetchSparkLive() if (l) setLive(l) }, []) useEffect(() => { loadLive() const iv = setInterval(loadLive, 4000) return () => clearInterval(iv) }, [loadLive]) const spark = live?.spark const alive = spark?.ui_ok && (spark?.status || '').toUpperCase() === 'ALIVE' const activeRuns = live?.active_runs ?? [] return (

Spark Lakehouse Workbench

Select data · transform on the distributed engine · materialize to Iceberg / S3 · live cluster metrics

0} /> 0} /> Native UI
{(['workbench', 'cluster', 'runs', 'ui'] as Tab[]).map((t) => ( ))}
{tab === 'workbench' && } {tab === 'cluster' && } {tab === 'runs' && } {tab === 'ui' && (