import { useState, useEffect, createElement, lazy, Suspense } from 'react' import { BrowserRouter, Routes, Route, Navigate, useParams } from 'react-router-dom' import { api, setCredentials, clearCredentials } from './api' import Sidebar from './components/Sidebar.jsx' import BottomNav from './components/BottomNav.jsx' import SourceTabs from './components/SourceTabs.jsx' import Login from './pages/Login' import SourceList from './pages/SourceList' import SourceDetail from './pages/SourceDetail' import Bridge from './pages/Bridge' import ImportHub from './pages/ImportHub' import Import from './pages/Import' import Rules from './pages/Rules' import Mappings from './pages/Mappings' import Records from './pages/Records' import Log from './pages/Log' const Pivot = lazy(() => import('./pages/Pivot')) import Remap from './pages/Remap' import Stacks from './pages/Stacks' // Source-scoped pages still take a `source` prop; this reads it off the URL so // they didn't all need rewriting when selection moved out of the status bar. function ScopedToSource({ component, ...props }) { const { name } = useParams() return createElement(component, { source: name, ...props }) } // Pivot doubles as the stack viewer; a stack in the URL takes precedence there function StackPivot() { const { name } = useParams() return {}} /> } export default function App() { const [authed, setAuthed] = useState(false) const [loginUser, setLoginUser] = useState('') const [sources, setSources] = useState([]) const [source, setSource] = useState(() => localStorage.getItem('selectedSource') || '') const [sidebarExpanded, setSidebarExpanded] = useState(() => localStorage.getItem('df_sidebar') !== 'collapsed') // Sets of names whose dfv view is out of sync with current definitions const [staleSources, setStaleSources] = useState(new Set()) const [staleStacks, setStaleStacks] = useState(new Set()) const [reprocessSources, setReprocessSources] = useState(new Set()) const [generating, setGenerating] = useState({}) // { 'source:name': true } async function handleLogin(user, pass) { setCredentials(user, pass) const s = await api.getSources() sessionStorage.setItem('df_user', user) sessionStorage.setItem('df_pass', pass) setSources(s) if (!source && s.length > 0) setSource(s[0].name) setAuthed(true) setLoginUser(user) } function handleLogout() { clearCredentials() sessionStorage.removeItem('df_user') sessionStorage.removeItem('df_pass') setAuthed(false) setLoginUser('') setSources([]) setStaleSources(new Set()) setStaleStacks(new Set()) setReprocessSources(new Set()) } // Load initial stale state from DB once on login useEffect(() => { if (!authed) return api.getStatus().then(s => { setStaleSources(new Set((s.stale_sources || []).map(x => x.name))) setStaleStacks(new Set((s.stale_stacks || []).map(x => x.name))) }).catch(() => {}) }, [authed]) function markSourceStale(name) { setStaleSources(prev => new Set([...prev, name])) } function markNeedsReprocess(name) { setReprocessSources(prev => new Set([...prev, name])) } async function handleReprocessSource(name) { setGenerating(g => ({ ...g, [`rp:${name}`]: true })) try { await api.reprocess(name) setReprocessSources(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`rp:${name}`]; return n }) } } function markStackStale(name) { setStaleStacks(prev => new Set([...prev, name])) } function clearStackStale(name) { setStaleStacks(prev => { const n = new Set(prev); n.delete(name); return n }) } async function handleGenerateSource(name) { setGenerating(g => ({ ...g, [`src:${name}`]: true })) try { await api.generateView(name) setStaleSources(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`src:${name}`]; return n }) } } async function handleGenerateStack(name) { setGenerating(g => ({ ...g, [`stk:${name}`]: true })) try { await api.generateStackView(name) setStaleStacks(prev => { const n = new Set(prev); n.delete(name); return n }) } catch (e) { alert(e.message) } finally { setGenerating(g => { const n = { ...g }; delete n[`stk:${name}`]; return n }) } } // On mount, restore session if credentials are saved useEffect(() => { const user = sessionStorage.getItem('df_user') const pass = sessionStorage.getItem('df_pass') if (user && pass) handleLogin(user, pass).catch(() => handleLogout()) }, []) useEffect(() => { if (source) localStorage.setItem('selectedSource', source) }, [source]) useEffect(() => { localStorage.setItem('df_sidebar', sidebarExpanded ? 'expanded' : 'collapsed') }, [sidebarExpanded]) if (!authed) return return (
{/* Main */}
{(staleSources.size > 0 || staleStacks.size > 0) && (
View out of sync: {[...staleSources].map(name => ( {name} ))} {staleSources.size > 0 && staleStacks.size > 0 && |} {[...staleStacks].map(name => ( stack: {name} ))}
)} {reprocessSources.size > 0 && (
Mappings updated: {[...reprocessSources].map(name => ( {name} ))}
)}
Loading…
}> } /> } /> }> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } /> } />
) }