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