fix: improve the ui for recent uptime section

This commit is contained in:
2026-08-29 18:37:43 +07:00
parent 6998f83201
commit 621afe1124
2 changed files with 68 additions and 18 deletions
+65 -16
View File
@@ -1,33 +1,81 @@
import { useState } from 'react'; import { useState, type ComponentProps } from 'react';
import { Bar, BarChart, Cell, XAxis, YAxis } from 'recharts'; import { Bar, BarChart, Cell, XAxis, YAxis } from 'recharts';
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart'; import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from '@/components/ui/chart';
import { Empty, EmptyDescription, EmptyTitle } from '@/components/ui/empty'; import { Empty, EmptyDescription, EmptyTitle } from '@/components/ui/empty';
import type { Check } from '../../api/monitors'; import type { Check } from '../../api/monitors';
type UptimeDatum = { type UptimeDatum = {
t: string; startTime: string;
endTime: string;
v: number; v: number;
ok: boolean; ok: boolean;
id: number; id: string;
successfulChecks: number;
totalChecks: number;
status: 'up' | 'down'; status: 'up' | 'down';
fill: string; fill: string;
}; };
const MAX_VISIBLE_SEGMENTS = 32;
const uptimeConfig = { const uptimeConfig = {
up: { label: 'Up', color: 'var(--primary)' }, up: { label: 'Up', color: 'var(--primary)' },
down: { label: 'Down', color: 'var(--chart-down)' }, down: { label: 'Down', color: 'var(--chart-down)' },
} satisfies ChartConfig; } 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<ComponentProps<typeof ChartTooltipContent>['formatter']> = (_, __, item) => {
const point = item.payload as UptimeDatum;
return (
<div className="flex w-full items-center justify-between gap-5">
<span className="text-[#777]">Successful</span>
<span className="font-mono font-medium tabular-nums text-[#171717]">
{point.successfulChecks}/{point.totalChecks}
</span>
</div>
);
};
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[] }) { export function UptimeBar({ checks }: { checks: Check[] }) {
const [hasAnimated, setHasAnimated] = useState(false); const [hasAnimated, setHasAnimated] = useState(false);
const data: UptimeDatum[] = checks.toReversed().map((check) => ({ const data = groupChecks(checks);
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)',
}));
if (data.length === 0) if (data.length === 0)
return ( return (
@@ -37,12 +85,12 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
</Empty> </Empty>
); );
const successfulChecks = data.filter((check) => check.ok).length; const successfulChecks = checks.filter((check) => check.ok).length;
return ( return (
<div> <div>
<div className="uptime-bar" role="img" aria-label={`${successfulChecks} of ${data.length} recent checks succeeded`}> <div className="uptime-bar" role="img" aria-label={`${successfulChecks} of ${checks.length} recent checks succeeded`}>
<ChartContainer config={uptimeConfig} className="h-full w-full"> <ChartContainer config={uptimeConfig} className="h-full w-full">
<BarChart data={data} barCategoryGap={2} margin={{ top: 10, right: 0, bottom: 0, left: 0 }}> <BarChart data={data} barCategoryGap="24%" margin={{ top: 6, right: 0, bottom: 0, left: 0 }}>
<XAxis dataKey="id" hide /> <XAxis dataKey="id" hide />
<YAxis hide domain={[0, 1]} /> <YAxis hide domain={[0, 1]} />
<ChartTooltip <ChartTooltip
@@ -51,14 +99,15 @@ export function UptimeBar({ checks }: { checks: Check[] }) {
className="min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm" className="min-w-36 gap-1.5 rounded-[6px] border-(--hairline) bg-white/97 px-3 py-2.5 shadow-[0_8px_24px_rgb(24_74_52/0.1),0_2px_6px_rgb(0_0_0/0.04)] backdrop-blur-sm"
hideIndicator={false} hideIndicator={false}
nameKey="status" nameKey="status"
labelFormatter={(_, payload) => new Date((payload[0].payload as UptimeDatum).t).toLocaleString()} labelFormatter={(_, payload) => formatBucketTime(payload[0].payload as UptimeDatum)}
formatter={formatUptimeTooltip}
/> />
} }
isAnimationActive={false} isAnimationActive={false}
/> />
<Bar <Bar
dataKey="v" dataKey="v"
radius={[2, 2, 0, 0]} radius={[3, 3, 1, 1]}
isAnimationActive={!hasAnimated} isAnimationActive={!hasAnimated}
animationDuration={700} animationDuration={700}
animationEasing="ease-out" animationEasing="ease-out"
+3 -2
View File
@@ -1154,8 +1154,9 @@ button {
min-height: 330px; min-height: 330px;
} }
.uptime-bar { .uptime-bar {
height: 132px; height: 148px;
padding: 28px 22px 14px; padding: 24px 22px 18px;
background: linear-gradient(180deg, #fcfefd 0%, #fff 100%);
} }
.uptime-legend { .uptime-legend {
display: flex; display: flex;