diff --git a/src/client/components/charts/UptimeBar.tsx b/src/client/components/charts/UptimeBar.tsx index d18eee0..8a60cd8 100644 --- a/src/client/components/charts/UptimeBar.tsx +++ b/src/client/components/charts/UptimeBar.tsx @@ -1,33 +1,81 @@ -import { useState } from 'react'; +import { useState, type ComponentProps } from 'react'; import { Bar, BarChart, Cell, XAxis, YAxis } from 'recharts'; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; import { Empty, EmptyDescription, EmptyTitle } from '@/components/ui/empty'; import type { Check } from '../../api/monitors'; type UptimeDatum = { - t: string; + startTime: string; + endTime: string; v: number; ok: boolean; - id: number; + id: string; + successfulChecks: number; + totalChecks: number; status: 'up' | 'down'; fill: string; }; +const MAX_VISIBLE_SEGMENTS = 32; + const uptimeConfig = { up: { label: 'Up', color: 'var(--primary)' }, down: { label: 'Down', color: 'var(--chart-down)' }, } satisfies ChartConfig; +function formatBucketTime(point: UptimeDatum) { + const start = new Date(point.startTime); + const end = new Date(point.endTime); + if (point.totalChecks === 1) return end.toLocaleString(); + + return new Intl.DateTimeFormat('en', { dateStyle: 'medium', timeStyle: 'short' }).formatRange(start, end); +} + +const formatUptimeTooltip: NonNullable['formatter']> = (_, __, item) => { + const point = item.payload as UptimeDatum; + return ( +
+ Successful + + {point.successfulChecks}/{point.totalChecks} + +
+ ); +}; + +function groupChecks(checks: Check[]): UptimeDatum[] { + const chronologicalChecks = checks.toReversed(); + const bucketCount = Math.min(chronologicalChecks.length, MAX_VISIBLE_SEGMENTS); + const buckets = Array.from({ length: bucketCount }, () => [] as Check[]); + + chronologicalChecks.forEach((check, index) => { + const bucketIndex = Math.min(bucketCount - 1, Math.floor((index * bucketCount) / chronologicalChecks.length)); + buckets[bucketIndex].push(check); + }); + + return buckets.map((bucket) => { + const first = bucket[0]; + const last = bucket[bucket.length - 1]; + const successfulChecks = bucket.filter((check) => check.ok).length; + const ok = successfulChecks === bucket.length; + + return { + startTime: first.checkedAt, + endTime: last.checkedAt, + v: 1, + ok, + id: `${first.id}-${last.id}`, + successfulChecks, + totalChecks: bucket.length, + status: ok ? 'up' : 'down', + fill: ok ? 'var(--color-up)' : 'var(--color-down)', + }; + }); +} + export function UptimeBar({ checks }: { checks: Check[] }) { const [hasAnimated, setHasAnimated] = useState(false); - const data: UptimeDatum[] = checks.toReversed().map((check) => ({ - t: check.checkedAt, - v: 1, - ok: check.ok, - id: check.id, - status: check.ok ? 'up' : 'down', - fill: check.ok ? 'var(--color-up)' : 'var(--color-down)', - })); + const data = groupChecks(checks); if (data.length === 0) return ( @@ -37,12 +85,12 @@ export function UptimeBar({ checks }: { checks: Check[] }) { ); - const successfulChecks = data.filter((check) => check.ok).length; + const successfulChecks = checks.filter((check) => check.ok).length; return (
-
+
- + new Date((payload[0].payload as UptimeDatum).t).toLocaleString()} + labelFormatter={(_, payload) => formatBucketTime(payload[0].payload as UptimeDatum)} + formatter={formatUptimeTooltip} /> } isAnimationActive={false} />