Add "All dashboards" print-friendly stacked view (ticket 009)

Adds an "All dashboards" selector alongside the existing click-to-select
single-dashboard view. Selecting it stacks every dashboard vertically in
normal document flow, each preceded by a heading naming it, with no
fixed-height or scroll-clipping containers, so the browser's native print
function can render the full stack across pages.

Verified in-browser: single-dashboard editing still works unchanged, and
"All dashboards" renders all dashboards (including an empty one) stacked
with named separators; confirmed via computed layout that only the
pre-existing app-level scroll container grows to fit the content, with no
clipping container inside the Dashboards section itself.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
Hugh Kaznowski
2026-09-05 18:54:52 +01:00
co-authored by Claude Sonnet 5
parent 6b6480e99d
commit 559883dbab
2 changed files with 49 additions and 10 deletions
+49 -1
View File
@@ -26,6 +26,9 @@ import { HelpIcon } from './HelpIcon'
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
/// Virtual selection id for the "All dashboards" stacked/print view; distinct from any real dashboard id.
const ALL_DASHBOARDS_ID = '__all__'
/// Long enough that rapid typing doesn't spam the save endpoint, short enough that a pause reads as "done" and commits promptly.
const AUTOSAVE_DEBOUNCE_MS = 600
@@ -96,6 +99,36 @@ function AutosaveStatus({ status, error }: { status: SaveStatus; error: string |
return null
}
/// Read-only, print-friendly view of every dashboard stacked one after another, each preceded by a heading naming it.
/// Deliberately uses normal document flow (no scroll containers, no fixed heights) so the browser's native print
/// function can lay the full stack out across as many pages as it needs.
function AllDashboardsView({ dashboards, range }: { dashboards: Dashboard[]; range: TimeRange | null }) {
if (dashboards.length === 0) {
return <div className="text-xs text-text-faint">No dashboards saved yet.</div>
}
return (
<div className="space-y-6">
{dashboards.map((d) => (
<section key={d.id} className="break-inside-avoid-page">
<h3 className="mb-2 border-b border-border pb-1 text-sm font-medium text-text">{d.name}</h3>
{d.panels.length === 0 ? (
<div className="text-xs text-text-faint">No panels in this dashboard.</div>
) : (
<div className="grid grid-cols-1 gap-3 lg:grid-cols-2 print:grid-cols-1">
{d.panels.map((panel) => (
<div key={panel.id} className="break-inside-avoid-page rounded border border-border p-2">
{panel.title && <div className="mb-1 text-xs text-text-subtle">{panel.title}</div>}
<PanelPreview panel={panel} range={range} />
</div>
))}
</div>
)}
</section>
))}
</div>
)
}
export function Dashboards({ range }: { range: TimeRange | null }) {
const [activeProfile, setActiveProfile] = useState<string | null>(null)
const [dashboards, setDashboards] = useState<Dashboard[]>([])
@@ -281,6 +314,19 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
</button>
</form>
<div className="space-y-1">
{dashboards.length > 0 && (
<button
type="button"
className={`flex w-full items-center rounded border px-2 py-1 text-left text-xs ${
selectedId === ALL_DASHBOARDS_ID
? 'border-accent bg-accent/10 text-accent-text'
: 'border-border-soft text-text-secondary hover:bg-surface-2'
}`}
onClick={() => setSelectedId(ALL_DASHBOARDS_ID)}
>
All dashboards
</button>
)}
{dashboards.length === 0 && <div className="text-xs text-text-faint">No dashboards saved yet.</div>}
{dashboards.map((d) => (
<div
@@ -308,7 +354,9 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
</div>
<div className="min-w-0 flex-1">
{!draft ? (
{selectedId === ALL_DASHBOARDS_ID ? (
<AllDashboardsView dashboards={dashboards} range={range} />
) : !draft ? (
<div className="text-xs text-text-faint">Select or create a dashboard to edit its panels.</div>
) : (
<div className="space-y-3">