diff --git a/tickets/035-collapsible-sections.md b/tickets/035-collapsible-sections.md deleted file mode 100644 index 0faa006..0000000 --- a/tickets/035-collapsible-sections.md +++ /dev/null @@ -1,34 +0,0 @@ -# 035 — Collapsible primary UI sections - -## Summary - -`App.tsx` stacks the Log viewer (`LogTimeline` + `LogPreview`), Params -([034](034-params-declaration-ui.md)), `Metrics`, and `Dashboards` vertically -with no way to collapse any of them. On a small screen — or just when only -one section is being worked on — the others take up space and force -scrolling. Make each section collapsible. - -## Scope - -- Each of the four sections (Log viewer, Params, Metrics, Dashboards) gets a - collapse/expand toggle in its header, next to its existing "?" help icon. -- Collapsed state persists across reloads per-section, via `localStorage` - (same pattern as `LogPreview`'s `VISIBLE_PARAM_COLUMNS_STORAGE_KEY` at - `webapp/src/components/LogPreview.tsx:13`). -- A collapsed section still shows its header (title + toggle), just not its - body/content. -- Collapsing a section must not tear down state that would be lost/reset on - re-expand (e.g. `Dashboards`' in-progress panel edits, `Metrics`' declare - form) — hide with CSS/conditional render at the container level, not by - unmounting stateful subtrees. - -## Acceptance criteria - -- Each section can be independently collapsed and re-expanded. -- Reloading the page preserves each section's collapsed/expanded state. -- Collapsing a section with unsaved in-progress input (e.g. a half-filled - Metrics declare form) and re-expanding it preserves that input. - -## Depends on - -- [034 — Params declaration UI](034-params-declaration-ui.md) diff --git a/webapp/src/App.tsx b/webapp/src/App.tsx index 98e14b9..2b4c667 100644 --- a/webapp/src/App.tsx +++ b/webapp/src/App.tsx @@ -7,6 +7,7 @@ import { Params } from './components/Params' import { Metrics } from './components/Metrics' import { Dashboards } from './components/Dashboards' import { Alerts } from './components/Alerts' +import { CollapseToggle, useSectionCollapsed } from './components/CollapsibleSection' import { fetchLogs, fetchParamDeclarations, @@ -30,6 +31,7 @@ export default function App() { const [activePreset, setActivePreset] = useState('all') const [filter, setFilter] = useState('') const [filterError, setFilterError] = useState(null) + const [logViewerCollapsed, setLogViewerCollapsed] = useSectionCollapsed('log-viewer') // Read from inside the poll loop below without making it a dependency — // a rolling preset's own re-resolution (ticket 024) updates `range`, and @@ -123,15 +125,24 @@ export default function App() {
- - setLogViewerCollapsed(!logViewerCollapsed)} /> + } /> - + diff --git a/webapp/src/components/CollapsibleSection.tsx b/webapp/src/components/CollapsibleSection.tsx new file mode 100644 index 0000000..364d537 --- /dev/null +++ b/webapp/src/components/CollapsibleSection.tsx @@ -0,0 +1,59 @@ +import { useState } from 'react' + +/// localStorage key for which of the four primary sections (log viewer, +/// params, metrics, dashboards) are collapsed (ticket 035). Mirrors +/// LogPreview's VISIBLE_PARAM_COLUMNS_STORAGE_KEY pattern — one key holding +/// a record of section id -> collapsed. +const COLLAPSED_SECTIONS_STORAGE_KEY = 'ottyl.collapsedSections' + +function loadCollapsedSections(): Record { + try { + const raw = localStorage.getItem(COLLAPSED_SECTIONS_STORAGE_KEY) + if (!raw) return {} + const parsed = JSON.parse(raw) + return parsed && typeof parsed === 'object' ? parsed : {} + } catch { + return {} + } +} + +function saveCollapsedSections(sections: Record) { + try { + localStorage.setItem(COLLAPSED_SECTIONS_STORAGE_KEY, JSON.stringify(sections)) + } catch { + // best-effort — a private browsing mode or full storage just means the + // collapsed state won't persist across reloads. + } +} + +/// Persists a section's collapsed state across reloads. Deliberately +/// returns only the boolean + setter — callers keep rendering their +/// subtree, just hide it with CSS (`hidden`), so collapsing never unmounts +/// in-progress form state. +export function useSectionCollapsed(sectionId: string): [boolean, (next: boolean) => void] { + const [collapsed, setCollapsedState] = useState(() => loadCollapsedSections()[sectionId] ?? false) + + function setCollapsed(next: boolean) { + setCollapsedState(next) + const all = loadCollapsedSections() + all[sectionId] = next + saveCollapsedSections(all) + } + + return [collapsed, setCollapsed] +} + +export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) { + return ( + + ) +} diff --git a/webapp/src/components/Dashboards.tsx b/webapp/src/components/Dashboards.tsx index fbdc77d..9a43aa2 100644 --- a/webapp/src/components/Dashboards.tsx +++ b/webapp/src/components/Dashboards.tsx @@ -14,6 +14,7 @@ import { type Panel, type PromqlSeries, } from '../lib/api' +import { CollapseToggle, useSectionCollapsed } from './CollapsibleSection' import { HelpIcon } from './HelpIcon' const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie'] @@ -113,6 +114,7 @@ export function Dashboards({ range }: { range: TimeRange | null }) { // nothing actually changed (e.g. re-selecting the same dashboard, or the // re-render that follows a successful save itself). const lastSavedRef = useRef(null) + const [collapsed, setCollapsed] = useSectionCollapsed('dashboards') async function loadDashboards(profileName: string, selectAfterLoad?: string) { try { @@ -231,6 +233,7 @@ export function Dashboards({ range }: { range: TimeRange | null }) { return (
+ setCollapsed(!collapsed)} /> Dashboards — activate a profile (see the settings menu) to create and save dashboards.
) @@ -240,12 +243,13 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
+ setCollapsed(!collapsed)} /> Dashboards — profile "{activeProfile}"
- {error && ( + {error && !collapsed && (
{error}
)} -
+