fix(health): заменить плитки источников на компактный мультивыбор
quality / commitlint (push) Skipped
CD / update-wiki (push) Successful in 7s
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / web (push) Successful in 54s
quality / api (push) Successful in 43s
CD / quality (push) Successful in 1m48s
CD / publish (push) Successful in 1m39s

Крупные KPI-кнопки и вкладка «Все» заменены на ToggleGroup: фильтр занимает меньше места и не сбрасывает последний источник.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Denozordec
2026-08-20 02:13:57 +07:00
co-authored by Cursor
parent 1bf6cfa0d0
commit 634a9dc362
3 changed files with 106 additions and 164 deletions
@@ -14,6 +14,10 @@ import {
} from '@cfdm/ui/components/item'
import { HealthCheckBadge } from '@/components/health-check-badge'
import { cn } from '@cfdm/ui/lib/utils'
import {
ToggleGroup,
ToggleGroupItem,
} from '@cfdm/ui/components/toggle-group'
import { parseHealthProviders, type HealthCheckAggregate, type HealthCheckProvider } from '@cfdm/shared'
import type { HealthLogStatus } from '@/lib/health-log'
@@ -261,6 +265,81 @@ export function HealthAggregateTiles({
)
}
function toggleProviders(
active: HealthProvider[],
id: HealthProvider,
): HealthProvider[] {
if (active.includes(id)) {
if (active.length === 1) return active
return active.filter((item) => item !== id)
}
return [...active, id]
}
/**
* Компактный мультивыбор типа пробы (toolbar в stacked Frame).
* Preview: https://reui.io/preview/base/list-9 · https://reui.io/preview/base/chart-17
* Docs: https://reui.io/docs/components/base/icon-tile · https://reui.io/docs/components/base/badge
*/
export function HealthSourceFilterBar({
enabled,
selected,
statuses,
onChange,
}: {
enabled: HealthProvider[]
selected: HealthProvider[]
statuses: Partial<Record<HealthProvider, HealthLogStatus>>
onChange: (next: HealthProvider[]) => void
}) {
const visible = HEALTH_PROVIDER_ITEMS.filter((item) => enabled.includes(item.id))
if (visible.length === 0) return null
const active = selected.length > 0 ? selected : enabled
return (
<ToggleGroup
multiple
variant="outline"
size="sm"
className="w-full min-w-0"
value={active}
aria-label="Тип пробы"
onValueChange={(next) => {
const values = next.filter((value): value is HealthProvider =>
visible.some((item) => item.id === value),
)
if (values.length === 0) return
onChange(values)
}}
>
{visible.map((item) => (
<ToggleGroupItem
key={item.id}
value={item.id}
aria-label={item.title}
className="min-w-0 flex-1 gap-1.5"
>
<IconTile
variant="elevated"
size="xs"
className={item.iconClassName}
aria-hidden="true"
>
{item.icon}
</IconTile>
<span className="truncate">{item.title}</span>
<HealthCheckBadge
status={statuses[item.id] ?? 'unknown'}
provider={item.id}
size="xs"
/>
</ToggleGroupItem>
))}
</ToggleGroup>
)
}
/**
* Read-only status tiles for enabled probe sources; click filters the monitor.
* Preview: https://reui.io/preview/base/list-9 · https://reui.io/preview/base/stats-12
@@ -282,15 +361,6 @@ export function HealthProviderStatusTiles({
const active = selected.length > 0 ? selected : enabled
function toggle(id: HealthProvider) {
if (active.includes(id)) {
if (active.length === 1) return
onChange(active.filter((item) => item !== id))
return
}
onChange([...active, id])
}
return (
<ChoiceFrame>
{visible.map((item) => (
@@ -309,7 +379,7 @@ export function HealthProviderStatusTiles({
size="xs"
/>
}
onActivate={() => toggle(item.id)}
onActivate={() => onChange(toggleProviders(active, item.id))}
/>
))}
</ChoiceFrame>
@@ -24,6 +24,7 @@ export {
HealthSourceTiles,
HealthAggregateTiles,
HealthProviderStatusTiles,
HealthSourceFilterBar,
type HealthProvider,
type HealthAggregate,
} from './health-source-tiles'
@@ -1,8 +1,6 @@
import { useMemo, useState, type ReactNode } from 'react'
import { LayersIcon } from 'lucide-react'
import { useMemo, useState } from 'react'
import { HealthTimeline } from '@/components/health/health-timeline'
import { HealthCheckBadge } from '@/components/health-check-badge'
import {
Frame,
FrameDescription,
@@ -10,16 +8,8 @@ import {
FramePanel,
FrameTitle,
} from '@/components/reui/frame'
import { IconTile } from '@/components/reui/icon-tile'
import { Badge } from '@/components/reui/badge'
import {
lastProbeLatency,
probeUptimePercent,
UptimeChart,
UPTIME_PERIODS,
type UptimePeriodKey,
} from '@/components/reui-kit/uptime-chart'
import { HEALTH_PROVIDER_ITEMS } from '@/components/reui-kit/health-source-tiles'
import { UptimeChart, UPTIME_PERIODS, type UptimePeriodKey } from '@/components/reui-kit/uptime-chart'
import { HealthSourceFilterBar } from '@/components/reui-kit/health-source-tiles'
import {
collapseStatusChanges,
filterByPeriod,
@@ -27,34 +17,13 @@ import {
type HealthLogProbe,
type HealthLogStatus,
} from '@/lib/health-log'
import { cn } from '@cfdm/ui/lib/utils'
import type { HealthCheckProvider } from '@cfdm/shared'
const ALL_TAB = 'all' as const
type SourceTab = typeof ALL_TAB | HealthCheckProvider
function formatUptime(value: number | null): string {
if (value == null) return '—'
return `${value.toFixed(value >= 99.95 ? 2 : 1)}%`
}
function formatLatency(ms: number | null): string {
if (ms == null) return 'нет проб'
return `${ms} мс`
}
function statusTileClass(status: HealthLogStatus | undefined): string {
if (status === 'down') return 'text-destructive'
if (status === 'degraded') return 'text-warning'
if (status === 'up') return 'text-success'
return 'text-muted-foreground'
}
/**
* Единый блок мониторинга: переключатель источников (dashboard-4) + график
* (chart-17) + таймлайн смен статуса (solution-ai-ops-1 / timeline).
* Единый блок мониторинга: компактный мультивыбор типа пробы (list-9 / ToggleGroup)
* + график (chart-17) + таймлайн смен статуса.
*
* Preview: https://reui.io/preview/base/dashboard-4
* Preview: https://reui.io/preview/base/list-9
* Preview: https://reui.io/preview/base/chart-17
* Preview: https://reui.io/preview/base/solution-ai-ops-1
* Docs: https://reui.io/docs/components/base/frame
@@ -74,19 +43,20 @@ export function ServiceHealthMonitor({
isLoading?: boolean
}) {
const [period, setPeriod] = useState<UptimePeriodKey>('5D')
const [source, setSource] = useState<SourceTab>(ALL_TAB)
const [selected, setSelected] = useState<HealthCheckProvider[] | null>(null)
const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5
const enabledItems = useMemo(
() => HEALTH_PROVIDER_ITEMS.filter((item) => enabledProviders.includes(item.id)),
const enabled = useMemo(
() => [...enabledProviders],
[enabledProviders],
)
const selectedProviders = useMemo((): HealthCheckProvider[] => {
if (source === ALL_TAB) return [...enabledProviders]
if (enabledProviders.includes(source)) return [source]
return [...enabledProviders]
}, [enabledProviders, source])
const activeProviders = useMemo((): HealthCheckProvider[] => {
const picked = (selected ?? enabled).filter((provider) =>
enabled.includes(provider),
)
return picked.length > 0 ? picked : enabled
}, [enabled, selected])
const periodItems = useMemo(
() => filterByPeriod(items, days),
@@ -94,59 +64,22 @@ export function ServiceHealthMonitor({
)
const filtered = useMemo(
() => filterByProviders(periodItems, selectedProviders),
[periodItems, selectedProviders],
() => filterByProviders(periodItems, activeProviders),
[periodItems, activeProviders],
)
const changes = useMemo(() => collapseStatusChanges(filtered), [filtered])
const showAllTab = enabledItems.length > 1
const tabCount = enabledItems.length + (showAllTab ? 1 : 0)
const activeSource: SourceTab =
source === ALL_TAB || enabledProviders.includes(source)
? source
: ALL_TAB
return (
<Frame stacked spacing="sm" className="min-w-0 w-full">
<FrameHeader className="p-0!">
<div
className={cn(
'grid',
tabCount <= 2 && 'grid-cols-2',
tabCount === 3 && 'grid-cols-1 sm:grid-cols-3',
tabCount >= 4 && 'grid-cols-2',
)}
>
{showAllTab ? (
<SourceMetricButton
selected={activeSource === ALL_TAB}
icon={<LayersIcon />}
iconClassName="text-muted-foreground"
label="Все источники"
value={formatUptime(probeUptimePercent(periodItems))}
hint={`${periodItems.length} проб`}
onSelect={() => setSource(ALL_TAB)}
/>
) : null}
{enabledItems.map((item) => {
const series = filterByProviders(periodItems, [item.id])
return (
<SourceMetricButton
key={item.id}
selected={activeSource === item.id}
icon={item.icon}
iconClassName={item.iconClassName}
label={item.title}
value={formatUptime(probeUptimePercent(series))}
hint={formatLatency(lastProbeLatency(series))}
status={statuses[item.id] ?? 'unknown'}
onSelect={() => setSource(item.id)}
/>
)
})}
</div>
</FrameHeader>
<FramePanel>
<HealthSourceFilterBar
enabled={enabled}
selected={activeProviders}
statuses={statuses}
onChange={setSelected}
/>
</FramePanel>
<UptimeChart
items={filtered}
@@ -185,65 +118,3 @@ export function ServiceHealthMonitor({
</Frame>
)
}
function SourceMetricButton({
selected,
icon,
iconClassName,
label,
value,
hint,
status,
onSelect,
}: {
selected: boolean
icon: ReactNode
iconClassName: string
label: string
value: string
hint: string
status?: HealthLogStatus
onSelect: () => void
}) {
return (
<button
type="button"
aria-pressed={selected}
aria-label={`${label}: ${value}`}
onClick={onSelect}
className={cn(
'focus-visible:ring-ring/50 hover:bg-muted/40 relative flex min-w-0 items-start gap-3 border-e border-b p-4 text-start transition-colors last:border-e-0 focus-visible:ring-2 focus-visible:outline-none sm:border-b-0',
selected && 'bg-muted/40',
)}
>
<IconTile
variant="elevated"
className={cn(
'size-10.5',
status ? statusTileClass(status) : iconClassName,
)}
aria-hidden="true"
>
{icon}
</IconTile>
<span className="flex min-w-0 flex-1 flex-col gap-0.5">
<span className="flex items-center justify-between gap-2">
<span className="text-muted-foreground text-sm font-medium">{label}</span>
{status ? (
<HealthCheckBadge status={status} size="xs" />
) : (
<Badge variant="outline" size="sm">
{hint}
</Badge>
)}
</span>
<span className="text-foreground text-2xl leading-none font-bold tabular-nums">
{value}
</span>
{status ? (
<span className="text-muted-foreground text-xs">{hint}</span>
) : null}
</span>
</button>
)
}