Collapse sections by clicking their title bar (ticket 006)
Alerts, Metrics, Params, and Dashboards now toggle collapsed state when their header row is clicked, instead of requiring a dedicated collapse button. The chevron (CollapseToggle) becomes a purely visual, non-focusable indicator for these sections; HelpIcon and PopOutButton now stop propagation so they keep working without also collapsing the section. The log viewer's toggle (App.tsx/FilterBar) keeps its dedicated button, since that row is a filter toolbar with an editable input rather than a plain title bar, so CollapseToggle's onToggle prop stays available for that case. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XFUNZwCvz3kcod3352ZR2o
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
01eadf415c
commit
5038295554
@@ -1,8 +0,0 @@
|
|||||||
# Collapse sections by clicking title bar instead of a dedicated button
|
|
||||||
|
|
||||||
Instead of having collapse buttons on the main view components/categories, clicking the title bars should collapse them.
|
|
||||||
|
|
||||||
## Acceptance criteria
|
|
||||||
- Clicking a section's title bar toggles its collapsed/expanded state.
|
|
||||||
- Dedicated collapse buttons are removed (or repurposed as a visual indicator only, e.g. a chevron, without being the sole click target).
|
|
||||||
- Title bar remains usable for any other existing interactions (if any) without conflicting with the collapse click.
|
|
||||||
@@ -9,7 +9,13 @@ import {
|
|||||||
type AlertRule,
|
type AlertRule,
|
||||||
type CompareOp,
|
type CompareOp,
|
||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
import { CollapseToggle, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
import {
|
||||||
|
CollapseToggle,
|
||||||
|
handleSectionHeaderKeyDown,
|
||||||
|
sectionHeaderInteractiveClassName,
|
||||||
|
sectionHeaderLeftClassName,
|
||||||
|
useSectionCollapsed,
|
||||||
|
} from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
import { queryInputClassName } from './FilterBar'
|
import { queryInputClassName } from './FilterBar'
|
||||||
|
|
||||||
@@ -231,13 +237,20 @@ export function Alerts() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className={sectionHeaderClassName}>
|
<div
|
||||||
|
className={sectionHeaderInteractiveClassName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
|
||||||
|
>
|
||||||
<div className={sectionHeaderLeftClassName}>
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Alerts
|
Alerts
|
||||||
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
<HelpIcon sectionId="alerts" label="Help: Alerts" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div hidden={collapsed}>
|
<div hidden={collapsed}>
|
||||||
|
|||||||
@@ -3,9 +3,15 @@ import type { TimeRange } from './LogTimeline'
|
|||||||
|
|
||||||
/// 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.
|
||||||
|
/// The bar itself is the collapse/expand click target (see `sectionHeaderInteractiveClassName`) —
|
||||||
|
/// this className alone (no click handling) is for the rare header that stays static.
|
||||||
export const sectionHeaderClassName =
|
export const sectionHeaderClassName =
|
||||||
'flex items-center justify-between gap-2 border-b border-border-soft bg-surface-1 px-3 py-1.5'
|
'flex items-center justify-between gap-2 border-b border-border-soft bg-surface-1 px-3 py-1.5'
|
||||||
|
|
||||||
|
/// `sectionHeaderClassName` plus the affordance for a header row that toggles collapse on click:
|
||||||
|
/// pointer cursor and a hover tint so the whole bar reads as clickable, not just the chevron.
|
||||||
|
export const sectionHeaderInteractiveClassName = `${sectionHeaderClassName} cursor-pointer select-none hover:bg-surface-2`
|
||||||
|
|
||||||
/// Left-hand cluster of a section header: title text, help icon, collapse/pop-out controls.
|
/// Left-hand cluster of a section header: title text, help icon, collapse/pop-out controls.
|
||||||
export const sectionHeaderLeftClassName = 'flex items-center gap-1.5 text-sm font-medium text-text-secondary'
|
export const sectionHeaderLeftClassName = 'flex items-center gap-1.5 text-sm font-medium text-text-secondary'
|
||||||
|
|
||||||
@@ -46,7 +52,24 @@ export function useSectionCollapsed(sectionId: string): [boolean, (next: boolean
|
|||||||
return [collapsed, setCollapsed]
|
return [collapsed, setCollapsed]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
|
/// Chevron indicator for a section's collapsed/expanded state. For the title-bar sections
|
||||||
|
/// (Alerts/Metrics/Params/Dashboards), the whole header row is the click target (see
|
||||||
|
/// `sectionHeaderInteractiveClassName`) and `onToggle` is omitted — this renders as a
|
||||||
|
/// non-interactive, decorative `span` rather than a nested `<button>`, with `aria-expanded`
|
||||||
|
/// living on the header row itself. `onToggle` stays available for the log viewer's toggle
|
||||||
|
/// (`App.tsx`), which sits in a filter toolbar rather than a plain title bar — there the
|
||||||
|
/// chevron itself remains a dedicated button.
|
||||||
|
export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle?: () => void }) {
|
||||||
|
if (!onToggle) {
|
||||||
|
return (
|
||||||
|
<span
|
||||||
|
aria-hidden="true"
|
||||||
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted"
|
||||||
|
>
|
||||||
|
{collapsed ? '▸' : '▾'}
|
||||||
|
</span>
|
||||||
|
)
|
||||||
|
}
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -61,6 +84,17 @@ export function CollapseToggle({ collapsed, onToggle }: { collapsed: boolean; on
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Keyboard handler for a header row using `sectionHeaderInteractiveClassName`: Enter/Space
|
||||||
|
/// activate it like a button, matching the `role="button"` semantics on the row.
|
||||||
|
export function handleSectionHeaderKeyDown(onToggle: () => void) {
|
||||||
|
return (e: React.KeyboardEvent) => {
|
||||||
|
if (e.key === 'Enter' || e.key === ' ') {
|
||||||
|
e.preventDefault()
|
||||||
|
onToggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 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'
|
||||||
|
|
||||||
@@ -78,7 +112,10 @@ export function PopOutButton({ section, range }: { section: PopOutSectionId; ran
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => window.open(popOutUrl(section, range), '_blank')}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
window.open(popOutUrl(section, range), '_blank')
|
||||||
|
}}
|
||||||
title="Open in new tab"
|
title="Open in new tab"
|
||||||
aria-label="Open in new tab"
|
aria-label="Open in new tab"
|
||||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||||
|
|||||||
@@ -16,8 +16,9 @@ import {
|
|||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
import {
|
import {
|
||||||
CollapseToggle,
|
CollapseToggle,
|
||||||
|
handleSectionHeaderKeyDown,
|
||||||
PopOutButton,
|
PopOutButton,
|
||||||
sectionHeaderClassName,
|
sectionHeaderInteractiveClassName,
|
||||||
sectionHeaderLeftClassName,
|
sectionHeaderLeftClassName,
|
||||||
useSectionCollapsed,
|
useSectionCollapsed,
|
||||||
} from './CollapsibleSection'
|
} from './CollapsibleSection'
|
||||||
@@ -217,13 +218,20 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
|
|||||||
if (!activeProfile) {
|
if (!activeProfile) {
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className={sectionHeaderClassName}>
|
<div
|
||||||
|
className={sectionHeaderInteractiveClassName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
|
||||||
|
>
|
||||||
<div className={sectionHeaderLeftClassName}>
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Dashboards
|
Dashboards
|
||||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} />
|
||||||
<PopOutButton section="dashboards" range={range} />
|
<PopOutButton section="dashboards" range={range} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -236,13 +244,20 @@ export function Dashboards({ range }: { range: TimeRange | null }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className={sectionHeaderClassName}>
|
<div
|
||||||
|
className={sectionHeaderInteractiveClassName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
|
||||||
|
>
|
||||||
<div className={sectionHeaderLeftClassName}>
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Dashboards — profile "{activeProfile}"
|
Dashboards — profile "{activeProfile}"
|
||||||
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
<HelpIcon sectionId="dashboards" label="Help: Dashboards" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} />
|
||||||
<PopOutButton section="dashboards" range={range} />
|
<PopOutButton section="dashboards" range={range} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ export function HelpIcon({ sectionId, label }: { sectionId: string; label?: stri
|
|||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => openSection(sectionId)}
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
openSection(sectionId)
|
||||||
|
}}
|
||||||
title="Open docs"
|
title="Open docs"
|
||||||
aria-label={label ?? 'Open docs for this section'}
|
aria-label={label ?? 'Open docs for this section'}
|
||||||
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
className="flex h-5 w-5 shrink-0 items-center justify-center rounded-full border border-border-soft bg-surface-2 text-xs leading-none text-text-muted hover:border-border-strong hover:text-text-secondary"
|
||||||
|
|||||||
@@ -10,7 +10,14 @@ import {
|
|||||||
type MetricDeclaration,
|
type MetricDeclaration,
|
||||||
type MetricParamMatcher,
|
type MetricParamMatcher,
|
||||||
} from '../lib/api'
|
} from '../lib/api'
|
||||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
import {
|
||||||
|
CollapseToggle,
|
||||||
|
handleSectionHeaderKeyDown,
|
||||||
|
PopOutButton,
|
||||||
|
sectionHeaderInteractiveClassName,
|
||||||
|
sectionHeaderLeftClassName,
|
||||||
|
useSectionCollapsed,
|
||||||
|
} from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
const EMPTY_MATCHER: MetricParamMatcher = { param_name: '', capture_group: '', kind: 'string' }
|
||||||
@@ -151,13 +158,20 @@ export function Metrics({ range }: { range: TimeRange | null }) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="border-b border-border text-sm">
|
<div className="border-b border-border text-sm">
|
||||||
<div className={sectionHeaderClassName}>
|
<div
|
||||||
|
className={sectionHeaderInteractiveClassName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
|
||||||
|
>
|
||||||
<div className={sectionHeaderLeftClassName}>
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Metrics
|
Metrics
|
||||||
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
<HelpIcon sectionId="metrics" label="Help: Metrics" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} />
|
||||||
<PopOutButton section="metrics" range={range} />
|
<PopOutButton section="metrics" range={range} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,13 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
import { ApiError, declareParam, type ParamDeclaration } from '../lib/api'
|
||||||
import { CollapseToggle, PopOutButton, sectionHeaderClassName, sectionHeaderLeftClassName, useSectionCollapsed } from './CollapsibleSection'
|
import {
|
||||||
|
CollapseToggle,
|
||||||
|
handleSectionHeaderKeyDown,
|
||||||
|
PopOutButton,
|
||||||
|
sectionHeaderInteractiveClassName,
|
||||||
|
sectionHeaderLeftClassName,
|
||||||
|
useSectionCollapsed,
|
||||||
|
} from './CollapsibleSection'
|
||||||
import { HelpIcon } from './HelpIcon'
|
import { HelpIcon } from './HelpIcon'
|
||||||
|
|
||||||
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
const EMPTY_KIND: ParamDeclaration['kind'] = 'string'
|
||||||
@@ -34,13 +41,20 @@ 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 className={sectionHeaderClassName}>
|
<div
|
||||||
|
className={sectionHeaderInteractiveClassName}
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
aria-expanded={!collapsed}
|
||||||
|
onClick={() => setCollapsed(!collapsed)}
|
||||||
|
onKeyDown={handleSectionHeaderKeyDown(() => setCollapsed(!collapsed))}
|
||||||
|
>
|
||||||
<div className={sectionHeaderLeftClassName}>
|
<div className={sectionHeaderLeftClassName}>
|
||||||
Params
|
Params
|
||||||
<HelpIcon sectionId="parameters" label="Help: Params" />
|
<HelpIcon sectionId="parameters" label="Help: Params" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<CollapseToggle collapsed={collapsed} onToggle={() => setCollapsed(!collapsed)} />
|
<CollapseToggle collapsed={collapsed} />
|
||||||
<PopOutButton section="params" />
|
<PopOutButton section="params" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user