Compare commits

...

9 Commits

Author SHA1 Message Date
Trisha Lim
8cef969dd4 Fix build 2025-01-07 13:17:27 +00:00
Trisha Lim
dffb22b4e8 Fix build 2025-01-07 13:13:39 +00:00
Trisha Lim
adffece2b6 Styling 2025-01-07 12:09:01 +00:00
Trisha Lim
209aec88f4 Remove heart icon 2025-01-06 18:21:31 +00:00
Trisha Lim
17fec793e6 Style heatmap chart 2025-01-06 18:20:12 +00:00
Trisha Lim
793fbe63d1 Add heatmap chart 2025-01-06 17:53:53 +00:00
Trisha Lim
a2bb6aa34d Get latency data over time 2025-01-06 17:20:43 +00:00
Trisha Lim
d04a3c83d3 Change to table 2025-01-06 16:02:44 +00:00
Trisha Lim
a58ee23642 Status page design 2025-01-06 14:48:12 +00:00
5 changed files with 351 additions and 25 deletions

View File

@@ -4,4 +4,9 @@
@layer components {
@import "shiki.css";
.g-apexcharts-tooltip {
@apply py-2 px-3 rounded-md shadow-sm border;
@apply bg-white dark:bg-stone-925 dark:border-stone-900;
}
}

View File

@@ -1,6 +1,6 @@
import { cn } from "@/lib/utils";
import { LatencyChart } from "@/components/LatencyChart";
import { clsx } from "clsx";
import { HeroHeader } from "gcmp-design-system/src/app/components/molecules/HeroHeader";
import { HeartIcon } from "lucide-react";
export const metadata = {
title: "Status",
@@ -30,11 +30,49 @@ export default async function Page() {
intervalMs: 1000,
refId: "A",
},
{
datasource: {
type: "prometheus",
uid: "grafanacloud-prom",
},
editorMode: "code",
expr: '1000 / 2 * avg(probe_duration_seconds{probe=~".*", instance="https://mesh.jazz.tools/self-sync-check", job="self-sync-check"} * on (instance, job,probe,config_version) group_left probe_success{probe=~".*",instance="https://mesh.jazz.tools/self-sync-check", job="self-sync-check"} > 0) by (probe)',
instant: false,
interval: "",
intervalFactor: 1,
legendFormat: "{{probe}}",
refId: "B",
},
],
}),
});
const responseData = await res.json();
if (!responseData.results?.A?.frames || !responseData.results?.B?.frames)
return;
const byProbe: any[] = [];
for (const frame of responseData.results.A.frames) {
const probe = frame.schema.fields[1].labels.probe;
byProbe[probe] = {
status: frame.data.values[1][0],
label: startCase(probe),
};
}
for (const frame of responseData.results.B.frames) {
const probe = frame.schema.fields[1].labels.probe;
if (!byProbe[probe]) {
byProbe[probe] = {
label: startCase(probe),
};
}
byProbe[probe].latencyOverTime = frame.data.values;
}
return (
<div className="container flex flex-col gap-6 pb-10 lg:pb-20">
<HeroHeader
@@ -42,29 +80,56 @@ export default async function Page() {
slogan="Great system status spage by smart people."
/>
<div className="grid sm:grid-cols-2 lg:grid-cols-5 gap-8">
{responseData.results.A.frames.map((frame) => (
<div key={frame.schema.fields[1].labels.probe}>
<h2 className="text-2xl">
{startCase(frame.schema.fields[1].labels.probe)}
</h2>
<div className="mt-2">
<HeartIcon
className={cn(
"w-10 h-10",
frame.data.values[1][0] === 1
? "text-green-500"
: "text-red-500",
)}
/>
</div>
</div>
))}
</div>
<div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-8">
{/* <pre>{JSON.stringify(responseData, null, 2)}</pre> */}
</div>
<table className="min-w-full">
<thead className="text-left text-sm font-semibold text-stone-900 dark:text-white">
<tr>
<th scope="col" className="py-3.5 pl-4 pr-3 sm:pl-3 w-3/5">
Latency
</th>
<th scope="col" className="px-3 py-3.5">
Average
</th>
<th scope="col" className="px-3 py-3.5 whitespace-nowrap">
99th %
</th>
<th scope="col" className="px-3 py-3.5">
Status
</th>
<th>
<span className="sr-only">Location</span>
</th>
</tr>
</thead>
<tbody>
{Object.values(byProbe).map((row) => (
<tr key={row.label} className="border-t">
<td className="pr-3">
<LatencyChart data={row} />
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">100ms</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">100ms</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
<div className="flex items-center gap-2">
<div
className={clsx(
"flex-none rounded-full p-1",
row.status === 1
? "text-green-400 bg-green-400/10"
: "text-rose-400 bg-rose-400/10",
)}
>
<div className="size-1.5 rounded-full bg-current" />
</div>
{row.status === 1 ? "Up" : "Down"}
</div>
</td>
<td className="whitespace-nowrap px-3 py-4 text-sm">
{row.label}
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,161 @@
"use client";
import { ApexOptions } from "apexcharts";
import dynamic from "next/dynamic";
import { useMemo } from "react";
const ReactApexChart = dynamic(() => import("react-apexcharts"), {
ssr: false,
});
export function LatencyChart({ data }: { data: any }) {
const series = useMemo(() => {
return {
name: "Latency",
data: data.latencyOverTime[0].map(
(timestamp: number, latency: number) => ({
x: timestamp,
y: Math.round(data.latencyOverTime[1][latency]),
}),
),
};
}, [data]);
const options: ApexOptions = {
grid: {
show: false,
},
stroke: {
show: false,
},
chart: {
animations: {
enabled: false,
},
type: "heatmap",
toolbar: {
show: false,
},
},
dataLabels: {
enabled: false,
},
title: {
text: "",
},
xaxis: {
labels: {
show: false,
},
axisBorder: {
show: false,
},
axisTicks: {
show: false,
},
tooltip: {
enabled: false,
},
},
yaxis: {
show: false,
labels: {
formatter: function (value: number) {
return value + " milliseconds";
},
},
},
tooltip: {
theme: "",
custom: function ({ series, seriesIndex, dataPointIndex, w }) {
const latency = series[seriesIndex][dataPointIndex];
const date = new Date(
w.globals.labels[dataPointIndex],
).toLocaleString();
return `
<div class="">
<span class="text-xs">${date}</span><br/>
<span><span class="font-semibold text-stone-900 dark:text-white">${latency}</span> milliseconds</span><br/>
</div>
`;
},
cssClass: "g-apexcharts-tooltip",
},
plotOptions: {
heatmap: {
radius: 0,
colorScale: {
ranges: [
{
from: 0,
to: 10,
color: "#1ae200",
},
{
from: 10,
to: 50,
color: "#32c119",
},
{
from: 50,
to: 100,
color: "#62bd56",
},
{
from: 100,
to: 200,
color: "#4c8944",
},
{
from: 200,
to: 300,
color: "#3b6537",
},
{
from: 300,
to: 400,
color: "#405b3d",
},
{
from: 400,
to: 500,
color: "#395335",
},
{
from: 500,
to: 750,
color: "#283e2b",
},
{
from: 750,
to: 1000,
color: "#1e2a1d",
},
{
from: 1000,
to: 3000,
color: "#162018",
},
{
from: 3000,
color: "#ff001e",
},
],
},
},
},
};
return (
<ReactApexChart
options={options}
series={[series]}
type="heatmap"
width={900}
height={100}
/>
);
}

View File

@@ -24,6 +24,7 @@
"@types/topojson-client": "^3.1.5",
"@vercel/analytics": "^1.3.1",
"@vercel/speed-insights": "^1.0.12",
"apexcharts": "^4.3.0",
"clsx": "^2.1.1",
"gcmp-design-system": "workspace:*",
"jazz-browser": "workspace:../*",
@@ -39,6 +40,7 @@
"next-themes": "^0.2.1",
"qrcode": "^1.5.4",
"react": "^18",
"react-apexcharts": "^1.7.0",
"react-dom": "^18",
"rehype-slug": "^6.0.0",
"shiki": "^0.14.6",

View File

@@ -208,6 +208,9 @@ importers:
'@vercel/speed-insights':
specifier: ^1.0.12
version: 1.0.12(next@14.2.15(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
apexcharts:
specifier: ^4.3.0
version: 4.3.0
clsx:
specifier: ^2.1.1
version: 2.1.1
@@ -253,6 +256,9 @@ importers:
react:
specifier: ^18
version: 18.3.1
react-apexcharts:
specifier: ^1.7.0
version: 1.7.0(apexcharts@4.3.0)(react@18.3.1)
react-dom:
specifier: ^18
version: 18.3.1(react@18.3.1)
@@ -674,6 +680,31 @@ packages:
resolution: {integrity: sha512-/4UjstX8ploZklY8MmlOQoXB1jWIo3Go4MP0R39sbkoWuN6rJ7Zt6l4bjkDPM4hKsjoODh9SSKn2otlp3XL3/A==}
engines: {node: '>=14.17'}
'@svgdotjs/svg.draggable.js@3.0.4':
resolution: {integrity: sha512-vWi/Col5Szo74HJVBgMHz23kLVljt3jvngmh0DzST45iO2ubIZ487uUAHIxSZH2tVRyiaaTL+Phaasgp4gUD2g==}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@svgdotjs/svg.filter.js@3.0.8':
resolution: {integrity: sha512-YshF2YDaeRA2StyzAs5nUPrev7npQ38oWD0eTRwnsciSL2KrRPMoUw8BzjIXItb3+dccKGTX3IQOd2NFzmHkog==}
engines: {node: '>= 0.8.0'}
'@svgdotjs/svg.js@3.2.4':
resolution: {integrity: sha512-BjJ/7vWNowlX3Z8O4ywT58DqbNRyYlkk6Yz/D13aB7hGmfQTvGX4Tkgtm/ApYlu9M7lCQi15xUEidqMUmdMYwg==}
'@svgdotjs/svg.resize.js@2.0.5':
resolution: {integrity: sha512-4heRW4B1QrJeENfi7326lUPYBCevj78FJs8kfeDxn5st0IYPIRXoTtOSYvTzFWgaWWXd3YCDE6ao4fmv91RthA==}
engines: {node: '>= 14.18'}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@svgdotjs/svg.select.js': ^4.0.1
'@svgdotjs/svg.select.js@4.0.2':
resolution: {integrity: sha512-5gWdrvoQX3keo03SCmgaBbD+kFftq0F/f2bzCbNnpkkvW6tk4rl4MakORzFuNjvXPWwB4az9GwuvVxQVnjaK2g==}
engines: {node: '>= 14.18'}
peerDependencies:
'@svgdotjs/svg.js': ^3.2.4
'@swc/counter@0.1.3':
resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==}
@@ -1205,6 +1236,9 @@ packages:
'@xtuc/long@4.2.2':
resolution: {integrity: sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==}
'@yr/monotone-cubic-spline@1.0.3':
resolution: {integrity: sha512-FQXkOta0XBSUPHndIKON2Y9JeQz5ZeMqLYZVVK93FliNBFm7LNMIZmY6FrMEB9XPcDbE2bekMbZD6kzDkxwYjA==}
abbrev@2.0.0:
resolution: {integrity: sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -1264,6 +1298,9 @@ packages:
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
engines: {node: '>= 8'}
apexcharts@4.3.0:
resolution: {integrity: sha512-PfvZQpv91T68hzry9l5zP3Gip7sQvF0nFK91uCBrswIKX7rbIdbVNS4fOks9m9yP3Ppgs6LHgU2M/mjoG4NM0A==}
arg@5.0.2:
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
@@ -2294,6 +2331,9 @@ packages:
resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==}
engines: {node: ^10 || ^12 || >=14}
prop-types@15.8.1:
resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
property-information@6.5.0:
resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==}
@@ -2327,11 +2367,20 @@ packages:
rbush@3.0.1:
resolution: {integrity: sha512-XRaVO0YecOpEuIvbhbpTrZgoiI6xBlz6hnlr6EHhd+0x9ase6EmeN+hdwwUaJvLcsFFQ8iWVF1GAK1yB0BWi0w==}
react-apexcharts@1.7.0:
resolution: {integrity: sha512-03oScKJyNLRf0Oe+ihJxFZliBQM9vW3UWwomVn4YVRTN1jsIR58dLWt0v1sb8RwJVHDMbeHiKQueM0KGpn7nOA==}
peerDependencies:
apexcharts: '>=4.0.0'
react: '>=0.13'
react-dom@18.3.1:
resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==}
peerDependencies:
react: ^18.3.1
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
react-promise-suspense@0.3.4:
resolution: {integrity: sha512-I42jl7L3Ze6kZaq+7zXWSunBa3b1on5yfvUW6Eo/3fFOj6dZ5Bqmcd264nJbTK/gn1HjjILAjSwnZbV4RpSaNQ==}
@@ -3086,6 +3135,25 @@ snapshots:
hast-util-to-string: 2.0.0
unist-util-visit: 4.1.2
'@svgdotjs/svg.draggable.js@3.0.4(@svgdotjs/svg.js@3.2.4)':
dependencies:
'@svgdotjs/svg.js': 3.2.4
'@svgdotjs/svg.filter.js@3.0.8':
dependencies:
'@svgdotjs/svg.js': 3.2.4
'@svgdotjs/svg.js@3.2.4': {}
'@svgdotjs/svg.resize.js@2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))':
dependencies:
'@svgdotjs/svg.js': 3.2.4
'@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
'@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4)':
dependencies:
'@svgdotjs/svg.js': 3.2.4
'@swc/counter@0.1.3': {}
'@swc/helpers@0.5.5':
@@ -4405,6 +4473,8 @@ snapshots:
'@xtuc/long@4.2.2': {}
'@yr/monotone-cubic-spline@1.0.3': {}
abbrev@2.0.0: {}
acorn-import-assertions@1.9.0(acorn@8.14.0):
@@ -4449,6 +4519,15 @@ snapshots:
normalize-path: 3.0.0
picomatch: 2.3.1
apexcharts@4.3.0:
dependencies:
'@svgdotjs/svg.draggable.js': 3.0.4(@svgdotjs/svg.js@3.2.4)
'@svgdotjs/svg.filter.js': 3.0.8
'@svgdotjs/svg.js': 3.2.4
'@svgdotjs/svg.resize.js': 2.0.5(@svgdotjs/svg.js@3.2.4)(@svgdotjs/svg.select.js@4.0.2(@svgdotjs/svg.js@3.2.4))
'@svgdotjs/svg.select.js': 4.0.2(@svgdotjs/svg.js@3.2.4)
'@yr/monotone-cubic-spline': 1.0.3
arg@5.0.2: {}
astring@1.8.6: {}
@@ -5808,6 +5887,12 @@ snapshots:
picocolors: 1.0.0
source-map-js: 1.2.0
prop-types@15.8.1:
dependencies:
loose-envify: 1.4.0
object-assign: 4.1.1
react-is: 16.13.1
property-information@6.5.0: {}
proto-list@1.2.4: {}
@@ -5838,12 +5923,20 @@ snapshots:
dependencies:
quickselect: 2.0.0
react-apexcharts@1.7.0(apexcharts@4.3.0)(react@18.3.1):
dependencies:
apexcharts: 4.3.0
prop-types: 15.8.1
react: 18.3.1
react-dom@18.3.1(react@18.3.1):
dependencies:
loose-envify: 1.4.0
react: 18.3.1
scheduler: 0.23.2
react-is@16.13.1: {}
react-promise-suspense@0.3.4:
dependencies:
fast-deep-equal: 2.0.1