From e279a510d83446ca6f94e87473e4d445308befe0 Mon Sep 17 00:00:00 2001 From: Paul Trowbridge Date: Tue, 28 Apr 2026 20:54:55 -0400 Subject: [PATCH] Lift source/version selection to a functional StatusBar MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App.jsx owns sources/sourceId/versions/versionId and persists them across reloads. StatusBar renders the dropdowns plus a status badge — Source-only on Setup, Source · Version · status on Baseline/Forecast. The duplicate in-view selector bars in Forecast and Baseline are gone; Baseline keeps its version actions (New/Close/Reopen/Delete) inline. Setup reports source-list mutations up via refreshSources so the bar stays in sync after register/deregister. Co-Authored-By: Claude Opus 4.7 --- ui/src/App.jsx | 58 +++++++++++++++++++++++++++++---- ui/src/components/StatusBar.jsx | 51 +++++++++++++++++++++-------- ui/src/views/Baseline.jsx | 54 ++++-------------------------- ui/src/views/Forecast.jsx | 45 +------------------------ ui/src/views/Setup.jsx | 3 +- 5 files changed, 100 insertions(+), 111 deletions(-) diff --git a/ui/src/App.jsx b/ui/src/App.jsx index e4d4e3f..1ed8ba1 100644 --- a/ui/src/App.jsx +++ b/ui/src/App.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useState, useEffect, useCallback } from 'react' import Sidebar from './components/Sidebar.jsx' import StatusBar from './components/StatusBar.jsx' import Setup from './views/Setup.jsx' @@ -9,20 +9,66 @@ export default function App() { const [view, setView] = useState(() => localStorage.getItem('pf_view') || 'forecast') const [sidebarExpanded, setSidebarExpanded] = useState(() => localStorage.getItem('pf_sidebar') !== 'collapsed') + const [sources, setSources] = useState([]) + const [sourceId, setSourceId] = useState(() => localStorage.getItem('pf_sourceId') || '') + const [versions, setVersions] = useState([]) + const [versionId, setVersionId] = useState(() => localStorage.getItem('pf_versionId') || '') + useEffect(() => { localStorage.setItem('pf_view', view) }, [view]) useEffect(() => { localStorage.setItem('pf_sidebar', sidebarExpanded ? 'expanded' : 'collapsed') }, [sidebarExpanded]) + useEffect(() => { localStorage.setItem('pf_sourceId', sourceId || '') }, [sourceId]) + useEffect(() => { localStorage.setItem('pf_versionId', versionId || '') }, [versionId]) + + const refreshSources = useCallback(async () => { + const data = await fetch('/api/sources').then(r => r.json()) + setSources(data) + return data + }, []) + + const refreshVersions = useCallback(async (sid) => { + const id = sid ?? sourceId + if (!id) { setVersions([]); return [] } + const data = await fetch(`/api/sources/${id}/versions`).then(r => r.json()) + setVersions(data) + return data + }, [sourceId]) + + useEffect(() => { + refreshSources().then(data => { + if (data.length === 0) { setSourceId(''); return } + if (!sourceId || !data.some(s => String(s.id) === String(sourceId))) { + setSourceId(String(data[0].id)) + } + }) + }, []) + + useEffect(() => { + if (!sourceId) { setVersions([]); setVersionId(''); return } + refreshVersions(sourceId).then(data => { + if (data.length === 0) { setVersionId(''); return } + if (!versionId || !data.some(v => String(v.id) === String(versionId))) { + setVersionId(String(data[0].id)) + } + }) + }, [sourceId]) + + const ctx = { + sources, sourceId, setSourceId, + versions, versionId, setVersionId, + refreshSources, refreshVersions, setVersions, + } return (
- +
- {view === 'setup' && } - {view === 'baseline' && } - {view === 'forecast' && } + {view === 'setup' && } + {view === 'baseline' && } + {view === 'forecast' && }
) -} \ No newline at end of file +} diff --git a/ui/src/components/StatusBar.jsx b/ui/src/components/StatusBar.jsx index d1a62dc..3da49cf 100644 --- a/ui/src/components/StatusBar.jsx +++ b/ui/src/components/StatusBar.jsx @@ -1,21 +1,46 @@ import useTheme from '../theme.jsx' -export default function StatusBar() { +export default function StatusBar({ view, sources = [], sourceId, setSourceId, versions = [], versionId, setVersionId }) { const { dark, setDark } = useTheme() + const showVersion = view === 'baseline' || view === 'forecast' + const selectedVersion = versions.find(v => String(v.id) === String(versionId)) return ( -
+
Source - sales_orders - | - Version - FY2026 Plan - | - Baseline - 44,313 rows - | - Status - open + + + {showVersion && ( + <> + | + Version + + {selectedVersion && ( + + {selectedVersion.status} + + )} + + )} +
) -} \ No newline at end of file +} diff --git a/ui/src/views/Baseline.jsx b/ui/src/views/Baseline.jsx index e62f762..c49dcc0 100644 --- a/ui/src/views/Baseline.jsx +++ b/ui/src/views/Baseline.jsx @@ -55,11 +55,7 @@ function emptyFilter(cols) { return { col: cols[0]?.cname || '', op: 'BETWEEN', values: ['', ''] } } -export default function Baseline() { - const [sources, setSources] = useState([]) - const [sourceId, setSourceId] = useState('') - const [versions, setVersions] = useState([]) - const [versionId, setVersionId] = useState('') +export default function Baseline({ sources = [], sourceId, versions = [], versionId, setVersionId, refreshVersions }) { const [filterCols, setFilterCols] = useState([]) const [log, setLog] = useState([]) @@ -81,20 +77,8 @@ export default function Baseline() { const [expandedId, setExpandedId] = useState(null) const [msg, setMsg] = useState(null) - useEffect(() => { - fetch('/api/sources').then(r => r.json()).then(data => { - setSources(data) - if (data.length > 0) setSourceId(String(data[0].id)) - }) - }, []) - useEffect(() => { if (!sourceId) return - fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()).then(data => { - setVersions(data) - if (data.length > 0) setVersionId(String(data[0].id)) - else setVersionId('') - }) fetch(`/api/sources/${sourceId}/cols`).then(r => r.json()).then(cols => { const fc = cols.filter(c => c.role === 'date' || c.role === 'filter') setFilterCols(fc) @@ -124,8 +108,7 @@ export default function Baseline() { }) const data = await res.json() if (!res.ok) { flash(data.error, 'error'); return } - const updated = await fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()) - setVersions(updated) + await refreshVersions(sourceId) setVersionId(String(data.id)) setShowNewVersion(false) setNewVerName('') @@ -220,8 +203,7 @@ export default function Baseline() { }) const data = await res.json() if (!res.ok) { flash(data.error, 'error'); return } - const updated = await fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()) - setVersions(updated) + await refreshVersions(sourceId) flash('Version closed') } @@ -229,8 +211,7 @@ export default function Baseline() { const res = await fetch(`/api/versions/${versionId}/reopen`, { method: 'POST' }) const data = await res.json() if (!res.ok) { flash(data.error, 'error'); return } - const updated = await fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()) - setVersions(updated) + await refreshVersions(sourceId) flash('Version reopened') } @@ -239,8 +220,7 @@ export default function Baseline() { const res = await fetch(`/api/versions/${versionId}`, { method: 'DELETE' }) const data = await res.json() if (!res.ok) { flash(data.error, 'error'); return } - const updated = await fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()) - setVersions(updated) + const updated = await refreshVersions(sourceId) setVersionId(updated.length > 0 ? String(updated[0].id) : '') flash('Version deleted') } @@ -264,33 +244,13 @@ export default function Baseline() {
)} - {/* Source + Version bar */} + {/* Version actions */}
-
- Source - -
-
- Version - - {versionId && ( - - {selectedVersion?.status} - - )} -
{versionId && ( -
+
{selectedVersion?.status === 'open' ? : diff --git a/ui/src/views/Forecast.jsx b/ui/src/views/Forecast.jsx index 3017291..1f17dc9 100644 --- a/ui/src/views/Forecast.jsx +++ b/ui/src/views/Forecast.jsx @@ -29,12 +29,8 @@ function cleanLayout(cfg, validCols) { return c } -export default function Forecast() { +export default function Forecast({ sourceId, versionId }) { const { dark } = useTheme() - const [sources, setSources] = useState([]) - const [sourceId, setSourceId] = useState('') - const [versions, setVersions] = useState([]) - const [versionId, setVersionId] = useState('') const [loading, setLoading] = useState(false) const [largeDataset, setLargeDataset] = useState(false) const [loadProgress, setLoadProgress] = useState(null) // { received, total } @@ -86,21 +82,6 @@ export default function Forecast() { window.addEventListener('mouseup', onUp) } - useEffect(() => { - fetch('/api/sources').then(r => r.json()).then(data => { - setSources(data) - if (data.length > 0) setSourceId(String(data[0].id)) - }) - }, []) - - useEffect(() => { - if (!sourceId) return - fetch(`/api/sources/${sourceId}/versions`).then(r => r.json()).then(data => { - setVersions(data) - setVersionId(data.length > 0 ? String(data[0].id) : '') - }) - }, [sourceId]) - useEffect(() => { if (!versionId || !sourceId) return loadLayouts(versionId) @@ -420,36 +401,12 @@ export default function Forecast() { } } - const selectedVersion = versions.find(v => String(v.id) === versionId) const dimCols = colMetaRef.current.filter(c => c.role === 'dimension') const hasSlice = Object.keys(slice).length > 0 return (
- {/* Source / version bar */} -
-
- Source - -
-
- Version - - {selectedVersion && ( - - {selectedVersion.status} - - )} -
-
- {/* Toolbar */}
diff --git a/ui/src/views/Setup.jsx b/ui/src/views/Setup.jsx index 3942c52..d267417 100644 --- a/ui/src/views/Setup.jsx +++ b/ui/src/views/Setup.jsx @@ -11,7 +11,7 @@ const ROLE_STYLE = { ignore: 'bg-gray-100 text-gray-400', } -export default function Setup() { +export default function Setup({ refreshSources }) { const [tables, setTables] = useState([]) const [sources, setSources] = useState([]) const [selectedSource, setSelectedSource] = useState(null) @@ -40,6 +40,7 @@ export default function Setup() { }) }) }).catch(console.error) + refreshSources?.() } function selectSource(source) {