import { useState, useEffect } from 'react' const ROLES = ['ignore', 'dimension', 'value', 'units', 'date', 'filter'] const ROLE_STYLE = { dimension: 'bg-blue-50 text-blue-700', value: 'bg-green-50 text-green-700', units: 'bg-green-50 text-green-700', date: 'bg-purple-50 text-purple-700', filter: 'bg-yellow-50 text-yellow-700', ignore: 'bg-gray-100 text-gray-400', } export default function Setup() { const [tables, setTables] = useState([]) const [sources, setSources] = useState([]) const [selectedSource, setSelectedSource] = useState(null) const [cols, setCols] = useState([]) const [editedCols, setEditedCols] = useState([]) const [colsDirty, setColsDirty] = useState(false) const [preview, setPreview] = useState(null) // { schema, tname, columns, rows } const [previewLoading, setPreviewLoading] = useState(false) const [sqlStatus, setSqlStatus] = useState({}) // sourceId -> bool const [saving, setSaving] = useState(false) const [generating, setGenerating] = useState(false) const [msg, setMsg] = useState(null) useEffect(() => { fetch('/api/tables').then(r => r.json()).then(setTables).catch(console.error) loadSources() }, []) function loadSources() { fetch('/api/sources').then(r => r.json()).then(data => { setSources(data) // check sql status for each source data.forEach(s => { fetch(`/api/sources/${s.id}/sql`).then(r => r.json()).then(sqls => { setSqlStatus(prev => ({ ...prev, [s.id]: sqls.length > 0 })) }) }) }).catch(console.error) } function selectSource(source) { setSelectedSource(source) setColsDirty(false) fetch(`/api/sources/${source.id}/cols`).then(r => r.json()).then(data => { setCols(data) setEditedCols(data.map(c => ({ ...c }))) }) } async function openPreview(schema, tname, e) { e.stopPropagation() setPreviewLoading(true) setPreview({ schema, tname, loading: true }) try { const data = await fetch(`/api/tables/${schema}/${tname}/preview`).then(r => r.json()) setPreview({ schema, tname, ...data }) } catch { setPreview(null) } finally { setPreviewLoading(false) } } async function registerSource(schema, tname) { try { const res = await fetch('/api/sources', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ schema, tname, created_by: 'admin' }) }) if (!res.ok) { const err = await res.json() flash(err.error, 'error') return } const source = await res.json() loadSources() flash(`Registered ${schema}.${tname}`) // auto-select new source and load its cols fetch(`/api/sources/${source.id}/cols`).then(r => r.json()).then(data => { setSelectedSource(source) setCols(data) setEditedCols(data.map(c => ({ ...c }))) setColsDirty(false) }) } catch (err) { flash(err.message, 'error') } } function updateCol(idx, field, value) { setEditedCols(prev => { const next = prev.map((c, i) => i === idx ? { ...c, [field]: value } : c) return next }) setColsDirty(true) } async function saveCols() { setSaving(true) try { const res = await fetch(`/api/sources/${selectedSource.id}/cols`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(editedCols) }) if (!res.ok) { const e = await res.json(); flash(e.error, 'error'); return } const saved = await res.json() setCols(saved) setEditedCols(saved.map(c => ({ ...c }))) setColsDirty(false) flash('Saved') } catch (err) { flash(err.message, 'error') } finally { setSaving(false) } } async function generateSQL() { setGenerating(true) try { const res = await fetch(`/api/sources/${selectedSource.id}/generate-sql`, { method: 'POST' }) const data = await res.json() if (!res.ok) { flash(data.error, 'error'); return } setSqlStatus(prev => ({ ...prev, [selectedSource.id]: true })) flash(`SQL generated: ${data.operations.join(', ')}`) } catch (err) { flash(err.message, 'error') } finally { setGenerating(false) } } async function deleteSource(id, e) { e.stopPropagation() if (!confirm('Deregister this source? Existing forecast tables are not affected.')) return await fetch(`/api/sources/${id}`, { method: 'DELETE' }) if (selectedSource?.id === id) { setSelectedSource(null); setCols([]); setEditedCols([]) } loadSources() } function flash(text, type = 'ok') { setMsg({ text, type }) setTimeout(() => setMsg(null), 3000) } const registeredKeys = new Set(sources.map(s => `${s.schema}.${s.tname}`)) return (
{/* All Tables */}
All Tables
{tables.map(t => { const key = `${t.schema}.${t.tname}` const registered = registeredKeys.has(key) return ( openPreview(t.schema, t.tname, e)} className="border-t border-gray-50 hover:bg-blue-50 cursor-pointer group" > ) })}
schema table rows
{t.schema}
{t.tname} {registered && }
{Number(t.row_estimate).toLocaleString()}
{/* Right side */}
{/* Flash message */} {msg && (
{msg.text}
)}
{/* Registered Sources */}
Registered Sources
{sources.length === 0 && ( )} {sources.map(s => ( selectSource(s)} className={`border-t border-gray-50 cursor-pointer hover:bg-gray-50 ${selectedSource?.id === s.id ? 'bg-blue-50' : ''}`} > ))}
source schema sql created
No sources registered — click a table to preview, then register it
{s.tname} {s.schema} {sqlStatus[s.id] ? ✓ ready : } {s.created_by || '—'}
{/* Col Meta Editor */} {selectedSource ? (
Col Meta — {selectedSource.schema}.{selectedSource.tname}
{colsDirty && ( )}
{editedCols.map((col, i) => ( ))}
column role key label
{col.cname} updateCol(i, 'is_key', e.target.checked)} disabled={col.role !== 'dimension'} className="cursor-pointer disabled:opacity-20" /> updateCol(i, 'label', e.target.value)} placeholder={col.cname} className="border border-transparent hover:border-gray-200 focus:border-gray-300 rounded px-1.5 py-0.5 w-full outline-none bg-transparent" />
) : (
Select a source to edit col meta
)}
{/* Table preview modal */} {preview && (
setPreview(null)} />
{preview.schema}.{preview.tname} {preview.columns && ( {preview.columns.length} columns )} {!registeredKeys.has(`${preview.schema}.${preview.tname}`) && ( )}
{preview.loading ? (
Loading…
) : (
{/* Columns */}
Columns
{(preview.columns || []).map(c => ( ))}
name type nullable
{c.column_name} {c.data_type} {c.is_nullable}
{/* Sample rows */}
Sample rows
{(preview.columns || []).map(c => ( ))} {(preview.rows || []).map((row, i) => ( {(preview.columns || []).map(c => ( ))} ))}
{c.column_name}
{row[c.column_name] == null ? 'null' : String(row[c.column_name])}
)}
)}
) }