From 7a8bacade932120ca94c9d7a14c72ffd2d2fc84a Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 20 Aug 2026 01:44:16 +0700 Subject: [PATCH] =?UTF-8?q?feat(health):=20=D0=BE=D0=B1=D1=8A=D0=B5=D0=B4?= =?UTF-8?q?=D0=B8=D0=BD=D0=B8=D1=82=D1=8C=20=D0=B8=D1=81=D1=82=D0=BE=D1=87?= =?UTF-8?q?=D0=BD=D0=B8=D0=BA=D0=B8,=20=D0=B3=D1=80=D0=B0=D1=84=D0=B8?= =?UTF-8?q?=D0=BA=20=D0=B8=20=D1=82=D0=B0=D0=B9=D0=BC=D0=BB=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD=20=D0=B2=20=D0=BE=D0=B4=D0=B8=D0=BD=20=D0=B1=D0=BB=D0=BE?= =?UTF-8?q?=D0=BA=20=D0=BC=D0=BE=D0=BD=D0=B8=D1=82=D0=BE=D1=80=D0=B8=D0=BD?= =?UTF-8?q?=D0=B3=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Переключатель серий в шапке Frame меняет график uptime и смены статуса без отдельного списка плиток. Co-authored-by: Cursor --- apps/web/src/components/reui-kit/index.ts | 2 +- .../reui-kit/service-health-monitor.tsx | 191 +++++++++++++++++- .../src/components/reui-kit/uptime-chart.tsx | 96 +++++---- .../_auth/services/$serviceId/index.tsx | 20 +- 4 files changed, 243 insertions(+), 66 deletions(-) diff --git a/apps/web/src/components/reui-kit/index.ts b/apps/web/src/components/reui-kit/index.ts index d2a46b1..6d1dd03 100644 --- a/apps/web/src/components/reui-kit/index.ts +++ b/apps/web/src/components/reui-kit/index.ts @@ -1,4 +1,4 @@ -export { UptimeChart, type UptimeProbe, type UptimePeriodKey } from './uptime-chart' +export { UptimeChart, type UptimeProbe, type UptimePeriodKey, probeUptimePercent, lastProbeLatency } from './uptime-chart' export { ServiceHealthMonitor } from './service-health-monitor' export { applyFiltersToData, getActiveFilters, renderSingleSelectedLabel } from './filter-utils' export { CertStatusChart, GroupDomainsChart } from './dashboard-analytics' 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 1e32972..abc2079 100644 --- a/apps/web/src/components/reui-kit/service-health-monitor.tsx +++ b/apps/web/src/components/reui-kit/service-health-monitor.tsx @@ -1,6 +1,8 @@ -import { useMemo, useState } from 'react' +import { useMemo, useState, type ReactNode } from 'react' +import { LayersIcon } from 'lucide-react' import { HealthTimeline } from '@/components/health/health-timeline' +import { HealthCheckBadge } from '@/components/health-check-badge' import { Frame, FrameDescription, @@ -8,41 +10,145 @@ import { FramePanel, FrameTitle, } from '@/components/reui/frame' -import { UptimeChart, UPTIME_PERIODS, type UptimePeriodKey } from '@/components/reui-kit/uptime-chart' +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 { collapseStatusChanges, filterByPeriod, filterByProviders, type HealthLogProbe, + type HealthLogStatus, } from '@/lib/health-log' +import { cn } from '@cfdm/ui/lib/utils' import type { HealthCheckProvider } from '@cfdm/shared' +type SourceTab = 'all' | HealthCheckProvider + +const ALL_TAB: SourceTab = 'all' + +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' +} + /** - * Combined uptime chart + status-change timeline (solution-analytics-8 DNA). - * Preview: https://reui.io/preview/base/solution-analytics-8 · https://reui.io/preview/base/chart-17 - * Docs: https://reui.io/docs/components/base/frame · https://reui.io/docs/components/base/timeline + * Единый блок мониторинга: переключатель источников (dashboard-4) + график + * (chart-17) + таймлайн смен статуса (solution-ai-ops-1 / timeline). + * + * Preview: https://reui.io/preview/base/dashboard-4 + * 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 + * Docs: https://reui.io/docs/components/base/icon-tile + * Docs: https://reui.io/docs/components/base/timeline + * Docs: https://reui.io/docs/components/base/badge */ export function ServiceHealthMonitor({ items, - selectedProviders, + enabledProviders, + statuses, isLoading = false, }: { items: HealthLogProbe[] - selectedProviders: readonly HealthCheckProvider[] + enabledProviders: readonly HealthCheckProvider[] + statuses: Partial> isLoading?: boolean }) { const [period, setPeriod] = useState('5D') + const [source, setSource] = useState(ALL_TAB) const days = UPTIME_PERIODS.find((entry) => entry.key === period)?.days ?? 5 + const enabledItems = useMemo( + () => HEALTH_PROVIDER_ITEMS.filter((item) => enabledProviders.includes(item.id)), + [enabledProviders], + ) + + const selectedProviders = useMemo(() => { + if (source === ALL_TAB) return [...enabledProviders] + if (enabledProviders.includes(source)) return [source] + return [...enabledProviders] + }, [enabledProviders, source]) + + const periodItems = useMemo( + () => filterByPeriod(items, days), + [items, days], + ) + const filtered = useMemo( - () => filterByProviders(filterByPeriod(items, days), selectedProviders), - [items, days, selectedProviders], + () => filterByProviders(periodItems, selectedProviders), + [periodItems, selectedProviders], ) 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)} + /> + ) + })} +
+
+ + Смены статуса - Только переходы up / degraded / down · Cloudflare = Worker, не Health Checks API + Только переходы up / degraded / down · Cloudflare = Worker, не Health + Checks API ) } + +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 ( + + ) +} diff --git a/apps/web/src/components/reui-kit/uptime-chart.tsx b/apps/web/src/components/reui-kit/uptime-chart.tsx index e968022..05e03cf 100644 --- a/apps/web/src/components/reui-kit/uptime-chart.tsx +++ b/apps/web/src/components/reui-kit/uptime-chart.tsx @@ -112,6 +112,23 @@ function UptimeDelta({ delta }: { delta: number }) { ) } +export function probeUptimePercent(items: UptimeProbe[]): number | null { + return uptimePercent(toSeries(items)) +} + +export function lastProbeLatency(items: UptimeProbe[]): number | null { + if (items.length === 0) return null + const latest = [...items].sort( + (a, b) => probeTime(b.checked_at) - probeTime(a.checked_at), + )[0] + return latest?.latency_ms ?? null +} + +function formatUptime(value: number | null): string { + if (value == null) return '—' + return `${value.toFixed(value >= 99.95 ? 2 : 1)}%` +} + interface UptimeChartProps { items: UptimeProbe[] isLoading?: boolean @@ -119,6 +136,8 @@ interface UptimeChartProps { onPeriodChange?: (period: UptimePeriodKey) => void skipPeriodFilter?: boolean embedded?: boolean + /** dashboard-4: chrome живёт в родительском FrameHeader (переключатель серий). */ + hideHeader?: boolean } export function UptimeChart({ @@ -128,6 +147,7 @@ export function UptimeChart({ onPeriodChange, skipPeriodFilter = false, embedded = false, + hideHeader = false, }: UptimeChartProps) { const gradientId = useId().replace(/:/g, '') const [internalPeriod, setInternalPeriod] = useState('5D') @@ -155,43 +175,45 @@ export function UptimeChart({ const panel = ( -
-
- -
-

Uptime

-

- Пробы health-check за период -

-
-
- - - - } + {hideHeader ? null : ( +
+
+
+ + +
+

Uptime

+

+ Пробы health-check за период +

+
+
+ + + + } + > + + +

Доля успешных проб и задержка (мс) по журналу health-log.

+
+
+
+
+ )} {isLoading ? (
@@ -207,7 +229,7 @@ export function UptimeChart({
- {uptime == null ? '—' : `${uptime.toFixed(uptime >= 99.95 ? 2 : 1)}%`} + {formatUptime(uptime)}
{delta == null ? ( diff --git a/apps/web/src/routes/_auth/services/$serviceId/index.tsx b/apps/web/src/routes/_auth/services/$serviceId/index.tsx index f160b1e..e3e3ea7 100644 --- a/apps/web/src/routes/_auth/services/$serviceId/index.tsx +++ b/apps/web/src/routes/_auth/services/$serviceId/index.tsx @@ -28,7 +28,6 @@ import { } from '@/components/services/service-detail-grid' import { LbModeTile } from '@/components/services/service-unit-card' import { - HealthProviderStatusTiles, KpiStatGrid, ServiceHealthMonitor, } from '@/components/reui-kit' @@ -45,7 +44,6 @@ import { providerHealthStatuses, } from '@/lib/health-log' import type { ServiceView, UpdateServiceConfigInput } from '@/lib/schemas' -import type { HealthCheckProvider } from '@cfdm/shared' import { createServiceNode, deleteServiceNode, @@ -112,9 +110,6 @@ function ServiceDetailPage() { const [editOpen, setEditOpen] = useState(false) const [saving, setSaving] = useState(false) - const [selectedProviders, setSelectedProviders] = useState< - HealthCheckProvider[] | null - >(null) const [togglingIp, setTogglingIp] = useState(null) const [changeIp, setChangeIp] = useState<{ bindingId: number @@ -234,11 +229,6 @@ function ServiceDetailPage() { () => enabledHealthProviders(service?.domains ?? []), [service], ) - const activeProviders = - selectedProviders?.filter((provider) => enabledProviders.includes(provider)) ?? - enabledProviders - const effectiveProviders = - activeProviders.length > 0 ? activeProviders : enabledProviders const providerStatuses = useMemo( () => providerHealthStatuses(logItems, enabledProviders), [logItems, enabledProviders], @@ -333,20 +323,14 @@ function ServiceDetailPage() { ]} /> - -