admin: the overview's last bucket is drawn as still counting; the query detail names the device as the activity list does
This commit is contained in:
@@ -6,8 +6,9 @@
|
||||
* the reader is looking at.
|
||||
*/
|
||||
|
||||
import { useId } from "react";
|
||||
import * as stylex from "@stylexjs/stylex";
|
||||
import { formatCount, formatTime } from "@/lib/format";
|
||||
import { formatCount } from "@/lib/format";
|
||||
import type { Bucket } from "@/lib/types";
|
||||
import { styles as shared } from "@/ui/styles";
|
||||
import { colors } from "@/ui/tokens.stylex";
|
||||
@@ -19,11 +20,15 @@ import {
|
||||
ChartTooltip,
|
||||
EmptyChart,
|
||||
HitBands,
|
||||
STILL_COUNTING_DASH,
|
||||
STILL_COUNTING_OPACITY,
|
||||
areaPath,
|
||||
bucketTitle,
|
||||
labelTickValues,
|
||||
plotArea,
|
||||
pointScale,
|
||||
smoothPath,
|
||||
svgId,
|
||||
useActiveIndex,
|
||||
useMeasuredWidth,
|
||||
valueScale,
|
||||
@@ -75,9 +80,9 @@ function columnsOf(buckets: Bucket[]): Column[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function tooltipOf(column: Column): TooltipContent {
|
||||
function tooltipOf(column: Column, stillCounting: boolean): TooltipContent {
|
||||
return {
|
||||
title: formatTime(column.ts),
|
||||
title: bucketTitle(column.ts, stillCounting),
|
||||
rows: [
|
||||
...SERIES.map((series) => ({
|
||||
key: series.key,
|
||||
@@ -105,6 +110,7 @@ export default function TimeseriesChart({ data }: { data: TimeseriesData }) {
|
||||
// A hover survives a re-render only while it still names the same bucket at
|
||||
// the same place: a poll that rolls the window, or a resize, retires it.
|
||||
const hovered = useActiveIndex(`${data.since}:${data.bucket_seconds}:${data.buckets.length}:${width}`);
|
||||
const clipId = svgId(useId());
|
||||
|
||||
if (data.buckets.length === 0 || data.buckets.every((bucket) => bucket.queries === 0)) {
|
||||
return <EmptyChart containerRef={containerRef} />;
|
||||
@@ -120,12 +126,16 @@ export default function TimeseriesChart({ data }: { data: TimeseriesData }) {
|
||||
const centers = timestamps.map((ts) => xScale(ts) ?? plot.x);
|
||||
const pointsOf = (key: SeriesKey): [number, number][] =>
|
||||
columns.map((column, i) => [centers[i], yScale(column[key])]);
|
||||
const last = columns.length - 1;
|
||||
// The curve into the last point belongs to the bucket that is still
|
||||
// filling, so everything right of the point before it is drawn as such.
|
||||
const settledEdge = last > 0 ? centers[last - 1] : plot.x;
|
||||
|
||||
return (
|
||||
<ChartRoot containerRef={containerRef}>
|
||||
<svg
|
||||
role="img"
|
||||
aria-label={`Queries over time, ${formatCount(data.buckets.length)} buckets: total and blocked queries per bucket`}
|
||||
aria-label={`Queries over time, ${formatCount(data.buckets.length)} buckets: total and blocked queries per bucket, the last still counting`}
|
||||
width="100%"
|
||||
height={CHART_HEIGHT}
|
||||
viewBox={`0 0 ${width} ${CHART_HEIGHT}`}
|
||||
@@ -139,23 +149,45 @@ export default function TimeseriesChart({ data }: { data: TimeseriesData }) {
|
||||
xTickValues={labelTickValues(timestamps, plot.width)}
|
||||
bucketSeconds={data.bucket_seconds}
|
||||
/>
|
||||
<defs>
|
||||
<clipPath id={`${clipId}-settled`}>
|
||||
<rect x={0} y={0} width={settledEdge} height={CHART_HEIGHT} />
|
||||
</clipPath>
|
||||
<clipPath id={`${clipId}-counting`}>
|
||||
<rect x={settledEdge} y={0} width={Math.max(0, width - settledEdge)} height={CHART_HEIGHT} />
|
||||
</clipPath>
|
||||
</defs>
|
||||
{SERIES.map((series) => {
|
||||
const points = pointsOf(series.key);
|
||||
const line = smoothPath(points, plot.y, plot.bottom);
|
||||
const area = areaPath(line, points, plot);
|
||||
return (
|
||||
<g key={series.key} data-series={series.key}>
|
||||
<path
|
||||
d={areaPath(line, points, plot)}
|
||||
fill={series.color}
|
||||
fillOpacity={series.fillOpacity}
|
||||
/>
|
||||
<path
|
||||
d={line}
|
||||
fill="none"
|
||||
stroke={series.color}
|
||||
strokeWidth={LINE_WIDTH}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
<g clipPath={`url(#${clipId}-settled)`}>
|
||||
<path d={area} fill={series.color} fillOpacity={series.fillOpacity} />
|
||||
<path
|
||||
d={line}
|
||||
fill="none"
|
||||
stroke={series.color}
|
||||
strokeWidth={LINE_WIDTH}
|
||||
strokeLinejoin="round"
|
||||
/>
|
||||
</g>
|
||||
<g clipPath={`url(#${clipId}-counting)`} data-still-counting="">
|
||||
<path
|
||||
d={area}
|
||||
fill={series.color}
|
||||
fillOpacity={series.fillOpacity * STILL_COUNTING_OPACITY}
|
||||
/>
|
||||
<path
|
||||
d={line}
|
||||
fill="none"
|
||||
stroke={series.color}
|
||||
strokeWidth={LINE_WIDTH}
|
||||
strokeLinejoin="round"
|
||||
strokeDasharray={STILL_COUNTING_DASH}
|
||||
/>
|
||||
</g>
|
||||
</g>
|
||||
);
|
||||
})}
|
||||
@@ -185,7 +217,7 @@ export default function TimeseriesChart({ data }: { data: TimeseriesData }) {
|
||||
{hovered.index !== null && (
|
||||
<ChartTooltip
|
||||
index={hovered.index}
|
||||
content={tooltipOf(columns[hovered.index])}
|
||||
content={tooltipOf(columns[hovered.index], hovered.index === last)}
|
||||
left={centers[hovered.index]}
|
||||
top={yScale(columns[hovered.index].queries)}
|
||||
beside
|
||||
@@ -206,9 +238,9 @@ export default function TimeseriesChart({ data }: { data: TimeseriesData }) {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{columns.map((column) => (
|
||||
{columns.map((column, index) => (
|
||||
<tr key={column.ts}>
|
||||
<th scope="row">{formatTime(column.ts)}</th>
|
||||
<th scope="row">{bucketTitle(column.ts, index === last)}</th>
|
||||
<td>{formatCount(column.queries)}</td>
|
||||
<td>{formatCount(column.blocked)}</td>
|
||||
<td>{formatCount(column.allowed)}</td>
|
||||
|
||||
Reference in New Issue
Block a user