refactor: Enhance health-related components by integrating date formatting for last checked timestamps and improving tooltip display, ensuring better readability and user experience
Build, Test, and Push CFDM Docker Image / test (push) Successful in 4m0s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 2m5s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 7s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
Build, Test, and Push CFDM Docker Image / test (push) Successful in 4m0s
Build, Test, and Push CFDM Docker Image / build-and-push (push) Successful in 2m5s
Build, Test, and Push CFDM Docker Image / update-wiki (push) Successful in 7s
Build, Test, and Push CFDM Docker Image / create-release (push) Has been skipped
This commit is contained in:
@@ -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<IpHealthStatus['status'], string> = {
|
||||
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 (
|
||||
<TooltipProvider>
|
||||
@@ -61,7 +62,7 @@ function IpHealthDot({ health }: { health: IpHealthStatus }) {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{tooltipParts.join('\n')}</TooltipContent>
|
||||
<TooltipContent className="whitespace-pre-line">{tooltipParts.join('\n')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
@@ -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<IpHealthStatus['status'], string> = {
|
||||
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 (
|
||||
<TooltipProvider>
|
||||
@@ -68,7 +69,7 @@ function IpHealthDot({ health }: { health: IpHealthStatus }) {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<TooltipContent>{tooltipParts.join('\n')}</TooltipContent>
|
||||
<TooltipContent className="whitespace-pre-line">{tooltipParts.join('\n')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
@@ -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<VariantProps<typeof badgeVariants>['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}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{tooltipParts.join('\n')}</TooltipContent>
|
||||
<TooltipContent className="whitespace-pre-line">{tooltipParts.join('\n')}</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
)
|
||||
|
||||
@@ -101,6 +101,7 @@ export function ServiceGroupHeader({
|
||||
latencyMs={groupHealth.worstLatencyMs}
|
||||
lastCheckedAt={groupHealth.lastCheckedAt}
|
||||
lastError={groupHealth.lastError}
|
||||
title={column.domain ?? undefined}
|
||||
/>
|
||||
) : null}
|
||||
{!isOpen && isDragging && !dragDisabled ? (
|
||||
|
||||
@@ -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({
|
||||
</div>
|
||||
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{healthBinding && bindingHealth ? (
|
||||
<HealthCheckBadge
|
||||
status={bindingHealth.status}
|
||||
latencyMs={bindingHealth.worstLatencyMs}
|
||||
lastCheckedAt={bindingHealth.lastCheckedAt}
|
||||
lastError={bindingHealth.lastError}
|
||||
/>
|
||||
) : null}
|
||||
{healthBindings.map((binding) => (
|
||||
<BindingHealthBadge key={binding.binding_id} binding={binding} />
|
||||
))}
|
||||
{syncStatus ? <StatusBadge status={syncStatus} /> : null}
|
||||
{isToggling ? (
|
||||
<AppSpinner className="size-4" />
|
||||
@@ -216,3 +206,22 @@ export const ServiceRow = memo(function ServiceRow({
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
function BindingHealthBadge({ binding }: { binding: ServiceDomainBinding }) {
|
||||
const { data: health } = useAggregatedHealth(
|
||||
'binding',
|
||||
binding.binding_id,
|
||||
true,
|
||||
)
|
||||
if (!health) return null
|
||||
return (
|
||||
<HealthCheckBadge
|
||||
status={health.status}
|
||||
latencyMs={health.worstLatencyMs}
|
||||
lastCheckedAt={health.lastCheckedAt}
|
||||
lastError={health.lastError}
|
||||
title={bindingToFqdn(binding)}
|
||||
className={health.status === 'unknown' ? 'opacity-60' : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user