import { useState, useCallback, useRef, useEffect, useMemo } from 'react'; import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import type { ServiceHealthData, StatusBlockDetail } from '@/utils/usage'; import type { UsageOverviewPayload } from './hooks/useUsageData'; import styles from '@/pages/UsagePage.module.scss'; const COLOR_STOPS = [ { r: 239, g: 68, b: 68 }, // #ef4444 { r: 250, g: 204, b: 21 }, // #facc15 { r: 34, g: 197, b: 94 }, // #22c55e ] as const; const TOOLTIP_OFFSET = 8; const TOOLTIP_SAFE_WIDTH = 180; const TOOLTIP_SAFE_HEIGHT = 72; type TooltipHorizontalPosition = 'center' | 'left' | 'right'; type TooltipVerticalPosition = 'above' | 'below'; interface ActiveTooltipState { idx: number; anchorEl: HTMLDivElement; horizontal: TooltipHorizontalPosition; vertical: TooltipVerticalPosition; left: number; top: number; transform: string; } function rateToColor(rate: number): string { const t = Math.max(0, Math.min(1, rate)); const segment = t < 0.5 ? 0 : 1; const localT = segment === 0 ? t * 2 : (t - 0.5) * 2; const from = COLOR_STOPS[segment]; const to = COLOR_STOPS[segment + 1]; const r = Math.round(from.r + (to.r - from.r) * localT); const g = Math.round(from.g + (to.g - from.g) * localT); const b = Math.round(from.b + (to.b - from.b) * localT); return `rgb(${r}, ${g}, ${b})`; } function formatDateTime(timestamp: number): string { const date = new Date(timestamp); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); const h = date.getHours().toString().padStart(2, '0'); const m = date.getMinutes().toString().padStart(2, '0'); return `${month}/${day} ${h}:${m}`; } function parseTime(value?: string): number { if (!value) return 0; const parsed = Date.parse(value); return Number.isFinite(parsed) ? parsed : 0; } function ServiceHealthTitle({ title, subtitle, eyebrow }: { title: string; subtitle: string; eyebrow: string }) { return (
{eyebrow}

{title}

{subtitle}

); } export interface ServiceHealthCardProps { usage: UsageOverviewPayload | null; loading: boolean; } export function ServiceHealthCard({ usage, loading }: ServiceHealthCardProps) { const { t } = useTranslation(); const [activeTooltip, setActiveTooltip] = useState(null); const gridRef = useRef(null); const healthData: ServiceHealthData = useMemo(() => { const blockDetails = (usage?.service_health?.block_details ?? []).map((block) => ({ startTime: Date.parse(block.start_time), endTime: Date.parse(block.end_time), success: Number(block.success ?? 0), failure: Number(block.failure ?? 0), rate: Number(block.rate ?? -1), })); const rows = Number(usage?.service_health?.rows ?? 7) || 7; return { totalSuccess: Number(usage?.service_health?.total_success ?? 0), totalFailure: Number(usage?.service_health?.total_failure ?? 0), successRate: Number(usage?.service_health?.success_rate ?? 0), rows, columns: Number(usage?.service_health?.columns ?? Math.max(1, Math.ceil(blockDetails.length / rows))) || 1, bucketSeconds: Number(usage?.service_health?.bucket_seconds ?? 0), windowStart: parseTime(usage?.service_health?.window_start), windowEnd: parseTime(usage?.service_health?.window_end), blockDetails, }; }, [usage]); const hasData = healthData.totalSuccess + healthData.totalFailure > 0; useEffect(() => { if (activeTooltip === null) return; const handler = (e: PointerEvent) => { if (gridRef.current && !gridRef.current.contains(e.target as Node)) { setActiveTooltip(null); } }; document.addEventListener('pointerdown', handler); return () => document.removeEventListener('pointerdown', handler); }, [activeTooltip]); const buildTooltipState = useCallback( (idx: number, anchorEl: HTMLDivElement | null): ActiveTooltipState | null => { if (!anchorEl || !anchorEl.isConnected) { return null; } const rect = anchorEl.getBoundingClientRect(); const centerX = rect.left + rect.width / 2; let horizontal: TooltipHorizontalPosition = 'center'; let left = centerX; if (centerX <= TOOLTIP_SAFE_WIDTH / 2) { horizontal = 'left'; left = rect.left; } else if (centerX >= window.innerWidth - TOOLTIP_SAFE_WIDTH / 2) { horizontal = 'right'; left = rect.right; } const vertical: TooltipVerticalPosition = rect.top <= TOOLTIP_SAFE_HEIGHT ? 'below' : 'above'; const top = vertical === 'below' ? rect.bottom + TOOLTIP_OFFSET : rect.top - TOOLTIP_OFFSET; const translateX = horizontal === 'center' ? '-50%' : horizontal === 'right' ? '-100%' : '0'; const translateY = vertical === 'below' ? '0' : '-100%'; return { idx, anchorEl, horizontal, vertical, left: Math.round(left), top: Math.round(top), transform: `translate(${translateX}, ${translateY})`, }; }, [] ); useEffect(() => { if (!activeTooltip) return; const updateTooltipPosition = () => { if (!document.body.contains(activeTooltip.anchorEl)) { setActiveTooltip(null); return; } setActiveTooltip(buildTooltipState(activeTooltip.idx, activeTooltip.anchorEl)); }; window.addEventListener('resize', updateTooltipPosition); window.addEventListener('scroll', updateTooltipPosition, true); return () => { window.removeEventListener('resize', updateTooltipPosition); window.removeEventListener('scroll', updateTooltipPosition, true); }; }, [activeTooltip, buildTooltipState]); const openTooltip = useCallback( (idx: number, anchorEl: HTMLDivElement) => { setActiveTooltip(buildTooltipState(idx, anchorEl)); }, [buildTooltipState] ); const handlePointerEnter = useCallback( (e: React.PointerEvent, idx: number) => { if (e.pointerType === 'mouse') { openTooltip(idx, e.currentTarget); } }, [openTooltip] ); const handlePointerLeave = useCallback((e: React.PointerEvent) => { if (e.pointerType === 'mouse') { setActiveTooltip(null); } }, []); const handlePointerDown = useCallback( (e: React.PointerEvent, idx: number) => { if (e.pointerType === 'touch') { e.preventDefault(); const anchorEl = e.currentTarget; setActiveTooltip((prev) => (prev?.idx === idx ? null : buildTooltipState(idx, anchorEl))); } }, [buildTooltipState] ); const renderTooltip = (detail: StatusBlockDetail, tooltipState: ActiveTooltipState) => { const total = detail.success + detail.failure; const posClass = tooltipState.horizontal === 'left' ? styles.healthTooltipLeft : tooltipState.horizontal === 'right' ? styles.healthTooltipRight : ''; const vertClass = tooltipState.vertical === 'below' ? styles.healthTooltipBelow : ''; const timeRange = `${formatDateTime(detail.startTime)} – ${formatDateTime(detail.endTime)}`; const tooltip = (
{timeRange} {total > 0 ? ( {t('status_bar.success_short')} {detail.success} {t('status_bar.failure_short')} {detail.failure} ({(detail.rate * 100).toFixed(1)}%) ) : ( {t('status_bar.no_requests')} )}
); return typeof document === 'undefined' ? tooltip : createPortal(tooltip, document.body); }; const rateClass = !hasData ? '' : healthData.successRate >= 90 ? styles.healthRateHigh : healthData.successRate >= 50 ? styles.healthRateMedium : styles.healthRateLow; const windowLabel = healthData.windowStart > 0 && healthData.windowEnd > 0 ? `${formatDateTime(healthData.windowStart)} – ${formatDateTime(healthData.windowEnd)}` : t('usage_stats.service_health_window'); const healthCountsLabel = `${t('status_bar.success_short')} ${healthData.totalSuccess}, ${t('status_bar.failure_short')} ${healthData.totalFailure}`; const gridStyle = useMemo( () => ({ '--health-grid-columns': String(healthData.columns), '--health-grid-rows': String(healthData.rows), '--health-grid-aspect-columns': String(healthData.columns), '--health-grid-aspect-rows': String(healthData.rows), '--health-grid-width': '100%', }) as React.CSSProperties, [healthData.columns, healthData.rows] ); return (
{windowLabel} {loading ? '--' : hasData ? `${healthData.successRate.toFixed(1)}%` : '--'}
{healthData.blockDetails.map((detail, idx) => { const isIdle = detail.rate === -1; const blockStyle = isIdle ? undefined : { backgroundColor: rateToColor(detail.rate) }; const isActive = activeTooltip?.idx === idx; const timeRange = `${formatDateTime(detail.startTime)} – ${formatDateTime(detail.endTime)}`; const summary = detail.success + detail.failure > 0 ? `${t('status_bar.success_short')} ${detail.success}, ${t('status_bar.failure_short')} ${detail.failure}` : t('status_bar.no_requests'); return (
openTooltip(idx, e.currentTarget)} onBlur={() => setActiveTooltip(null)} onPointerEnter={(e) => handlePointerEnter(e, idx)} onPointerLeave={handlePointerLeave} onPointerDown={(e) => handlePointerDown(e, idx)} >
{isActive && activeTooltip && renderTooltip(detail, activeTooltip)}
); })}
{t('usage_stats.service_health_oldest')}
{t('usage_stats.service_health_newest')}
); }