mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 05:41:59 +00:00
fix: improve the ui for recent uptime section
This commit is contained in:
@@ -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<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[] }) {
|
||||
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[] }) {
|
||||
</Empty>
|
||||
);
|
||||
|
||||
const successfulChecks = data.filter((check) => check.ok).length;
|
||||
const successfulChecks = checks.filter((check) => check.ok).length;
|
||||
return (
|
||||
<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">
|
||||
<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 />
|
||||
<YAxis hide domain={[0, 1]} />
|
||||
<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"
|
||||
hideIndicator={false}
|
||||
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}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="v"
|
||||
radius={[2, 2, 0, 0]}
|
||||
radius={[3, 3, 1, 1]}
|
||||
isAnimationActive={!hasAnimated}
|
||||
animationDuration={700}
|
||||
animationEasing="ease-out"
|
||||
|
||||
@@ -1154,8 +1154,9 @@ button {
|
||||
min-height: 330px;
|
||||
}
|
||||
.uptime-bar {
|
||||
height: 132px;
|
||||
padding: 28px 22px 14px;
|
||||
height: 148px;
|
||||
padding: 24px 22px 18px;
|
||||
background: linear-gradient(180deg, #fcfefd 0%, #fff 100%);
|
||||
}
|
||||
.uptime-legend {
|
||||
display: flex;
|
||||
|
||||
Reference in New Issue
Block a user