Every rule delivers to the ntfy sink configured in Settings.
@@ -420,20 +399,13 @@ export function Alerts() {
{submitError &&
{submitError}
}
-
-
- {editingId && (
-
- )}
-
+
diff --git a/webapp/src/components/CollapsibleSection.tsx b/webapp/src/components/CollapsibleSection.tsx
index dc390ff..9cee9f1 100644
--- a/webapp/src/components/CollapsibleSection.tsx
+++ b/webapp/src/components/CollapsibleSection.tsx
@@ -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 (
+
+
+ {title}
+
+
+
+
+ {popOut}
+
+
+ )
+}
+
+/// 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 (
+
+
+ {editing && (
+
+ )}
+
+ )
+}
+
/// 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'
diff --git a/webapp/src/components/Dashboards.tsx b/webapp/src/components/Dashboards.tsx
index 1b9e682..45c51ca 100644
--- a/webapp/src/components/Dashboards.tsx
+++ b/webapp/src/components/Dashboards.tsx
@@ -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 (