mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-21 23:56:39 +00:00
748ba46669
* feat(dashboard): status masthead, unified gauges, stack health sparklines Rework the home dashboard around a single cyan-railed status masthead that carries the health state word, node meta, and reasons inline. The resource block collapses into one strip with a CPU hero sparkline, memory and disk gauge bars, and a network tile whose sparkline is built from per-container byte-counter deltas. Stack health becomes an 8-column mono table with row tinting, an uptime column sourced from the oldest running container's creation time, and a per-stack 10-minute CPU sparkline. Historical charts pick up a cyan gradient and an amber peak marker. A shared Sparkline primitive backs all of the above. * docs(dashboard): refresh screenshot for phase B layout * fix(dashboard): anchor sparkline bucketing to latest metric timestamp The cpuHistory, netHistory, and cpuPeakLabel memos called Date.now() inside useMemo, which violates react-hooks/purity: the rule fires because re-renders can produce different bucket boundaries from the same inputs. Derive a historyEndAt anchor from the newest metric sample in the polled series and thread it through to ResourceGauges.
125 lines
3.1 KiB
TypeScript
125 lines
3.1 KiB
TypeScript
import * as React from "react"
|
|
|
|
import { cn } from "@/lib/utils"
|
|
|
|
type SvgSvgAttrs = Omit<React.SVGAttributes<SVGSVGElement>, "points" | "strokeWidth">
|
|
|
|
export interface SparklineProps extends SvgSvgAttrs {
|
|
points: number[]
|
|
stroke?: string
|
|
fill?: string
|
|
peakIndex?: number
|
|
peakColor?: string
|
|
showPeak?: boolean
|
|
strokeWidth?: number
|
|
min?: number
|
|
max?: number
|
|
width?: number
|
|
height?: number
|
|
}
|
|
|
|
const VIEW_W = 100
|
|
const VIEW_H = 28
|
|
const PEAK_R = 0.9
|
|
|
|
export const Sparkline = React.forwardRef<SVGSVGElement, SparklineProps>(
|
|
(
|
|
{
|
|
points,
|
|
stroke = "var(--chart-1)",
|
|
fill = "var(--chart-1)",
|
|
peakIndex,
|
|
peakColor = "var(--data-peak)",
|
|
showPeak = true,
|
|
strokeWidth,
|
|
min,
|
|
max,
|
|
width,
|
|
height,
|
|
className,
|
|
...rest
|
|
},
|
|
ref,
|
|
) => {
|
|
const id = React.useId()
|
|
|
|
if (!points || points.length < 2) {
|
|
return (
|
|
<svg
|
|
ref={ref}
|
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
|
preserveAspectRatio="none"
|
|
className={cn("block h-full w-full", className)}
|
|
width={width}
|
|
height={height}
|
|
{...rest}
|
|
aria-hidden="true"
|
|
/>
|
|
)
|
|
}
|
|
|
|
const lo = min ?? Math.min(...points)
|
|
const hi = max ?? Math.max(...points)
|
|
const range = hi - lo || 1
|
|
|
|
const coords = points.map((v, i) => {
|
|
const x = (i / (points.length - 1)) * VIEW_W
|
|
const y = VIEW_H - ((v - lo) / range) * VIEW_H
|
|
return [x, y] as const
|
|
})
|
|
|
|
const line = coords.map(([x, y], i) => `${i === 0 ? "M" : "L"}${x.toFixed(2)},${y.toFixed(2)}`).join(" ")
|
|
const area = `${line} L${VIEW_W.toFixed(2)},${VIEW_H} L0,${VIEW_H} Z`
|
|
|
|
const resolvedPeakIndex =
|
|
typeof peakIndex === "number"
|
|
? peakIndex
|
|
: points.indexOf(Math.max(...points))
|
|
|
|
const peak = showPeak && resolvedPeakIndex >= 0 ? coords[resolvedPeakIndex] : null
|
|
|
|
return (
|
|
<svg
|
|
ref={ref}
|
|
viewBox={`0 0 ${VIEW_W} ${VIEW_H}`}
|
|
preserveAspectRatio="none"
|
|
className={cn("block h-full w-full overflow-visible", className)}
|
|
width={width}
|
|
height={height}
|
|
{...rest}
|
|
aria-hidden="true"
|
|
>
|
|
<defs>
|
|
<linearGradient id={`spark-fill-${id}`} x1="0" x2="0" y1="0" y2="1">
|
|
<stop offset="0%" stopColor={fill} stopOpacity={0.35} />
|
|
<stop offset="100%" stopColor={fill} stopOpacity={0} />
|
|
</linearGradient>
|
|
</defs>
|
|
<path d={area} fill={`url(#spark-fill-${id})`} stroke="none" vectorEffect="non-scaling-stroke" />
|
|
<path
|
|
d={line}
|
|
fill="none"
|
|
stroke={stroke}
|
|
strokeWidth={strokeWidth ?? 1.25}
|
|
strokeLinejoin="round"
|
|
strokeLinecap="round"
|
|
vectorEffect="non-scaling-stroke"
|
|
/>
|
|
{peak ? (
|
|
<circle
|
|
cx={peak[0]}
|
|
cy={peak[1]}
|
|
r={PEAK_R}
|
|
fill={peakColor}
|
|
stroke="var(--background)"
|
|
strokeWidth={0.5}
|
|
vectorEffect="non-scaling-stroke"
|
|
/>
|
|
) : null}
|
|
</svg>
|
|
)
|
|
},
|
|
)
|
|
|
|
Sparkline.displayName = "Sparkline"
|