From 934afa5c955217d3bd5bf05dde1dff0b39ff837c Mon Sep 17 00:00:00 2001 From: Denozordec Date: Thu, 25 Jun 2026 23:12:20 +0700 Subject: [PATCH] refactor: Enhance health-related components by integrating date formatting for last checked timestamps and improving tooltip display, ensuring better readability and user experience --- apps/web/src/components/dns-records-table.tsx | 5 ++- .../src/components/domain-bindings-card.tsx | 5 ++- .../web/src/components/health-check-badge.tsx | 11 +++-- .../services-board/service-group-header.tsx | 1 + .../components/services-board/service-row.tsx | 41 +++++++++++-------- apps/web/src/lib/format.ts | 16 +++++++- apps/web/src/lib/use-aggregated-health.ts | 6 ++- 7 files changed, 59 insertions(+), 26 deletions(-) diff --git a/apps/web/src/components/dns-records-table.tsx b/apps/web/src/components/dns-records-table.tsx index 81b3c4a..437588c 100644 --- a/apps/web/src/components/dns-records-table.tsx +++ b/apps/web/src/components/dns-records-table.tsx @@ -3,6 +3,7 @@ import { ConfirmDialog } from '@/components/confirm-dialog' import { TableCard } from '@/components/table-card' import { groupDnsRecords, type DnsRecordGroup } from '@/lib/dns-grouping' import type { DnsRecord, IpHealthStatus } from '@/lib/schemas' +import { formatDate } from '@/lib/format' import { AppBadge } from '@/components/app-badge' import { AppButton } from '@/components/app-button' import { @@ -45,7 +46,7 @@ const healthLabel: Record = { function IpHealthDot({ health }: { health: IpHealthStatus }) { const tooltipParts: string[] = [`Статус: ${healthLabel[health.status]}`] if (health.latency_ms != null) tooltipParts.push(`Задержка: ${health.latency_ms} мс`) - if (health.last_checked_at) tooltipParts.push(`Проверка: ${health.last_checked_at}`) + if (health.last_checked_at) tooltipParts.push(`Проверка: ${formatDate(health.last_checked_at)}`) if (health.last_error) tooltipParts.push(`Ошибка: ${health.last_error}`) return ( @@ -61,7 +62,7 @@ function IpHealthDot({ health }: { health: IpHealthStatus }) { /> } /> - {tooltipParts.join('\n')} + {tooltipParts.join('\n')} ) diff --git a/apps/web/src/components/domain-bindings-card.tsx b/apps/web/src/components/domain-bindings-card.tsx index f7abac8..4c5547c 100644 --- a/apps/web/src/components/domain-bindings-card.tsx +++ b/apps/web/src/components/domain-bindings-card.tsx @@ -4,6 +4,7 @@ import { EmptyState } from '@/components/empty-state' import { StatusBadge } from '@/components/status-badge' import { groupBindingsByHostname } from '@/lib/domain-ips' import { useHealthRows } from '@/lib/use-aggregated-health' +import { formatDate } from '@/lib/format' import type { IpHealthStatus, ServiceBinding } from '@/lib/schemas' import { AppBadge } from '@/components/app-badge' import { AppButton } from '@/components/app-button' @@ -52,7 +53,7 @@ const healthLabel: Record = { function IpHealthDot({ health }: { health: IpHealthStatus }) { const tooltipParts: string[] = [`Статус: ${healthLabel[health.status]}`] if (health.latency_ms != null) tooltipParts.push(`Задержка: ${health.latency_ms} мс`) - if (health.last_checked_at) tooltipParts.push(`Проверка: ${health.last_checked_at}`) + if (health.last_checked_at) tooltipParts.push(`Проверка: ${formatDate(health.last_checked_at)}`) if (health.last_error) tooltipParts.push(`Ошибка: ${health.last_error}`) return ( @@ -68,7 +69,7 @@ function IpHealthDot({ health }: { health: IpHealthStatus }) { /> } /> - {tooltipParts.join('\n')} + {tooltipParts.join('\n')} ) diff --git a/apps/web/src/components/health-check-badge.tsx b/apps/web/src/components/health-check-badge.tsx index f4e39fb..47d3261 100644 --- a/apps/web/src/components/health-check-badge.tsx +++ b/apps/web/src/components/health-check-badge.tsx @@ -3,6 +3,7 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@cfdm/ import type { VariantProps } from 'class-variance-authority' import { cn } from '@cfdm/ui/lib/utils' import type { IpHealthStatus } from '@/lib/schemas' +import { formatDate } from '@/lib/format' type BadgeVariant = NonNullable['variant']> @@ -25,6 +26,7 @@ interface HealthCheckBadgeProps { latencyMs?: number | null lastCheckedAt?: string | null lastError?: string | null + title?: string className?: string } @@ -33,14 +35,17 @@ export function HealthCheckBadge({ latencyMs, lastCheckedAt, lastError, + title, className, }: HealthCheckBadgeProps) { const variant = healthVariants[status] const label = healthLabels[status] - const tooltipParts: string[] = [`Статус: ${label}`] + const tooltipParts: string[] = [] + if (title) tooltipParts.push(title) + tooltipParts.push(`Статус: ${label}`) if (latencyMs != null) tooltipParts.push(`Задержка: ${latencyMs} мс`) - if (lastCheckedAt) tooltipParts.push(`Проверка: ${lastCheckedAt}`) + if (lastCheckedAt) tooltipParts.push(`Проверка: ${formatDate(lastCheckedAt)}`) if (lastError) tooltipParts.push(`Ошибка: ${lastError}`) return ( @@ -59,7 +64,7 @@ export function HealthCheckBadge({ {label} - {tooltipParts.join('\n')} + {tooltipParts.join('\n')} ) diff --git a/apps/web/src/components/services-board/service-group-header.tsx b/apps/web/src/components/services-board/service-group-header.tsx index 4a3dc53..9733a76 100644 --- a/apps/web/src/components/services-board/service-group-header.tsx +++ b/apps/web/src/components/services-board/service-group-header.tsx @@ -101,6 +101,7 @@ export function ServiceGroupHeader({ latencyMs={groupHealth.worstLatencyMs} lastCheckedAt={groupHealth.lastCheckedAt} lastError={groupHealth.lastError} + title={column.domain ?? undefined} /> ) : null} {!isOpen && isDragging && !dragDisabled ? ( diff --git a/apps/web/src/components/services-board/service-row.tsx b/apps/web/src/components/services-board/service-row.tsx index 15db1d9..f185803 100644 --- a/apps/web/src/components/services-board/service-row.tsx +++ b/apps/web/src/components/services-board/service-row.tsx @@ -10,7 +10,7 @@ import { aggregateServiceSyncStatus, serviceDisplayFqdn, } from '@/lib/service-utils' -import type { ServiceView } from '@/lib/schemas' +import type { ServiceDomainBinding, ServiceView } from '@/lib/schemas' import { AppBadge } from '@/components/app-badge' import { AppButton } from '@/components/app-button' import { AppCheckbox } from '@/components/app-checkbox' @@ -72,13 +72,8 @@ export const ServiceRow = memo(function ServiceRow({ const syncStatus = aggregateServiceSyncStatus(service) const fqdn = serviceDisplayFqdn(service) const allFqdns = (service.domains ?? []).map((d) => bindingToFqdn(d)) - const healthBinding = (service.domains ?? []).find( - (d) => d.record_type === 'A' && (d.target_ips?.length ?? 0) > 1 && d.health_check_enabled, - ) - const { data: bindingHealth } = useAggregatedHealth( - 'binding', - healthBinding?.binding_id, - Boolean(healthBinding), + const healthBindings = (service.domains ?? []).filter( + (d) => d.record_type === 'A' && (d.target_ips?.length ?? 0) > 0 && d.health_check_enabled, ) const style = transform @@ -163,14 +158,9 @@ export const ServiceRow = memo(function ServiceRow({
- {healthBinding && bindingHealth ? ( - - ) : null} + {healthBindings.map((binding) => ( + + ))} {syncStatus ? : null} {isToggling ? ( @@ -216,3 +206,22 @@ export const ServiceRow = memo(function ServiceRow({
) }) + +function BindingHealthBadge({ binding }: { binding: ServiceDomainBinding }) { + const { data: health } = useAggregatedHealth( + 'binding', + binding.binding_id, + true, + ) + if (!health) return null + return ( + + ) +} diff --git a/apps/web/src/lib/format.ts b/apps/web/src/lib/format.ts index 82e093e..4d8090c 100644 --- a/apps/web/src/lib/format.ts +++ b/apps/web/src/lib/format.ts @@ -5,9 +5,23 @@ const dateFormatter = new Intl.DateTimeFormat('ru-RU', { const relativeFormatter = new Intl.RelativeTimeFormat('ru', { numeric: 'auto' }) +/** + * SQLite `datetime('now')` возвращает UTC в формате `YYYY-MM-DD HH:MM:SS` без суффикса `Z`. + * `new Date(...)` парсит такую строку как локальное время — отображение уезжает на TZ-сдвиг. + * Конвертируем в полноценный ISO с `Z`, чтобы `new Date(...)` трактовал время как UTC. + */ +export function sqliteUtcToIso(value: string | null | undefined): string | null { + if (!value) return null + const trimmed = value.trim() + if (trimmed.endsWith('Z') || /[+-]\d{2}:?\d{2}$/.test(trimmed)) return trimmed + const match = /^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})/.exec(trimmed) + if (!match) return trimmed + return `${match[1]}T${match[2]}Z` +} + export function formatDate(iso: string | null | undefined): string { if (!iso) return '—' - const date = new Date(iso) + const date = new Date(sqliteUtcToIso(iso) ?? iso) if (Number.isNaN(date.getTime())) return iso return dateFormatter.format(date) } diff --git a/apps/web/src/lib/use-aggregated-health.ts b/apps/web/src/lib/use-aggregated-health.ts index c239012..ed39676 100644 --- a/apps/web/src/lib/use-aggregated-health.ts +++ b/apps/web/src/lib/use-aggregated-health.ts @@ -1,5 +1,6 @@ import { useQuery } from '@tanstack/react-query' import { healthStatusQueryOptions } from '@/queries' +import { sqliteUtcToIso } from '@/lib/format' import type { IpHealthStatus } from '@/lib/schemas' export type HealthScope = 'binding' | 'group' @@ -60,8 +61,9 @@ export function aggregateHealth(rows: IpHealthStatus[]): AggregatedHealth { } } if (row.last_checked_at) { - if (!lastCheckedAt || row.last_checked_at > lastCheckedAt) { - lastCheckedAt = row.last_checked_at + const iso = sqliteUtcToIso(row.last_checked_at) ?? row.last_checked_at + if (!lastCheckedAt || iso > lastCheckedAt) { + lastCheckedAt = iso } } if (row.last_error && !lastError) lastError = row.last_error