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:
co-authored by
Claude Sonnet 5
parent
000efa5166
commit
8008e5611e
@@ -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,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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user