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 CompareOp,
} from '../lib/api'
import {
CollapseToggle,
handleSectionHeaderKeyDown,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { FormFooter, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { RegexHelpIcon } from './RegexHelpIcon'
import { queryInputClassName } from './FilterBar'
@@ -238,22 +232,7 @@ export function Alerts() {
return (
<div className="border-b border-border text-sm">
<div
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>
<SectionHeader title="Alerts" helpSectionId="alerts" collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
<div hidden={collapsed}>
<p className="px-3 pt-3 text-xs text-text-faint">
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>}
<div className="flex 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"
>
{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>
<FormFooter
editing={editingId !== null}
submitting={submitting}
idleLabel="Create rule"
editingLabel="Save rule"
onCancel={startCreate}
/>
</form>
<div className="min-w-0 flex-1">
@@ -1,5 +1,6 @@
import { useState } from 'react'
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
/// 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.
export type PopOutSectionId = 'log' | 'params' | 'metrics' | 'dashboards'
+15 -43
View File
@@ -15,15 +15,7 @@ import {
type PromqlSeries,
} from '../lib/api'
import { POLL_MS } from '../lib/polling'
import {
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
const CHART_TYPES: ChartType[] = ['line', 'bar', 'pie']
@@ -282,23 +274,13 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
if (!activeProfile) {
return (
<div className="border-b border-border text-sm">
<div
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={() => setCollapsed(!collapsed)}
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>
<SectionHeader
title="Dashboards"
helpSectionId="dashboards"
collapsed={collapsed}
onToggle={() => setCollapsed(!collapsed)}
popOut={<PopOutButton section="dashboards" range={range} activePreset={activePreset} />}
/>
<div className="p-3 text-xs text-text-faint">
Activate a profile (see the settings menu) to create and save dashboards.
</div>
@@ -308,23 +290,13 @@ export function Dashboards({ range, activePreset }: { range: TimeRange | null; a
return (
<div className="border-b border-border text-sm">
<div
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={() => setCollapsed(!collapsed)}
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>
<SectionHeader
title="Dashboards"
helpSectionId="dashboards"
collapsed={collapsed}
onToggle={() => setCollapsed(!collapsed)}
popOut={<PopOutButton section="dashboards" range={range} activePreset={activePreset} />}
/>
{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>
)}
+17 -45
View File
@@ -13,15 +13,7 @@ import {
type MetricDeclaration,
type ParamDeclaration,
} from '../lib/api'
import {
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { FormFooter, PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
import { RegexHelpIcon } from './RegexHelpIcon'
// 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 (
<div className="border-b border-border text-sm">
<div
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={() => setCollapsed(!collapsed)}
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>
<SectionHeader
title="Metrics"
helpSectionId="metrics"
collapsed={collapsed}
onToggle={() => setCollapsed(!collapsed)}
popOut={<PopOutButton section="metrics" range={range} activePreset={activePreset} />}
/>
<div className="flex gap-4 p-3" hidden={collapsed}>
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
<div>
@@ -300,25 +282,15 @@ export function Metrics({ range, activePreset }: { range: TimeRange | null; acti
</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">
<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"
>
{submitting ? (editingName ? 'Saving…' : 'Declaring…') : editingName ? 'Save changes' : 'Declare metric'}
</button>
{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>
<FormFooter
editing={editingName !== null}
submitting={submitting}
idleLabel="Declare metric"
editingLabel="Save changes"
submittingLabel="Declaring…"
editingSubmittingLabel="Saving…"
onCancel={resetForm}
/>
</form>
<div className="min-w-0 flex-1">
+17 -45
View File
@@ -1,14 +1,6 @@
import { useMemo, useState } from 'react'
import { ApiError, clearParams, declareParam, type ParamDeclaration } from '../lib/api'
import {
CollapseToggle,
handleSectionHeaderKeyDown,
PopOutButton,
sectionHeaderInteractiveClassName,
sectionHeaderLeftClassName,
useSectionCollapsed,
} from './CollapsibleSection'
import { HelpIcon } from './HelpIcon'
import { FormFooter, PopOutButton, SectionHeader, useSectionCollapsed } from './CollapsibleSection'
import { RegexHelpIcon } from './RegexHelpIcon'
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
@@ -155,23 +147,13 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
return (
<div className="border-b border-border text-sm">
<div
className={sectionHeaderInteractiveClassName}
role="button"
tabIndex={0}
aria-expanded={!collapsed}
onClick={() => setCollapsed(!collapsed)}
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>
<SectionHeader
title="Params"
helpSectionId="parameters"
collapsed={collapsed}
onToggle={() => setCollapsed(!collapsed)}
popOut={<PopOutButton section="params" />}
/>
<div className="flex gap-4 p-3" hidden={collapsed}>
<form onSubmit={handleSubmit} className="w-80 shrink-0 space-y-2">
<div>
@@ -276,25 +258,15 @@ export function Params({ params, onDeclared }: { params: ParamDeclaration[]; onD
</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">
<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"
>
{submitting ? (editingName ? 'Saving…' : 'Declaring…') : editingName ? 'Save changes' : 'Declare param'}
</button>
{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>
<FormFooter
editing={editingName !== null}
submitting={submitting}
idleLabel="Declare param"
editingLabel="Save changes"
submittingLabel="Declaring…"
editingSubmittingLabel="Saving…"
onCancel={resetForm}
/>
</form>
<div className="min-w-0 flex-1">