import { ComponentProps } from "react"; import { cva, cx } from "cva"; import { someIterable } from "../utils"; import { GridCard } from "./Card"; import MetricsChart from "./MetricsChart"; interface ChartPoint { date: number; metric: number | null; } interface MetricProps { title: string; description: string; stream?: Map; metric?: K; data?: ChartPoint[]; gate?: Map; supported?: boolean; map?: (p: { date: number; metric: number | null }) => ChartPoint; domain?: [number, number]; unit: string; heightClassName?: string; referenceValue?: number; badge?: ComponentProps["badge"]; badgeTheme?: ComponentProps["badgeTheme"]; } /* eslint-disable-next-line */ export function createChartArray( metrics: Map, metricName: K, ) { const result: { date: number; metric: number | null }[] = []; const iter = metrics.entries(); let next = iter.next() as IteratorResult<[number, T]>; const now = Math.floor(Date.now() / 1000); // We want 120 data points, in the chart. const firstDate = Math.min(next.value?.[0] ?? now, now - 120); for (let t = firstDate; t < now; t++) { while (!next.done && next.value[0] < t) next = iter.next(); const has = !next.done && next.value[0] === t; let metric = null; if (has) metric = next.value[1][metricName] as number; result.push({ date: t, metric }); if (has) next = iter.next(); } return result; } const theme = { light: "bg-white text-black border border-slate-800/20 dark:border dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300", danger: "bg-red-500 dark:border-red-700 dark:bg-red-800 dark:text-red-50", primary: "bg-blue-500 dark:border-blue-700 dark:bg-blue-800 dark:text-blue-50", }; interface SettingsItemProps { readonly title: string; readonly description: string | React.ReactNode; readonly badge?: string; readonly className?: string; readonly children?: React.ReactNode; readonly badgeTheme?: keyof typeof theme; } export function MetricHeader(props: SettingsItemProps) { const { title, description, badge } = props; const badgeVariants = cva({ variants: { theme: theme } }); return (
{title} {badge && ( {badge} )}
{description}
); } export function Metric({ title, description, stream, metric, data, gate, supported, map, domain = [0, 600], unit = "", heightClassName = "h-[127px]", badge, badgeTheme, }: MetricProps) { const ready = gate ? gate.size > 0 : stream ? stream.size > 0 : true; const supportedFinal = supported ?? (stream && metric ? someIterable(stream, ([, s]) => s[metric] !== undefined) : true); // Either we let the consumer provide their own chartArray, or we create one from the stream and metric. const raw = data ?? ((stream && metric && createChartArray(stream, metric)) || []); // If the consumer provides a map function, we apply it to the raw data. const dataFinal: ChartPoint[] = map ? raw.map(map) : raw; const recent = dataFinal .slice(-(raw.length - 1)) .filter(x => x.metric != null) as ChartPoint[]; // Average the recent values const computedReferenceValue = recent.length > 0 ? Math.round( recent.reduce((sum, x) => sum + (x.metric as number), 0) / recent.length, ) : undefined; return (
{!ready ? (

Waiting for data...

) : supportedFinal ? ( ) : (

Metric not supported

)}
); }