import { useMemo } from 'react'; import { useGlobalFilters, Tooltip } from '@cameleer/design-system'; import { useExecutionStats } from '../api/queries/executions'; import type { Scope } from '../hooks/useScope'; import { formatPercent } from '../utils/format-utils'; import styles from './TabKpis.module.css'; interface TabKpisProps { scope: Scope; } function formatNum(n: number): string { if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`; if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`; return n.toLocaleString(); } function formatMs(ms: number): string { if (ms >= 60_000) return `${(ms / 1000).toFixed(0)}s`; if (ms >= 1000) return `${(ms / 1000).toFixed(1)}s`; return `${Math.round(ms)}ms`; } type Trend = 'up' | 'down' | 'flat'; function trend(current: number, previous: number): Trend { if (current > previous) return 'up'; if (current < previous) return 'down'; return 'flat'; } function trendArrow(t: Trend): string { switch (t) { case 'up': return '\u2191'; case 'down': return '\u2193'; case 'flat': return '\u2192'; } } function changePercent(current: number, previous: number): string | null { if (previous === 0 && current === 0) return null; if (previous === 0) return '+\u221e%'; const pct = ((current - previous) / previous) * 100; const sign = pct > 0 ? '+' : ''; return `${sign}${pct.toFixed(1)}%`; } /* ── Time period labels ───────────────────────────── */ function shortTime(d: Date): string { const h = String(d.getHours()).padStart(2, '0'); const m = String(d.getMinutes()).padStart(2, '0'); return `${h}:${m}`; } function shortDate(d: Date): string { const months = ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec']; return `${months[d.getMonth()]} ${d.getDate()}`; } function formatRange(start: Date, end: Date): string { const sameDay = start.toDateString() === end.toDateString(); if (sameDay) return `${shortDate(start)} ${shortTime(start)}\u2013${shortTime(end)}`; return `${shortDate(start)} ${shortTime(start)} \u2013 ${shortDate(end)} ${shortTime(end)}`; } function computePreviousPeriod(start: Date, end: Date): { label: string; prevLabel: string } { const durationMs = end.getTime() - start.getTime(); const prevStart = new Date(start.getTime() - durationMs); const prevEnd = new Date(start.getTime()); return { label: formatRange(start, end), prevLabel: formatRange(prevStart, prevEnd), }; } /* ── Metric model ─────────────────────────────────── */ interface Metric { label: string; fullLabel: string; value: string; prevValue: string; change: string | null; trend: Trend; upIsBad?: boolean; } /* ── Tooltip ──────────────────────────────────────── */ function MetricTooltip({ m, currentLabel, prevLabel }: { m: Metric; currentLabel: string; prevLabel: string }) { const trendClass = m.trend === 'flat' ? styles.flat : (m.trend === 'up') === m.upIsBad ? styles.bad : styles.good; return (
{m.fullLabel}
Now {currentLabel}
{m.value}
Prev {prevLabel}
{m.prevValue}
{m.change && (
{trendArrow(m.trend)} {m.change}
)}
); } /* ── Main component ───────────────────────────────── */ export function TabKpis({ scope }: TabKpisProps) { const { timeRange } = useGlobalFilters(); const timeFrom = timeRange.start.toISOString(); const timeTo = timeRange.end.toISOString(); const { data: stats } = useExecutionStats( timeFrom, timeTo, scope.routeId, scope.appId, ); const { label: currentLabel, prevLabel } = useMemo( () => computePreviousPeriod(timeRange.start, timeRange.end), [timeRange.start, timeRange.end], ); const metrics: Metric[] = useMemo(() => { if (!stats) return []; const total = stats.totalCount ?? 0; const failed = stats.failedCount ?? 0; const prevTotal = stats.prevTotalCount ?? 0; const prevFailed = stats.prevFailedCount ?? 0; const errorRate = total > 0 ? (failed / total) * 100 : 0; const prevErrorRate = prevTotal > 0 ? (prevFailed / prevTotal) * 100 : 0; const avgMs = stats.avgDurationMs ?? 0; const prevAvgMs = stats.prevAvgDurationMs ?? 0; const p99 = stats.p99LatencyMs ?? 0; const prevP99 = stats.prevP99LatencyMs ?? 0; return [ { label: 'Total', fullLabel: 'Total Exchanges', value: formatNum(total), prevValue: formatNum(prevTotal), change: changePercent(total, prevTotal), trend: trend(total, prevTotal) }, { label: 'Err%', fullLabel: 'Error Rate', value: formatPercent(errorRate), prevValue: formatPercent(prevErrorRate), change: changePercent(errorRate, prevErrorRate), trend: trend(errorRate, prevErrorRate), upIsBad: true }, { label: 'Avg', fullLabel: 'Avg Latency', value: formatMs(avgMs), prevValue: formatMs(prevAvgMs), change: changePercent(avgMs, prevAvgMs), trend: trend(avgMs, prevAvgMs), upIsBad: true }, { label: 'P99', fullLabel: 'P99 Latency', value: formatMs(p99), prevValue: formatMs(prevP99), change: changePercent(p99, prevP99), trend: trend(p99, prevP99), upIsBad: true }, ]; }, [stats]); if (metrics.length === 0) return null; return (
{metrics.map((m, i) => { const arrowClass = m.trend === 'flat' ? styles.flat : (m.trend === 'up') === m.upIsBad ? styles.bad : styles.good; const isNearEnd = i >= metrics.length - 2; return ( } position="bottom" className={`${styles.tooltipWrap} ${isNearEnd ? styles.tooltipEnd : ''}`} >
{m.label} {m.value} {trendArrow(m.trend)}
); })}
); }