Skip to content
115 linesCodeBlameRaw
1/**
2 * An incident's parts, check by check: a latency chart per part, drawn as
3 * SVG on the server, with the slow line, slow checks dotted and failures
4 * marked along the top, and the latest checks as a table under it. No
5 * script and no inline style (sudo ships no JavaScript); the table is the
6 * way to read exact figures.
7 */
8import type { CheckHistory } from "@g1t/contracts";
9
10import { When } from "~/components/ui";
11import { latencyPlot, latencySummary, msWords, summaryWords } from "~/lib/latency";
12
13/** How many of the latest checks the table lists. */
14const LISTED = 30;
15
16const OUTCOME = { up: "OK", degraded: "Slow", down: "Not answering" } as const;
17
18function Chart({ history, name }: { history: CheckHistory; name: string }) {
19 const p = latencyPlot(history);
20 return (
21 <svg viewBox={`0 0 ${p.width} ${p.height}`} className="block h-auto w-full" role="img" aria-label={`${name}: how long each check took, ${summaryWords(latencySummary(history.samples))}`}>
22 {p.ticks.map((t) => (
23 <g key={t.label}>
24 <line x1={p.plot.x} x2={p.plot.x + p.plot.width} y1={t.y} y2={t.y} stroke="var(--g1t-line)" strokeWidth="1" strokeDasharray={t.label === "0" ? undefined : "2 4"} />
25 <text x={p.plot.x - 6} y={t.y + 3.5} textAnchor="end" fontSize="10" className="fill-faint tabular">
26 {t.label}
27 </text>
28 </g>
29 ))}
30 {p.slowY != null && (
31 <g>
32 <line x1={p.plot.x} x2={p.plot.x + p.plot.width} y1={p.slowY} y2={p.slowY} stroke="var(--g1t-warn)" strokeWidth="1" strokeDasharray="4 3" />
33 <text x={p.plot.x + p.plot.width} y={p.slowY - 3} textAnchor="end" fontSize="10" className="fill-warn">
34 slow over {msWords(history.slow_ms!)}
35 </text>
36 </g>
37 )}
38 {p.down.map((d, i) => (
39 <rect key={`d${i}`} x={d.x - 1} y={p.plot.y} width="2" height={p.plot.height} className="fill-danger/35" />
40 ))}
41 <path d={p.line} fill="none" stroke="var(--g1t-accent)" strokeWidth="1.5" strokeLinejoin="round" strokeLinecap="round" />
42 {p.slow.map((s, i) => (
43 <circle key={`s${i}`} cx={s.x} cy={s.y} r="2.5" className="fill-warn" />
44 ))}
45 <text x={p.plot.x} y={p.height - 4} fontSize="10" className="fill-faint">
46 {new Date(history.from).toISOString().slice(11, 16)} UTC
47 </text>
48 <text x={p.plot.x + p.plot.width} y={p.height - 4} textAnchor="end" fontSize="10" className="fill-faint">
49 {new Date(history.to).toISOString().slice(11, 16)} UTC
50 </text>
51 </svg>
52 );
53}
54
55function Part({ history, name }: { history: CheckHistory; name: string }) {
56 const summary = latencySummary(history.samples);
57 const latest = history.samples.slice(-LISTED).reverse();
58 return (
59 <div className="space-y-2">
60 <div className="flex flex-wrap items-baseline justify-between gap-x-3 gap-y-0.5">
61 <h3 className="text-sm font-medium">{name}</h3>
62 {summary.checks > 0 && <p className="text-xs text-faint">{summaryWords(summary)}</p>}
63 </div>
64 {history.samples.length === 0 ? (
65 <p className="text-sm text-muted">No checks kept for this span: checks are kept for 7 days.</p>
66 ) : (
67 <>
68 <Chart history={history} name={name} />
69 <details className="group">
70 <summary className="cursor-pointer text-xs text-muted select-none hover:text-fg">
71 The latest {Math.min(LISTED, history.samples.length)} checks
72 </summary>
73 <div className="mt-2 overflow-x-auto">
74 <table className="w-full text-left text-xs tabular">
75 <thead className="text-faint">
76 <tr>
77 <th className="py-1 pr-3 font-normal">At</th>
78 <th className="py-1 pr-3 font-normal">Took</th>
79 <th className="py-1 pr-3 font-normal">Result</th>
80 <th className="py-1 pr-3 font-normal">From</th>
81 <th className="py-1 font-normal">First try</th>
82 </tr>
83 </thead>
84 <tbody>
85 {latest.map((s) => (
86 <tr key={s.at} className="border-t border-line">
87 <td className="py-1 pr-3 whitespace-nowrap">
88 <When at={s.at} time />
89 </td>
90 <td className="py-1 pr-3">{s.ms != null ? msWords(s.ms) : "—"}</td>
91 <td className={`py-1 pr-3 ${s.outcome === "down" ? "text-danger" : s.outcome === "degraded" ? "text-warn" : "text-muted"}`}>{OUTCOME[s.outcome]}</td>
92 <td className="py-1 pr-3 font-mono">{s.colo ?? "—"}</td>
93 <td className="py-1 text-faint">{s.first_ms != null ? `${msWords(s.first_ms)}, asked again` : ""}</td>
94 </tr>
95 ))}
96 </tbody>
97 </table>
98 </div>
99 </details>
100 </>
101 )}
102 </div>
103 );
104}
105
106/** Every part's checks around an incident. */
107export function IncidentChecks({ checks, names }: { checks: CheckHistory[]; names: Map<string, string> }) {
108 return (
109 <div className="space-y-5">
110 {checks.map((h) => (
111 <Part key={h.key} history={h} name={names.get(h.key) ?? h.key} />
112 ))}
113 </div>
114 );
115}