Skip to content

Commit a7db0e8

Browse files
Enhance connection stats sidebar (#748)
* feat: add Metric component for data visualization * refactor: update ConnectionStatsSidebar to use Metric component for improved data visualization * feat: add someIterable utility function and update Metric components for consistent metric handling - Introduced `someIterable` function to check for the presence of a metric in an iterable. - Updated `CustomTooltip` and `Metric` components to use `metric` instead of `stat` for improved clarity. - Refactored `StatChart` to align with the new metric naming convention. * refactor: rename variable for clarity in Metric component * docs: add JSDoc comments to createChartArray function in Metric component for better documentation * feat: do an actual avg reference calc * feat: Dont collect stats without a video track * refactor: rename variables for clarity
1 parent bcc307b commit a7db0e8

File tree

5 files changed

+339
-192
lines changed

5 files changed

+339
-192
lines changed

ui/src/components/CustomTooltip.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
import Card from "@components/Card";
22

33
export interface CustomTooltipProps {
4-
payload: { payload: { date: number; stat: number }; unit: string }[];
4+
payload: { payload: { date: number; metric: number }; unit: string }[];
55
}
66

77
export default function CustomTooltip({ payload }: CustomTooltipProps) {
88
if (payload?.length) {
99
const toolTipData = payload[0];
10-
const { date, stat } = toolTipData.payload;
10+
const { date, metric } = toolTipData.payload;
1111

1212
return (
1313
<Card>
14-
<div className="p-2 text-black dark:text-white">
15-
<div className="font-semibold">
14+
<div className="px-2 py-1.5 text-black dark:text-white">
15+
<div className="text-[13px] font-semibold">
1616
{new Date(date * 1000).toLocaleTimeString()}
1717
</div>
1818
<div className="space-y-1">
1919
<div className="flex items-center gap-x-1">
2020
<div className="h-[2px] w-2 bg-blue-700" />
21-
<span >
22-
{stat} {toolTipData?.unit}
21+
<span className="text-[13px]">
22+
{metric} {toolTipData?.unit}
2323
</span>
2424
</div>
2525
</div>

ui/src/components/Metric.tsx

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/* eslint-disable react-refresh/only-export-components */
2+
import { ComponentProps } from "react";
3+
import { cva, cx } from "cva";
4+
5+
import { someIterable } from "../utils";
6+
7+
import { GridCard } from "./Card";
8+
import MetricsChart from "./MetricsChart";
9+
10+
interface ChartPoint {
11+
date: number;
12+
metric: number | null;
13+
}
14+
15+
interface MetricProps<T, K extends keyof T> {
16+
title: string;
17+
description: string;
18+
stream?: Map<number, T>;
19+
metric?: K;
20+
data?: ChartPoint[];
21+
gate?: Map<number, unknown>;
22+
supported?: boolean;
23+
map?: (p: { date: number; metric: number | null }) => ChartPoint;
24+
domain?: [number, number];
25+
unit: string;
26+
heightClassName?: string;
27+
referenceValue?: number;
28+
badge?: ComponentProps<typeof MetricHeader>["badge"];
29+
badgeTheme?: ComponentProps<typeof MetricHeader>["badgeTheme"];
30+
}
31+
32+
/**
33+
* Creates a chart array from a metrics map and a metric name.
34+
*
35+
* @param metrics - Expected to be ordered from oldest to newest.
36+
* @param metricName - Name of the metric to create a chart array for.
37+
*/
38+
export function createChartArray<T, K extends keyof T>(
39+
metrics: Map<number, T>,
40+
metricName: K,
41+
) {
42+
const result: { date: number; metric: number | null }[] = [];
43+
const iter = metrics.entries();
44+
let next = iter.next() as IteratorResult<[number, T]>;
45+
const nowSeconds = Math.floor(Date.now() / 1000);
46+
47+
// We want 120 data points, in the chart.
48+
const firstDate = Math.min(next.value?.[0] ?? nowSeconds, nowSeconds - 120);
49+
50+
for (let t = firstDate; t < nowSeconds; t++) {
51+
while (!next.done && next.value[0] < t) next = iter.next();
52+
const has = !next.done && next.value[0] === t;
53+
54+
let metric = null;
55+
if (has) metric = next.value[1][metricName] as number;
56+
result.push({ date: t, metric });
57+
58+
if (has) next = iter.next();
59+
}
60+
61+
return result;
62+
}
63+
64+
function computeReferenceValue(points: ChartPoint[]): number | undefined {
65+
const values = points
66+
.filter(p => p.metric != null && Number.isFinite(p.metric))
67+
.map(p => Number(p.metric));
68+
69+
if (values.length === 0) return undefined;
70+
71+
const sum = values.reduce((acc, v) => acc + v, 0);
72+
const mean = sum / values.length;
73+
return Math.round(mean);
74+
}
75+
76+
const theme = {
77+
light:
78+
"bg-white text-black border border-slate-800/20 dark:border dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300",
79+
danger: "bg-red-500 dark:border-red-700 dark:bg-red-800 dark:text-red-50",
80+
primary: "bg-blue-500 dark:border-blue-700 dark:bg-blue-800 dark:text-blue-50",
81+
};
82+
83+
interface SettingsItemProps {
84+
readonly title: string;
85+
readonly description: string | React.ReactNode;
86+
readonly badge?: string;
87+
readonly className?: string;
88+
readonly children?: React.ReactNode;
89+
readonly badgeTheme?: keyof typeof theme;
90+
}
91+
92+
export function MetricHeader(props: SettingsItemProps) {
93+
const { title, description, badge } = props;
94+
const badgeVariants = cva({ variants: { theme: theme } });
95+
96+
return (
97+
<div className="space-y-0.5">
98+
<div className="flex items-center gap-x-2">
99+
<div className="flex w-full items-center justify-between text-base font-semibold text-black dark:text-white">
100+
{title}
101+
{badge && (
102+
<span
103+
className={cx(
104+
"ml-2 rounded-sm px-2 py-1 font-mono text-[10px] leading-none font-medium",
105+
badgeVariants({ theme: props.badgeTheme ?? "light" }),
106+
)}
107+
>
108+
{badge}
109+
</span>
110+
)}
111+
</div>
112+
</div>
113+
<div className="text-sm text-slate-700 dark:text-slate-300">{description}</div>
114+
</div>
115+
);
116+
}
117+
118+
export function Metric<T, K extends keyof T>({
119+
title,
120+
description,
121+
stream,
122+
metric,
123+
data,
124+
gate,
125+
supported,
126+
map,
127+
domain = [0, 600],
128+
unit = "",
129+
heightClassName = "h-[127px]",
130+
badge,
131+
badgeTheme,
132+
}: MetricProps<T, K>) {
133+
const ready = gate ? gate.size > 0 : stream ? stream.size > 0 : true;
134+
const supportedFinal =
135+
supported ??
136+
(stream && metric ? someIterable(stream, ([, s]) => s[metric] !== undefined) : true);
137+
138+
// Either we let the consumer provide their own chartArray, or we create one from the stream and metric.
139+
const raw = data ?? ((stream && metric && createChartArray(stream, metric)) || []);
140+
141+
// If the consumer provides a map function, we apply it to the raw data.
142+
const dataFinal: ChartPoint[] = map ? raw.map(map) : raw;
143+
144+
// Compute the average value of the metric.
145+
const referenceValue = computeReferenceValue(dataFinal);
146+
147+
return (
148+
<div className="space-y-2">
149+
<MetricHeader
150+
title={title}
151+
description={description}
152+
badge={badge}
153+
badgeTheme={badgeTheme}
154+
/>
155+
156+
<GridCard>
157+
<div
158+
className={`flex ${heightClassName} w-full items-center justify-center text-sm text-slate-500`}
159+
>
160+
{!ready ? (
161+
<div className="flex flex-col items-center space-y-1">
162+
<p className="text-slate-700">Waiting for data...</p>
163+
</div>
164+
) : supportedFinal ? (
165+
<MetricsChart
166+
data={dataFinal}
167+
domain={domain}
168+
unit={unit}
169+
referenceValue={referenceValue}
170+
/>
171+
) : (
172+
<div className="flex flex-col items-center space-y-1">
173+
<p className="text-black">Metric not supported</p>
174+
</div>
175+
)}
176+
</div>
177+
</GridCard>
178+
</div>
179+
);
180+
}

ui/src/components/StatChart.tsx renamed to ui/src/components/MetricsChart.tsx

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ import {
1212

1313
import CustomTooltip, { CustomTooltipProps } from "@components/CustomTooltip";
1414

15-
export default function StatChart({
15+
export default function MetricsChart({
1616
data,
1717
domain,
1818
unit,
1919
referenceValue,
2020
}: {
21-
data: { date: number; stat: number | null | undefined }[];
21+
data: { date: number; metric: number | null | undefined }[];
2222
domain?: [string | number, string | number];
2323
unit?: string;
2424
referenceValue?: number;
@@ -33,7 +33,7 @@ export default function StatChart({
3333
strokeLinecap="butt"
3434
stroke="rgba(30, 41, 59, 0.1)"
3535
/>
36-
{referenceValue && (
36+
{referenceValue !== undefined && (
3737
<ReferenceLine
3838
y={referenceValue}
3939
strokeDasharray="3 3"
@@ -64,7 +64,7 @@ export default function StatChart({
6464
.map(x => x.date)}
6565
/>
6666
<YAxis
67-
dataKey="stat"
67+
dataKey="metric"
6868
axisLine={false}
6969
orientation="right"
7070
tick={{
@@ -73,6 +73,7 @@ export default function StatChart({
7373
fill: "rgba(107, 114, 128, 1)",
7474
}}
7575
padding={{ top: 0, bottom: 0 }}
76+
allowDecimals
7677
tickLine={false}
7778
unit={unit}
7879
domain={domain || ["auto", "auto"]}
@@ -87,7 +88,7 @@ export default function StatChart({
8788
<Line
8889
type="monotone"
8990
isAnimationActive={false}
90-
dataKey="stat"
91+
dataKey="metric"
9192
stroke="rgb(29 78 216)"
9293
strokeLinecap="round"
9394
strokeWidth={2}

0 commit comments

Comments
 (0)