diff --git a/apps/web/src/components/reui-kit/health-source-tiles.tsx b/apps/web/src/components/reui-kit/health-source-tiles.tsx index b497fee..499c36e 100644 --- a/apps/web/src/components/reui-kit/health-source-tiles.tsx +++ b/apps/web/src/components/reui-kit/health-source-tiles.tsx @@ -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> + 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 ( + { + const values = next.filter((value): value is HealthProvider => + visible.some((item) => item.id === value), + ) + if (values.length === 0) return + onChange(values) + }} + > + {visible.map((item) => ( + + + {item.title} + + + ))} + + ) +} + /** * 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 ( {visible.map((item) => ( @@ -309,7 +379,7 @@ export function HealthProviderStatusTiles({ size="xs" /> } - onActivate={() => toggle(item.id)} + onActivate={() => onChange(toggleProviders(active, item.id))} /> ))} diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index 6d1dd03..8914a91 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -24,6 +24,7 @@ export { HealthSourceTiles, HealthAggregateTiles, HealthProviderStatusTiles, + HealthSourceFilterBar, type HealthProvider, type HealthAggregate, } from './health-source-tiles' diff --git a/apps/web/src/components/reui-kit/service-health-monitor.tsx b/apps/web/src/components/reui-kit/service-health-monitor.tsx index e3a6c5e..a017d71 100644 --- a/apps/web/src/components/reui-kit/service-health-monitor.tsx +++ b/apps/web/src/components/reui-kit/service-health-monitor.tsx @@ -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('5D') - const [source, setSource] = useState(ALL_TAB) + const [selected, setSelected] = useState(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 ( - -
= 4 && 'grid-cols-2', - )} - > - {showAllTab ? ( - } - 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 ( - setSource(item.id)} - /> - ) - })} -
-
+ + + ) } - -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 ( - - ) -}