Extract shared SectionHeader and FormFooter components (ticket 087)

Params/Metrics/Alerts/Dashboards each hand-assembled the same collapsible-
section header wiring (role/tabIndex/aria-expanded/onClick/onKeyDown around
CollapseToggle+HelpIcon+PopOutButton), and Dashboards duplicated it a second
time for its no-active-profile branch. Params/Metrics/Alerts also duplicated
the declare/edit form footer (editing banner, submit label toggle, Cancel
button). Added SectionHeader and FormFooter to CollapsibleSection.tsx and
adopted them at all five call sites; pure extraction, no behavior change.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TeyhRfMYjMditek6geDRoQ
This commit is contained in:
Hugh Kaznowski
2026-09-07 10:41:17 +01:00
co-authored by Claude Sonnet 5
parent 000efa5166
commit 8008e5611e
5 changed files with 153 additions and 170 deletions
+9 -37
View File
@@ -9,13 +9,7 @@ import {
type AlertRule, type AlertRule,
type CompareOp, type CompareOp,
} from '../lib/api' } from '../lib/api'
import { import { FormFooter, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
CollapseToggle,
handleSectionHeaderKeyDown,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon' import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon' import { RegexHelpIcon } from './RegexHelpIcon'
import { queryInputClassName } from './FilterBar' import { queryInputClassName } from './FilterBar'
@@ -238,22 +232,7 @@ export function Alerts() {
return ( return (
<div className="border-b border-border text-sm"> <div className="border-b border-border text-sm">
<div <SectionHeader title="Alerts" helpSectionId="alerts" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={() => setCollapsed(!collapsed)}
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
>
<div className={sectionHeaderLeftClassName}>
Alerts
<HelpIcon sectionId="alerts" label="Help: Alerts" />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
</div>
</div>
<div hidden={collapsed}> <div hidden={collapsed}>
<p className="px-3 pt-3 text-xs text-text-faint"> <p className="px-3 pt-3 text-xs text-text-faint">
Every rule delivers to the ntfy sink configured in Settings. Every rule delivers to the ntfy sink configured in Settings.
@@ -420,20 +399,13 @@ export function Alerts() {
{submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>} {submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>}
<div className="flex gap-2"> <FormFooter
<button editing={editingId !== null}
type="submit" submitting={submitting}
disabled={submitting} idleLabel="Create rule"
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50" editingLabel="Save rule"
> onCancel={startCreate}
{submitting ? 'Saving…' : editingId ? 'Save rule' : 'Create rule'} />
</button>
{editingId && (
<button type="button" onClick={startCreate} className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2">
Cancel
</button>
)}
</div>
</form> </form>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
@@ -1,5 +1,6 @@
import { useState } from 'react' import { useState } from 'react'
import type { TimeRange, TimeRangePresetId } from '../lib/timeRange' import type { TimeRange, TimeRangePresetId } from '../lib/timeRange'
import { HelpIcon } from './HelpIcon'
/// Shared look for a primary section's title bar: shaded so it reads as a /// Shared look for a primary section's title bar: shaded so it reads as a
/// distinct band rather than blending into the section body below it. /// distinct band rather than blending into the section body below it.
@@ -95,6 +96,100 @@ export function handleSectionHeaderKeyDown(onToggle: () => void) {
} }
} }
/// The header row every settings panel (Params/Metrics/Alerts/Dashboards) opens with: a click/keyboard-toggleable
/// title bar pairing a title + `HelpIcon` on the left with `CollapseToggle` (and, for panels with a standalone
/// popped-out view, a `PopOutButton`) on the right. Assembles `sectionHeaderInteractiveClassName` /
/// `handleSectionHeaderKeyDown` / `sectionHeaderLeftClassName` so callers don't each re-wire the same
/// `role="button"`/`tabIndex`/`aria-expanded` boilerplate by hand.
export function SectionHeader({
title,
helpSectionId,
helpLabel,
collapsed,
onToggle,
popOut,
}: {
title: string
helpSectionId: string
helpLabel?: string
collapsed: boolean
onToggle: () => void
popOut?: React.ReactNode
}) {
return (
<div
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={onToggle}
onKeyDown={handleSectionHeaderKeyDown(onToggle)}
>
<div className={sectionHeaderLeftClassName}>
{title}
<HelpIcon sectionId={helpSectionId} label={helpLabel ?? `Help: ${title}`} />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
{popOut}
</div>
</div>
)
}
/// The declare/edit form footer shared by `Params.tsx`/`Metrics.tsx`/`Alerts.tsx`: a submit button whose label
/// swaps between create/edit and idle/in-flight variants, plus a Cancel button that only appears while editing.
/// Doesn't render the "Editing X" banner some callers show above this — that banner's placement relative to
/// `submitError` differs per caller, so it stays theirs to render; this covers only the part that's identical
/// everywhere. Label text stays a per-caller prop (e.g. "Declare param" vs "Declare metric") rather than
/// hardcoded here, since that's the one part of this footer each panel's copy actually differs on.
export function FormFooter({
editing,
submitting,
idleLabel,
editingLabel,
submittingLabel = 'Saving…',
editingSubmittingLabel,
onCancel,
}: {
editing: boolean
submitting: boolean
idleLabel: string
editingLabel: string
submittingLabel?: string
editingSubmittingLabel?: string
onCancel: () => void
}) {
const label = submitting
? editing
? (editingSubmittingLabel ?? submittingLabel)
: submittingLabel
: editing
? editingLabel
: idleLabel
return (
<div className="flex items-center gap-2">
<button
type="submit"
disabled={submitting}
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
>
{label}
</button>
{editing && (
<button
type="button"
onClick={onCancel}
disabled={submitting}
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
>
Cancel
</button>
)}
</div>
)
}
/// The four primary sections poppable into their own tab; matches the `?section=` query param a standalone page reads. /// The four primary sections poppable into their own tab; matches the `?section=` query param a standalone page reads.
export type PopOutSectionId = 'log' | 'params' | 'metrics' | 'dashboards' export type PopOutSectionId = 'log' | 'params' | 'metrics' | 'dashboards'
+15 -43
View File
@@ -15,15 +15,7 @@ import {
type PromqlSeries, type PromqlSeries,
} from '../lib/api' } from '../lib/api'
import { POLL_MS } from '../lib/polling' import { POLL_MS } from '../lib/polling'
import { import { PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie'] const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
@@ -282,23 +274,13 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
if (!activeProfile) { if (!activeProfile) {
return ( return (
<div className="border-b border-border text-sm"> <div className="border-b border-border text-sm">
<div <SectionHeader
className={sectionHeaderInteractiveClassName} title="Dashboards"
role="button" helpSectionId="dashboards"
tabIndex={0} collapsed={collapsed}
aria-expanded={!collapsed} onToggle={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed(!collapsed)} popOut={<PopOutButton section="dashboards" range={range} activePreset={activePreset} />}
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))} />
>
<div className={sectionHeaderLeftClassName}>
Dashboards
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
<PopOutButton section="dashboards" range={range} activePreset={activePreset} />
</div>
</div>
<div className="p-3 text-xs text-text-faint"> <div className="p-3 text-xs text-text-faint">
Activate a profile (see the settings menu) to create and save dashboards. Activate a profile (see the settings menu) to create and save dashboards.
</div> </div>
@@ -308,23 +290,13 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
return ( return (
<div className="border-b border-border text-sm"> <div className="border-b border-border text-sm">
<div <SectionHeader
className={sectionHeaderInteractiveClassName} title="Dashboards"
role="button" helpSectionId="dashboards"
tabIndex={0} collapsed={collapsed}
aria-expanded={!collapsed} onToggle={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed(!collapsed)} popOut={<PopOutButton section="dashboards" range={range} activePreset={activePreset} />}
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))} />
>
<div className={sectionHeaderLeftClassName}>
Dashboards
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
<PopOutButton section="dashboards" range={range} activePreset={activePreset} />
</div>
</div>
{error && !collapsed && ( {error && !collapsed && (
<div className="mx-3 mt-3 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div> <div className="mx-3 mt-3 rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{error}</div>
)} )}
+17 -45
View File
@@ -13,15 +13,7 @@ import {
type MetricDeclaration, type MetricDeclaration,
type ParamDeclaration, type ParamDeclaration,
} from '../lib/api' } from '../lib/api'
import { import { FormFooter, PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon' import { RegexHelpIcon } from './RegexHelpIcon'
// One row in the labels -> occurrence count table; `values` keeps the raw points for the sparkline rendering. // One row in the labels -> occurrence count table; `values` keeps the raw points for the sparkline rendering.
@@ -207,23 +199,13 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
return ( return (
<div className="border-b border-border text-sm"> <div className="border-b border-border text-sm">
<div <SectionHeader
className={sectionHeaderInteractiveClassName} title="Metrics"
role="button" helpSectionId="metrics"
tabIndex={0} collapsed={collapsed}
aria-expanded={!collapsed} onToggle={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed(!collapsed)} popOut={<PopOutButton section="metrics" range={range} activePreset={activePreset} />}
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))} />
>
<div className={sectionHeaderLeftClassName}>
Metrics
<HelpIcon sectionId="metrics" label="Help: Metrics" />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
<PopOutButton section="metrics" range={range} activePreset={activePreset} />
</div>
</div>
<div className="flex gap-4 p-3" hidden={collapsed}> <div className="flex gap-4 p-3" hidden={collapsed}>
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2"> <form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
<div> <div>
@@ -300,25 +282,15 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
</div> </div>
)} )}
{submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>} {submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>}
<div className="flex items-center gap-2"> <FormFooter
<button editing={editingName !== null}
type="submit" submitting={submitting}
disabled={submitting} idleLabel="Declare metric"
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50" editingLabel="Save changes"
> submittingLabel="Declaring…"
{submitting ? (editingName ? 'Saving…' : 'Declaring…') : editingName ? 'Save changes' : 'Declare metric'} editingSubmittingLabel="Saving…"
</button> onCancel={resetForm}
{editingName && ( />
<button
type="button"
onClick={resetForm}
disabled={submitting}
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
>
Cancel
</button>
)}
</div>
</form> </form>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
+17 -45
View File
@@ -1,14 +1,6 @@
import { useMemo, useState } from 'react' import { useMemo, useState } from 'react'
import { ApiError, clearParams, declareParam, type ParamDeclaration } from '../lib/api' import { ApiError, clearParams, declareParam, type ParamDeclaration } from '../lib/api'
import { import { FormFooter, PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon' import { RegexHelpIcon } from './RegexHelpIcon'
const EMPTY_KIND: ParamDeclaration['kind'] = 'string' const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
@@ -155,23 +147,13 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
return ( return (
<div className="border-b border-border text-sm"> <div className="border-b border-border text-sm">
<div <SectionHeader
className={sectionHeaderInteractiveClassName} title="Params"
role="button" helpSectionId="parameters"
tabIndex={0} collapsed={collapsed}
aria-expanded={!collapsed} onToggle={() => setCollapsed(!collapsed)}
onClick={() => setCollapsed(!collapsed)} popOut={<PopOutButton section="params" />}
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))} />
>
<div className={sectionHeaderLeftClassName}>
Params
<HelpIcon sectionId="parameters" label="Help: Params" />
</div>
<div className="flex items-center gap-1.5">
<CollapseToggle collapsed={collapsed} />
<PopOutButton section="params" />
</div>
</div>
<div className="flex gap-4 p-3" hidden={collapsed}> <div className="flex gap-4 p-3" hidden={collapsed}>
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2"> <form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
<div> <div>
@@ -276,25 +258,15 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
</div> </div>
)} )}
{submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>} {submitError && <div className="rounded border border-danger-border bg-danger-bg/50 px-2 py-1 text-xs text-danger-text">{submitError}</div>}
<div className="flex items-center gap-2"> <FormFooter
<button editing={editingName !== null}
type="submit" submitting={submitting}
disabled={submitting} idleLabel="Declare param"
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50" editingLabel="Save changes"
> submittingLabel="Declaring…"
{submitting ? (editingName ? 'Saving…' : 'Declaring…') : editingName ? 'Save changes' : 'Declare param'} editingSubmittingLabel="Saving…"
</button> onCancel={resetForm}
{editingName && ( />
<button
type="button"
onClick={resetForm}
disabled={submitting}
className="rounded border border-border-soft px-3 py-1 text-sm text-text-secondary hover:bg-surface-2 disabled:opacity-50"
>
Cancel
</button>
)}
</div>
</form> </form>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">