Import source: web/src/components/StatusRing
Import the existing local project snapshot. This commit organizes existing files; it does not represent newly developed functionality.
This commit is contained in:
@@ -0,0 +1,237 @@
|
|||||||
|
import { memo, useMemo } from "react";
|
||||||
|
import { motion } from "motion/react";
|
||||||
|
import type { LiveTarget } from "../lib/types";
|
||||||
|
import { arcPath, circumference, latencyScale, polar } from "../lib/geometry";
|
||||||
|
import { fmtLatency, fmtUptime, pctSign } from "../lib/format";
|
||||||
|
import { springs, STATE_COLOR, useUI } from "../lib/ui";
|
||||||
|
import { useSpringNumber } from "../hooks/useSpringNumber";
|
||||||
|
|
||||||
|
const C = 60; // center
|
||||||
|
const R_TRACK = 44; // uptime ring radius
|
||||||
|
const R_LAT = 36; // latency gauge radius
|
||||||
|
const LAT_SWEEP = 270; // latency gauge max sweep, degrees
|
||||||
|
|
||||||
|
/** 60 watch-face ticks; every fifth is longer — the "fine instrument" detail. */
|
||||||
|
function Ticks() {
|
||||||
|
const ticks = useMemo(() => {
|
||||||
|
const lines = [];
|
||||||
|
for (let i = 0; i < 60; i++) {
|
||||||
|
const major = i % 5 === 0;
|
||||||
|
const a = i * 6;
|
||||||
|
const p1 = polar(C, C, major ? 50 : 51.4, a);
|
||||||
|
const p2 = polar(C, C, 53, a);
|
||||||
|
lines.push(
|
||||||
|
<line
|
||||||
|
key={i}
|
||||||
|
x1={p1.x}
|
||||||
|
y1={p1.y}
|
||||||
|
x2={p2.x}
|
||||||
|
y2={p2.y}
|
||||||
|
stroke="var(--color-ink-700)"
|
||||||
|
strokeWidth={major ? 1 : 0.5}
|
||||||
|
opacity={major ? 0.9 : 0.45}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
}, []);
|
||||||
|
return <g>{ticks}</g>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface StatusRingProps {
|
||||||
|
target: LiveTarget;
|
||||||
|
/** Uptime fraction 0..100 for the selected window. */
|
||||||
|
uptime?: number;
|
||||||
|
size?: number;
|
||||||
|
/** Mount-stagger delay in seconds (boot sequence). */
|
||||||
|
delay?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Circular visualization #1 — the radial status ring.
|
||||||
|
* Uptime fills the outer arc; latency reads on an inner brass gauge;
|
||||||
|
* every completed probe lands as a visible pulse.
|
||||||
|
*/
|
||||||
|
export const StatusRing = memo(function StatusRing({
|
||||||
|
target,
|
||||||
|
uptime,
|
||||||
|
size = 150,
|
||||||
|
delay = 0,
|
||||||
|
}: StatusRingProps) {
|
||||||
|
const { lang, reduced } = useUI();
|
||||||
|
const color = STATE_COLOR[target.state];
|
||||||
|
|
||||||
|
const frac = (uptime ?? 0) / 100;
|
||||||
|
const circ = circumference(R_TRACK);
|
||||||
|
|
||||||
|
const latMS = target.last?.latency_ms ?? target.latency.p50_ms ?? 0;
|
||||||
|
const latSweep = latencyScale(latMS) * LAT_SWEEP;
|
||||||
|
const latCirc = circumference(R_LAT);
|
||||||
|
|
||||||
|
const displayUptime = useSpringNumber(uptime ?? 0);
|
||||||
|
|
||||||
|
const arcTransition = reduced
|
||||||
|
? { duration: 0 }
|
||||||
|
: { ...springs.arc, delay };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 120 120"
|
||||||
|
width={size}
|
||||||
|
height={size}
|
||||||
|
role="img"
|
||||||
|
aria-label={`${target.name}: ${target.state}, ${
|
||||||
|
uptime !== undefined ? fmtUptime(lang, uptime) + "%" : "—"
|
||||||
|
} uptime`}
|
||||||
|
>
|
||||||
|
<Ticks />
|
||||||
|
|
||||||
|
{/* track */}
|
||||||
|
<circle
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_TRACK}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--color-ink-800)"
|
||||||
|
strokeWidth={1}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* soft glow under the arc — a wide faint twin, no filters */}
|
||||||
|
<motion.circle
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_TRACK}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={7}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={circ}
|
||||||
|
transform={`rotate(-90 ${C} ${C})`}
|
||||||
|
initial={{ strokeDashoffset: circ, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
strokeDashoffset: circ * (1 - frac),
|
||||||
|
opacity: 0.16,
|
||||||
|
stroke: color,
|
||||||
|
}}
|
||||||
|
transition={arcTransition}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* uptime arc — springs to its value, never snaps */}
|
||||||
|
<motion.circle
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_TRACK}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
strokeWidth={2.75}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={circ}
|
||||||
|
transform={`rotate(-90 ${C} ${C})`}
|
||||||
|
initial={{ strokeDashoffset: circ, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
strokeDashoffset: circ * (1 - frac),
|
||||||
|
opacity: 1,
|
||||||
|
stroke: color,
|
||||||
|
}}
|
||||||
|
transition={arcTransition}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* latency gauge — brass, log scale 1ms..2.5s */}
|
||||||
|
<g opacity={0.9}>
|
||||||
|
<path
|
||||||
|
d={arcPath(C, C, R_LAT, 0, LAT_SWEEP)}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--color-ink-800)"
|
||||||
|
strokeWidth={0.75}
|
||||||
|
/>
|
||||||
|
<motion.circle
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_LAT}
|
||||||
|
fill="none"
|
||||||
|
stroke="var(--color-brass)"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeDasharray={latCirc}
|
||||||
|
transform={`rotate(-90 ${C} ${C})`}
|
||||||
|
initial={{ strokeDashoffset: latCirc, opacity: 0 }}
|
||||||
|
animate={{
|
||||||
|
strokeDashoffset: latCirc * (1 - latSweep / 360),
|
||||||
|
opacity: 0.95,
|
||||||
|
}}
|
||||||
|
transition={
|
||||||
|
reduced ? { duration: 0 } : { ...springs.gentle, delay: delay + 0.15 }
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
|
||||||
|
{/* live pulse — a probe just landed */}
|
||||||
|
{!reduced && target.pulse > 0 && (
|
||||||
|
<motion.circle
|
||||||
|
key={target.pulse}
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_TRACK}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
initial={{ r: R_TRACK, opacity: 0.55, strokeWidth: 2 }}
|
||||||
|
animate={{ r: R_TRACK + 11, opacity: 0, strokeWidth: 0.4 }}
|
||||||
|
transition={{ duration: 1.1, ease: "easeOut" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* state ripple — a confirmed transition radiates outward */}
|
||||||
|
{!reduced && target.stateChangedAt > 0 && (
|
||||||
|
<motion.circle
|
||||||
|
key={`state-${target.stateChangedAt}`}
|
||||||
|
cx={C}
|
||||||
|
cy={C}
|
||||||
|
r={R_TRACK}
|
||||||
|
fill="none"
|
||||||
|
stroke={color}
|
||||||
|
initial={{ r: 10, opacity: 0.8, strokeWidth: 3 }}
|
||||||
|
animate={{ r: 58, opacity: 0, strokeWidth: 0.5 }}
|
||||||
|
transition={{ duration: 1.4, ease: [0.16, 1, 0.3, 1] }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* center figures */}
|
||||||
|
<motion.g
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
transition={reduced ? { duration: 0 } : { duration: 0.6, delay: delay + 0.3 }}
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
x={C}
|
||||||
|
y={C + 3}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontFamily="var(--font-display)"
|
||||||
|
fontSize={uptime !== undefined && uptime < 100 ? 21 : 24}
|
||||||
|
fill="var(--color-paper)"
|
||||||
|
className="tabular"
|
||||||
|
>
|
||||||
|
{uptime !== undefined ? fmtUptime(lang, displayUptime) : "—"}
|
||||||
|
<tspan fontSize={10} fill="var(--color-paper-faint)" dx={1}>
|
||||||
|
{pctSign(lang)}
|
||||||
|
</tspan>
|
||||||
|
</text>
|
||||||
|
{/* re-keyed per probe so fresh data visibly lands */}
|
||||||
|
<motion.text
|
||||||
|
key={target.pulse}
|
||||||
|
x={C}
|
||||||
|
y={C + 17}
|
||||||
|
textAnchor="middle"
|
||||||
|
fontFamily="var(--font-mono)"
|
||||||
|
fontSize={7.5}
|
||||||
|
fill="var(--color-brass)"
|
||||||
|
className="tabular"
|
||||||
|
initial={reduced ? false : { opacity: 0, y: 3 }}
|
||||||
|
animate={{ opacity: 0.95, y: 0 }}
|
||||||
|
transition={reduced ? { duration: 0 } : springs.snappy}
|
||||||
|
>
|
||||||
|
{target.last ? fmtLatency(lang, latMS) : "·"}
|
||||||
|
</motion.text>
|
||||||
|
</motion.g>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user