mirror of
https://github.com/Nezumi-2711/uptime-monitoring.git
synced 2026-09-22 13:48:31 +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 { 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"
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user