167 lines
5.1 KiB
TypeScript
167 lines
5.1 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
|
|
/*
|
|
* The hero's signature element: a fleet panel that plays one honest cycle of
|
|
* what the product actually does — a workflow runs three steps, a TLS monitor
|
|
* fails and opens an incident, a key revocation lands — then rests. It is a
|
|
* dramatisation, not live data, so nothing here talks to an API.
|
|
*/
|
|
|
|
type LogLine = { time: string; body: React.ReactNode };
|
|
|
|
type Beat = {
|
|
at: number;
|
|
line: LogLine;
|
|
effect?: "incident" | "runDone" | "revoked";
|
|
};
|
|
|
|
const BEATS: Beat[] = [
|
|
{ at: 600, line: { time: "14:22:02", body: "running · step 1/3 · pull image" } },
|
|
{ at: 1500, line: { time: "14:22:04", body: "running · step 2/3 · migrate database" } },
|
|
{
|
|
at: 2600,
|
|
line: { time: "14:22:07", body: <><span className="ok">ok</span> · migrate database · exit 0</> },
|
|
},
|
|
{
|
|
at: 3400,
|
|
line: { time: "14:22:08", body: "running · step 3/3 · restart service" },
|
|
effect: "incident",
|
|
},
|
|
{
|
|
at: 4300,
|
|
line: { time: "14:22:10", body: <><span className="er">monitor</span> · edge-gw-02 tls · connection refused</> },
|
|
},
|
|
{
|
|
at: 5200,
|
|
line: { time: "14:22:11", body: <><span className="ok">ok</span> · restart service · exit 0</> },
|
|
effect: "runDone",
|
|
},
|
|
{
|
|
at: 6000,
|
|
line: { time: "14:22:12", body: <>run finished · <span className="ok">success</span> · 3 steps · 1 server</> },
|
|
effect: "revoked",
|
|
},
|
|
];
|
|
|
|
const FIRST_LINE: LogLine = { time: "14:22:01", body: "queued · deploy-app · 1 server" };
|
|
const MAX_LINES = 7;
|
|
|
|
export function InstrumentPanel() {
|
|
const [lines, setLines] = useState<LogLine[]>([FIRST_LINE]);
|
|
const [incident, setIncident] = useState(false);
|
|
const [runActive, setRunActive] = useState(true);
|
|
const [revoked, setRevoked] = useState(false);
|
|
const [resting, setResting] = useState(false);
|
|
|
|
useEffect(() => {
|
|
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
|
|
const apply = (effect: Beat["effect"]) => {
|
|
if (effect === "incident") setIncident(true);
|
|
if (effect === "runDone") setRunActive(false);
|
|
if (effect === "revoked") setRevoked(true);
|
|
};
|
|
|
|
|
|
if (reduced) {
|
|
setLines([FIRST_LINE, ...BEATS.map((b) => b.line)].slice(-MAX_LINES));
|
|
BEATS.forEach((b) => apply(b.effect));
|
|
setResting(true);
|
|
return;
|
|
}
|
|
|
|
const timers = BEATS.map((beat) =>
|
|
window.setTimeout(() => {
|
|
setLines((prev) => [...prev, beat.line].slice(-MAX_LINES));
|
|
apply(beat.effect);
|
|
}, beat.at)
|
|
);
|
|
timers.push(window.setTimeout(() => setResting(true), 6600));
|
|
|
|
return () => timers.forEach(window.clearTimeout);
|
|
}, []);
|
|
|
|
return (
|
|
<div className="instrument">
|
|
<div className="rail">
|
|
<div className="instrument__bar">
|
|
<span>
|
|
<b>northgate</b> · fleet
|
|
</span>
|
|
<span>12 servers</span>
|
|
<span>{incident ? "10 up" : "11 up"}</span>
|
|
<span>{incident ? "2 down" : "1 down"}</span>
|
|
<span>3 monitors</span>
|
|
<span>{runActive ? "1 run active" : "no runs active"}</span>
|
|
<span className="instrument__clock">14:22:12 UTC</span>
|
|
</div>
|
|
|
|
<div className="panes">
|
|
<section className="pane" aria-label="Fleet status">
|
|
<h2 className="pane__h">
|
|
Fleet <span>{revoked ? "key revoked · 1 server updated" : "agents polling"}</span>
|
|
</h2>
|
|
|
|
<Row host="proxmox-node-1" sub="4 keys · 12% cpu" state="up" label="Active" />
|
|
<Row host="db-primary" sub="3 keys · 61% cpu" state="up" label="Active" />
|
|
<Row
|
|
host="edge-gw-02"
|
|
sub={incident ? "2 keys · tls refused" : "2 keys · tls 41d"}
|
|
state={incident ? "down" : "up"}
|
|
label={incident ? "Incident" : "Active"}
|
|
/>
|
|
<Row host="win-build-01" sub="agent 1.4.1 · update ready" state="pend" label="Pending" />
|
|
<Row
|
|
host="app-worker-03"
|
|
sub={revoked ? "4 keys · 1 revoked" : "5 keys · idle"}
|
|
state="up"
|
|
label="Active"
|
|
/>
|
|
</section>
|
|
|
|
<section className="pane" aria-label="Workflow run">
|
|
<h2 className="pane__h">
|
|
Run <span>run_8f31c2</span>
|
|
</h2>
|
|
<div className="stream" aria-live="polite">
|
|
{lines.map((line, i) => (
|
|
<div key={`${line.time}-${i}`}>
|
|
<span className="t">{line.time}</span> {line.body}
|
|
</div>
|
|
))}
|
|
{resting && (
|
|
<div>
|
|
<span className="caret">_</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function Row({
|
|
host,
|
|
sub,
|
|
state,
|
|
label,
|
|
}: {
|
|
host: string;
|
|
sub: string;
|
|
state: "up" | "down" | "pend";
|
|
label: string;
|
|
}) {
|
|
return (
|
|
<div className="frow">
|
|
<span className={state === "up" ? "dot" : `dot dot--${state}`} />
|
|
<span className="frow__host">{host}</span>
|
|
<span className="frow__sub">{sub}</span>
|
|
<span className={`chip chip--${state}`}>{label}</span>
|
|
</div>
|
|
);
|
|
}
|