mirror of
https://github.com/prometheus/prometheus.git
synced 2025-01-12 06:17:27 -08:00
A lot of work around uPlot and resolution picking
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
1c91b82206
commit
8ef66c41a0
|
@ -14,5 +14,7 @@ module.exports = {
|
||||||
'warn',
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
|
'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
|
||||||
|
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import {
|
||||||
PromQLExtension,
|
PromQLExtension,
|
||||||
newCompleteStrategy,
|
newCompleteStrategy,
|
||||||
} from "@prometheus-io/codemirror-promql";
|
} from "@prometheus-io/codemirror-promql";
|
||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useMemo, useState } from "react";
|
||||||
import CodeMirror, {
|
import CodeMirror, {
|
||||||
EditorState,
|
EditorState,
|
||||||
EditorView,
|
EditorView,
|
||||||
|
@ -107,10 +107,6 @@ export class HistoryCompleteStrategy implements CompleteStrategy {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This is just a placeholder until query history is implemented, so disable the linter warning.
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
const queryHistory = [] as string[];
|
|
||||||
|
|
||||||
interface ExpressionInputProps {
|
interface ExpressionInputProps {
|
||||||
initialExpr: string;
|
initialExpr: string;
|
||||||
metricNames: string[];
|
metricNames: string[];
|
||||||
|
@ -163,13 +159,15 @@ const ExpressionInput: FC<ExpressionInputProps> = ({
|
||||||
if (formatResult) {
|
if (formatResult) {
|
||||||
setExpr(formatResult.data);
|
setExpr(formatResult.data);
|
||||||
notifications.show({
|
notifications.show({
|
||||||
color: "green",
|
|
||||||
title: "Expression formatted",
|
title: "Expression formatted",
|
||||||
message: "Expression formatted successfully!",
|
message: "Expression formatted successfully!",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [formatResult, formatError]);
|
}, [formatResult, formatError]);
|
||||||
|
|
||||||
|
// This is just a placeholder until query history is implemented, so disable the linter warning.
|
||||||
|
const queryHistory = useMemo<string[]>(() => [], []);
|
||||||
|
|
||||||
// (Re)initialize editor based on settings / setting changes.
|
// (Re)initialize editor based on settings / setting changes.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Build the dynamic part of the config.
|
// Build the dynamic part of the config.
|
||||||
|
|
|
@ -1,29 +1,24 @@
|
||||||
import { FC, useEffect, useId, useLayoutEffect, useState } from "react";
|
import { FC, useEffect, useId, useState } from "react";
|
||||||
import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
|
import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
|
||||||
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
||||||
import {
|
import { InstantQueryResult } from "../../api/responseTypes/query";
|
||||||
InstantQueryResult,
|
|
||||||
RangeSamples,
|
|
||||||
} from "../../api/responseTypes/query";
|
|
||||||
import { useAPIQuery } from "../../api/api";
|
import { useAPIQuery } from "../../api/api";
|
||||||
import classes from "./Graph.module.css";
|
import classes from "./Graph.module.css";
|
||||||
import { GraphDisplayMode } from "../../state/queryPageSlice";
|
import {
|
||||||
import { formatSeries } from "../../lib/formatSeries";
|
GraphDisplayMode,
|
||||||
import uPlot, { Series } from "uplot";
|
GraphResolution,
|
||||||
import UplotReact from "uplot-react";
|
getEffectiveResolution,
|
||||||
|
} from "../../state/queryPageSlice";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import "./uplot.css";
|
import "./uplot.css";
|
||||||
import { useElementSize } from "@mantine/hooks";
|
import { useElementSize } from "@mantine/hooks";
|
||||||
import { formatTimestamp } from "../../lib/formatTime";
|
import UPlotChart, { UPlotChartRange } from "./UPlotChart";
|
||||||
import { computePosition, shift, flip, offset, Axis } from "@floating-ui/dom";
|
|
||||||
import { colorPool } from "./ColorPool";
|
|
||||||
import UPlotChart, { UPlotChartProps, UPlotChartRange } from "./UPlotChart";
|
|
||||||
|
|
||||||
export interface GraphProps {
|
export interface GraphProps {
|
||||||
expr: string;
|
expr: string;
|
||||||
endTime: number | null;
|
endTime: number | null;
|
||||||
range: number;
|
range: number;
|
||||||
resolution: number | null;
|
resolution: GraphResolution;
|
||||||
showExemplars: boolean;
|
showExemplars: boolean;
|
||||||
displayMode: GraphDisplayMode;
|
displayMode: GraphDisplayMode;
|
||||||
retriggerIdx: number;
|
retriggerIdx: number;
|
||||||
|
@ -45,8 +40,7 @@ const Graph: FC<GraphProps> = ({
|
||||||
|
|
||||||
const effectiveEndTime = (endTime !== null ? endTime : Date.now()) / 1000;
|
const effectiveEndTime = (endTime !== null ? endTime : Date.now()) / 1000;
|
||||||
const startTime = effectiveEndTime - range / 1000;
|
const startTime = effectiveEndTime - range / 1000;
|
||||||
const effectiveResolution =
|
const effectiveResolution = getEffectiveResolution(resolution, range) / 1000;
|
||||||
resolution || Math.max(Math.floor(range / 250000), 1);
|
|
||||||
|
|
||||||
const { data, error, isFetching, isLoading, refetch } =
|
const { data, error, isFetching, isLoading, refetch } =
|
||||||
useAPIQuery<InstantQueryResult>({
|
useAPIQuery<InstantQueryResult>({
|
||||||
|
@ -62,7 +56,7 @@ const Graph: FC<GraphProps> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
// Keep the displayed chart range separate from the actual query range, so that
|
// Keep the displayed chart range separate from the actual query range, so that
|
||||||
// the chart will keep displaying the old range while a new query for a different range
|
// the chart will keep displaying the old range while a query for a new range
|
||||||
// is still in progress.
|
// is still in progress.
|
||||||
const [displayedChartRange, setDisplayedChartRange] =
|
const [displayedChartRange, setDisplayedChartRange] =
|
||||||
useState<UPlotChartRange>({
|
useState<UPlotChartRange>({
|
||||||
|
@ -77,6 +71,8 @@ const Graph: FC<GraphProps> = ({
|
||||||
endTime: effectiveEndTime,
|
endTime: effectiveEndTime,
|
||||||
resolution: effectiveResolution,
|
resolution: effectiveResolution,
|
||||||
});
|
});
|
||||||
|
// We actually want to update the displayed range only once the new data is there.
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
@ -4,10 +4,10 @@ import {
|
||||||
Center,
|
Center,
|
||||||
Space,
|
Space,
|
||||||
Box,
|
Box,
|
||||||
Input,
|
|
||||||
SegmentedControl,
|
SegmentedControl,
|
||||||
Stack,
|
Stack,
|
||||||
Select,
|
Select,
|
||||||
|
TextInput,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import {
|
import {
|
||||||
IconChartAreaFilled,
|
IconChartAreaFilled,
|
||||||
|
@ -20,6 +20,8 @@ import { FC, useState } from "react";
|
||||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||||
import {
|
import {
|
||||||
GraphDisplayMode,
|
GraphDisplayMode,
|
||||||
|
GraphResolution,
|
||||||
|
getEffectiveResolution,
|
||||||
removePanel,
|
removePanel,
|
||||||
setExpr,
|
setExpr,
|
||||||
setVisualizer,
|
setVisualizer,
|
||||||
|
@ -29,6 +31,10 @@ import TimeInput from "./TimeInput";
|
||||||
import RangeInput from "./RangeInput";
|
import RangeInput from "./RangeInput";
|
||||||
import ExpressionInput from "./ExpressionInput";
|
import ExpressionInput from "./ExpressionInput";
|
||||||
import Graph from "./Graph";
|
import Graph from "./Graph";
|
||||||
|
import {
|
||||||
|
formatPrometheusDuration,
|
||||||
|
parsePrometheusDuration,
|
||||||
|
} from "../../lib/formatTime";
|
||||||
|
|
||||||
export interface PanelProps {
|
export interface PanelProps {
|
||||||
idx: number;
|
idx: number;
|
||||||
|
@ -45,8 +51,40 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
|
||||||
const [retriggerIdx, setRetriggerIdx] = useState<number>(0);
|
const [retriggerIdx, setRetriggerIdx] = useState<number>(0);
|
||||||
|
|
||||||
const panel = useAppSelector((state) => state.queryPage.panels[idx]);
|
const panel = useAppSelector((state) => state.queryPage.panels[idx]);
|
||||||
|
const resolution = panel.visualizer.resolution;
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
|
|
||||||
|
const [customResolutionInput, setCustomResolutionInput] = useState<string>(
|
||||||
|
formatPrometheusDuration(
|
||||||
|
getEffectiveResolution(resolution, panel.visualizer.range)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
const setResolution = (res: GraphResolution) => {
|
||||||
|
dispatch(
|
||||||
|
setVisualizer({
|
||||||
|
idx,
|
||||||
|
visualizer: {
|
||||||
|
...panel.visualizer,
|
||||||
|
resolution: res,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onChangeCustomResolutionInput = (resText: string): void => {
|
||||||
|
const newResolution = parsePrometheusDuration(resText);
|
||||||
|
if (newResolution === null) {
|
||||||
|
setCustomResolutionInput(
|
||||||
|
formatPrometheusDuration(
|
||||||
|
getEffectiveResolution(resolution, panel.visualizer.range)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
setResolution({ type: "custom", value: newResolution });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<ExpressionInput
|
<ExpressionInput
|
||||||
|
@ -124,52 +162,99 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
|
title="Resolution"
|
||||||
placeholder="Resolution"
|
placeholder="Resolution"
|
||||||
maxDropdownHeight={500}
|
maxDropdownHeight={500}
|
||||||
data={[
|
data={[
|
||||||
{
|
{
|
||||||
group: "Automatic resolution",
|
group: "Automatic resolution",
|
||||||
items: [
|
items: [
|
||||||
{ label: "Low", value: "low" },
|
{ label: "Low res.", value: "low" },
|
||||||
{ label: "Medium", value: "medium" },
|
{ label: "Medium res.", value: "medium" },
|
||||||
{ label: "High", value: "high" },
|
{ label: "High res.", value: "high" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "Fixed resolution",
|
group: "Fixed resolution",
|
||||||
items: [
|
items: [
|
||||||
{ label: "10s", value: "10" },
|
{ label: "10s", value: "10000" },
|
||||||
{ label: "30s", value: "30" },
|
{ label: "30s", value: "30000" },
|
||||||
{ label: "1m", value: "60" },
|
{ label: "1m", value: "60000" },
|
||||||
{ label: "5m", value: "300" },
|
{ label: "5m", value: "300000" },
|
||||||
{ label: "15m", value: "900" },
|
{ label: "15m", value: "900000" },
|
||||||
{ label: "1h", value: "3600" },
|
{ label: "1h", value: "3600000" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
group: "Custom resolution",
|
group: "Custom resolution",
|
||||||
items: [{ label: "Enter value", value: "custom" }],
|
items: [{ label: "Enter value...", value: "custom" }],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
w={160}
|
w={160}
|
||||||
// value={value ? value.value : null}
|
value={
|
||||||
|
resolution.type === "auto"
|
||||||
|
? resolution.density
|
||||||
|
: resolution.type === "fixed"
|
||||||
|
? resolution.value.toString()
|
||||||
|
: "custom"
|
||||||
|
}
|
||||||
onChange={(_value, option) => {
|
onChange={(_value, option) => {
|
||||||
dispatch(
|
if (["low", "medium", "high"].includes(option.value)) {
|
||||||
setVisualizer({
|
setResolution({
|
||||||
idx,
|
type: "auto",
|
||||||
visualizer: {
|
density: option.value as "low" | "medium" | "high",
|
||||||
...panel.visualizer,
|
});
|
||||||
resolution: option
|
return;
|
||||||
? option.value
|
}
|
||||||
? parseInt(option.value)
|
|
||||||
: null
|
if (option.value === "custom") {
|
||||||
: null,
|
// Start the custom resolution at the current effective resolution.
|
||||||
},
|
const effectiveResolution = getEffectiveResolution(
|
||||||
})
|
resolution,
|
||||||
);
|
panel.visualizer.range
|
||||||
|
);
|
||||||
|
setResolution({
|
||||||
|
type: "custom",
|
||||||
|
value: effectiveResolution,
|
||||||
|
});
|
||||||
|
setCustomResolutionInput(
|
||||||
|
formatPrometheusDuration(effectiveResolution)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = parseInt(option.value);
|
||||||
|
if (!isNaN(value)) {
|
||||||
|
setResolution({
|
||||||
|
type: "fixed",
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
throw new Error("Invalid resolution value");
|
||||||
|
}
|
||||||
}}
|
}}
|
||||||
clearable
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{resolution.type === "custom" && (
|
||||||
|
<TextInput
|
||||||
|
placeholder="Resolution"
|
||||||
|
value={customResolutionInput}
|
||||||
|
onChange={(event) =>
|
||||||
|
setCustomResolutionInput(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
onBlur={() =>
|
||||||
|
onChangeCustomResolutionInput(customResolutionInput)
|
||||||
|
}
|
||||||
|
onKeyDown={(event) =>
|
||||||
|
event.key === "Enter" &&
|
||||||
|
onChangeCustomResolutionInput(customResolutionInput)
|
||||||
|
}
|
||||||
|
aria-label="Range"
|
||||||
|
style={{
|
||||||
|
width: `calc(44px + ${customResolutionInput.length + 3}ch)`,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
<SegmentedControl
|
<SegmentedControl
|
||||||
|
|
|
@ -79,6 +79,7 @@ const RangeInput: FC<RangeInputProps> = ({ range, onChangeRange }) => {
|
||||||
return (
|
return (
|
||||||
<Group gap={5}>
|
<Group gap={5}>
|
||||||
<Input
|
<Input
|
||||||
|
title="Range"
|
||||||
value={rangeInput}
|
value={rangeInput}
|
||||||
onChange={(event) => setRangeInput(event.currentTarget.value)}
|
onChange={(event) => setRangeInput(event.currentTarget.value)}
|
||||||
onBlur={() => onChangeRangeInput(rangeInput)}
|
onBlur={() => onChangeRangeInput(rangeInput)}
|
||||||
|
|
|
@ -26,6 +26,7 @@ const TimeInput: FC<TimeInputProps> = ({
|
||||||
<Group gap={5}>
|
<Group gap={5}>
|
||||||
<DatesProvider settings={{ timezone: useLocalTime ? undefined : "UTC" }}>
|
<DatesProvider settings={{ timezone: useLocalTime ? undefined : "UTC" }}>
|
||||||
<DateTimePicker
|
<DateTimePicker
|
||||||
|
title="End time"
|
||||||
w={230}
|
w={230}
|
||||||
valueFormat="YYYY-MM-DD HH:mm:ss"
|
valueFormat="YYYY-MM-DD HH:mm:ss"
|
||||||
withSeconds
|
withSeconds
|
||||||
|
|
|
@ -1,11 +1,5 @@
|
||||||
import { FC, useEffect, useId, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
|
import { RangeSamples } from "../../api/responseTypes/query";
|
||||||
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
|
||||||
import {
|
|
||||||
InstantQueryResult,
|
|
||||||
RangeSamples,
|
|
||||||
} from "../../api/responseTypes/query";
|
|
||||||
import { useAPIQuery } from "../../api/api";
|
|
||||||
import classes from "./Graph.module.css";
|
import classes from "./Graph.module.css";
|
||||||
import { GraphDisplayMode } from "../../state/queryPageSlice";
|
import { GraphDisplayMode } from "../../state/queryPageSlice";
|
||||||
import { formatSeries } from "../../lib/formatSeries";
|
import { formatSeries } from "../../lib/formatSeries";
|
||||||
|
@ -13,9 +7,8 @@ import uPlot, { Series } from "uplot";
|
||||||
import UplotReact from "uplot-react";
|
import UplotReact from "uplot-react";
|
||||||
import "uplot/dist/uPlot.min.css";
|
import "uplot/dist/uPlot.min.css";
|
||||||
import "./uplot.css";
|
import "./uplot.css";
|
||||||
import { useElementSize } from "@mantine/hooks";
|
|
||||||
import { formatTimestamp } from "../../lib/formatTime";
|
import { formatTimestamp } from "../../lib/formatTime";
|
||||||
import { computePosition, shift, flip, offset, Axis } from "@floating-ui/dom";
|
import { computePosition, shift, flip, offset } from "@floating-ui/dom";
|
||||||
import { colorPool } from "./ColorPool";
|
import { colorPool } from "./ColorPool";
|
||||||
|
|
||||||
const formatYAxisTickValue = (y: number | null): string => {
|
const formatYAxisTickValue = (y: number | null): string => {
|
||||||
|
@ -132,7 +125,7 @@ const tooltipPlugin = () => {
|
||||||
},
|
},
|
||||||
// When a series is selected by hovering close to it, store the
|
// When a series is selected by hovering close to it, store the
|
||||||
// index of the selected series.
|
// index of the selected series.
|
||||||
setSeries: (self: uPlot, seriesIdx: number | null, opts: Series) => {
|
setSeries: (_u: uPlot, seriesIdx: number | null, _opts: Series) => {
|
||||||
selectedSeriesIdx = seriesIdx;
|
selectedSeriesIdx = seriesIdx;
|
||||||
},
|
},
|
||||||
// When the cursor is moved, update the tooltip with the current
|
// When the cursor is moved, update the tooltip with the current
|
||||||
|
@ -155,8 +148,12 @@ const tooltipPlugin = () => {
|
||||||
const ts = u.data[0][idx];
|
const ts = u.data[0][idx];
|
||||||
const value = u.data[selectedSeriesIdx][idx];
|
const value = u.data[selectedSeriesIdx][idx];
|
||||||
const series = u.series[selectedSeriesIdx];
|
const series = u.series[selectedSeriesIdx];
|
||||||
|
// @ts-expect-error - uPlot doesn't have a field for labels, but we just attach some anyway.
|
||||||
const labels = series.labels;
|
const labels = series.labels;
|
||||||
const color = series.stroke();
|
if (typeof series.stroke !== "function") {
|
||||||
|
throw new Error("series.stroke is not a function");
|
||||||
|
}
|
||||||
|
const color = series.stroke(u, selectedSeriesIdx);
|
||||||
|
|
||||||
const x = left + boundingLeft;
|
const x = left + boundingLeft;
|
||||||
const y = top + boundingTop;
|
const y = top + boundingTop;
|
||||||
|
@ -205,29 +202,31 @@ const tooltipPlugin = () => {
|
||||||
// A helper function to automatically create enough space for the Y axis
|
// A helper function to automatically create enough space for the Y axis
|
||||||
// ticket labels depending on their length.
|
// ticket labels depending on their length.
|
||||||
const autoPadLeft = (
|
const autoPadLeft = (
|
||||||
self: uPlot,
|
u: uPlot,
|
||||||
values: string[],
|
values: string[],
|
||||||
axisIdx: number,
|
axisIdx: number,
|
||||||
cycleNum: number
|
cycleNum: number
|
||||||
) => {
|
) => {
|
||||||
const axis = self.axes[axisIdx];
|
const axis = u.axes[axisIdx];
|
||||||
|
|
||||||
// bail out, force convergence
|
// bail out, force convergence
|
||||||
if (cycleNum > 1) {
|
if (cycleNum > 1) {
|
||||||
|
// @ts-expect-error - got this from a uPlot demo example, not sure if it's correct.
|
||||||
return axis._size;
|
return axis._size;
|
||||||
}
|
}
|
||||||
|
|
||||||
let axisSize = axis.ticks.size + axis.gap;
|
let axisSize = axis.ticks!.size! + axis.gap!;
|
||||||
|
|
||||||
// find longest value
|
// Find longest tick text.
|
||||||
const longestVal = (values ?? []).reduce(
|
const longestVal = (values ?? []).reduce(
|
||||||
(acc, val) => (val.length > acc.length ? val : acc),
|
(acc, val) => (val.length > acc.length ? val : acc),
|
||||||
""
|
""
|
||||||
);
|
);
|
||||||
|
|
||||||
if (longestVal != "") {
|
if (longestVal != "") {
|
||||||
self.ctx.font = axis.font[0];
|
console.log("axis.font", axis.font![0]);
|
||||||
axisSize += self.ctx.measureText(longestVal).width / devicePixelRatio;
|
u.ctx.font = axis.font![0];
|
||||||
|
axisSize += u.ctx.measureText(longestVal).width / devicePixelRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Math.ceil(axisSize);
|
return Math.ceil(axisSize);
|
||||||
|
@ -236,7 +235,7 @@ const autoPadLeft = (
|
||||||
const getOptions = (
|
const getOptions = (
|
||||||
width: number,
|
width: number,
|
||||||
result: RangeSamples[],
|
result: RangeSamples[],
|
||||||
onSelectRange: (start: number, end: number) => void
|
onSelectRange: (_start: number, _end: number) => void
|
||||||
): uPlot.Options => ({
|
): uPlot.Options => ({
|
||||||
width: width - 30,
|
width: width - 30,
|
||||||
height: 550,
|
height: 550,
|
||||||
|
@ -258,7 +257,7 @@ const getOptions = (
|
||||||
live: false,
|
live: false,
|
||||||
markers: {
|
markers: {
|
||||||
fill: (
|
fill: (
|
||||||
self: uPlot,
|
_u: uPlot,
|
||||||
seriesIdx: number
|
seriesIdx: number
|
||||||
): CSSStyleDeclaration["borderColor"] => {
|
): CSSStyleDeclaration["borderColor"] => {
|
||||||
return colorPool[seriesIdx % colorPool.length];
|
return colorPool[seriesIdx % colorPool.length];
|
||||||
|
@ -285,7 +284,7 @@ const getOptions = (
|
||||||
},
|
},
|
||||||
// Y axis (sample value).
|
// Y axis (sample value).
|
||||||
{
|
{
|
||||||
values: (u: uPlot, splits: number[]) => splits.map(formatYAxisTickValue),
|
values: (_u: uPlot, splits: number[]) => splits.map(formatYAxisTickValue),
|
||||||
border: {
|
border: {
|
||||||
show: true,
|
show: true,
|
||||||
stroke: "#333",
|
stroke: "#333",
|
||||||
|
@ -298,6 +297,7 @@ const getOptions = (
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
series: [
|
series: [
|
||||||
|
{},
|
||||||
...result.map((r, idx) => ({
|
...result.map((r, idx) => ({
|
||||||
label: formatSeries(r.metric),
|
label: formatSeries(r.metric),
|
||||||
width: 2,
|
width: 2,
|
||||||
|
@ -323,44 +323,43 @@ export const normalizeData = (
|
||||||
endTime: number,
|
endTime: number,
|
||||||
resolution: number
|
resolution: number
|
||||||
): uPlot.AlignedData => {
|
): uPlot.AlignedData => {
|
||||||
const timeData: (number | null)[][] = [];
|
const timeData: number[] = [];
|
||||||
timeData[0] = [];
|
|
||||||
for (let t = startTime; t <= endTime; t += resolution) {
|
for (let t = startTime; t <= endTime; t += resolution) {
|
||||||
timeData[0].push(t);
|
timeData.push(t);
|
||||||
}
|
}
|
||||||
|
|
||||||
return timeData.concat(
|
const values = inputData.map(({ values, histograms }) => {
|
||||||
inputData.map(({ values, histograms }) => {
|
// Insert nulls for all missing steps.
|
||||||
// Insert nulls for all missing steps.
|
const data: (number | null)[] = [];
|
||||||
const data: (number | null)[] = [];
|
let valuePos = 0;
|
||||||
let valuePos = 0;
|
let histogramPos = 0;
|
||||||
let histogramPos = 0;
|
|
||||||
|
|
||||||
for (let t = startTime; t <= endTime; t += resolution) {
|
for (let t = startTime; t <= endTime; t += resolution) {
|
||||||
// Allow for floating point inaccuracy.
|
// Allow for floating point inaccuracy.
|
||||||
const currentValue = values && values[valuePos];
|
const currentValue = values && values[valuePos];
|
||||||
const currentHistogram = histograms && histograms[histogramPos];
|
const currentHistogram = histograms && histograms[histogramPos];
|
||||||
if (
|
if (
|
||||||
currentValue &&
|
currentValue &&
|
||||||
values.length > valuePos &&
|
values.length > valuePos &&
|
||||||
currentValue[0] < t + resolution / 100
|
currentValue[0] < t + resolution / 100
|
||||||
) {
|
) {
|
||||||
data.push(parseValue(currentValue[1]));
|
data.push(parseValue(currentValue[1]));
|
||||||
valuePos++;
|
valuePos++;
|
||||||
} else if (
|
} else if (
|
||||||
currentHistogram &&
|
currentHistogram &&
|
||||||
histograms.length > histogramPos &&
|
histograms.length > histogramPos &&
|
||||||
currentHistogram[0] < t + resolution / 100
|
currentHistogram[0] < t + resolution / 100
|
||||||
) {
|
) {
|
||||||
data.push(parseValue(currentHistogram[1].sum));
|
data.push(parseValue(currentHistogram[1].sum));
|
||||||
histogramPos++;
|
histogramPos++;
|
||||||
} else {
|
} else {
|
||||||
data.push(null);
|
data.push(null);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return data;
|
}
|
||||||
})
|
return data;
|
||||||
);
|
});
|
||||||
|
|
||||||
|
return [timeData, ...values];
|
||||||
};
|
};
|
||||||
|
|
||||||
const parseValue = (value: string): null | number => {
|
const parseValue = (value: string): null | number => {
|
||||||
|
@ -388,7 +387,6 @@ export interface UPlotChartProps {
|
||||||
const UPlotChart: FC<UPlotChartProps> = ({
|
const UPlotChart: FC<UPlotChartProps> = ({
|
||||||
data,
|
data,
|
||||||
range: { startTime, endTime, resolution },
|
range: { startTime, endTime, resolution },
|
||||||
displayMode,
|
|
||||||
width,
|
width,
|
||||||
onSelectRange,
|
onSelectRange,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
|
@ -7,14 +7,49 @@ export enum GraphDisplayMode {
|
||||||
Heatmap = "heatmap",
|
Heatmap = "heatmap",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GraphResolution =
|
||||||
|
| {
|
||||||
|
type: "auto";
|
||||||
|
density: "low" | "medium" | "high";
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "fixed";
|
||||||
|
value: number; // Resolution step in milliseconds.
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "custom";
|
||||||
|
value: number; // Resolution step in milliseconds.
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEffectiveResolution = (
|
||||||
|
resolution: GraphResolution,
|
||||||
|
range: number
|
||||||
|
) => {
|
||||||
|
switch (resolution.type) {
|
||||||
|
case "auto": {
|
||||||
|
const factor =
|
||||||
|
resolution.density === "high"
|
||||||
|
? 750
|
||||||
|
: resolution.density === "medium"
|
||||||
|
? 250
|
||||||
|
: 100;
|
||||||
|
return Math.max(Math.floor(range / factor), 1);
|
||||||
|
}
|
||||||
|
case "fixed":
|
||||||
|
return resolution.value; // TODO: Scope this to a list?
|
||||||
|
case "custom":
|
||||||
|
return resolution.value;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// NOTE: This is not represented as a discriminated union type
|
// NOTE: This is not represented as a discriminated union type
|
||||||
// because we want to preserve and partially share settings while
|
// because we want to preserve and partially share settings while
|
||||||
// switching between display modes.
|
// switching between display modes.
|
||||||
export interface Visualizer {
|
export interface Visualizer {
|
||||||
activeTab: "table" | "graph" | "explain";
|
activeTab: "table" | "graph" | "explain";
|
||||||
endTime: number | null; // Timestamp in milliseconds.
|
endTime: number | null; // Timestamp in milliseconds.
|
||||||
range: number; // Range in seconds.
|
range: number; // Range in milliseconds.
|
||||||
resolution: number | null; // Resolution step in seconds.
|
resolution: GraphResolution;
|
||||||
displayMode: GraphDisplayMode;
|
displayMode: GraphDisplayMode;
|
||||||
showExemplars: boolean;
|
showExemplars: boolean;
|
||||||
}
|
}
|
||||||
|
@ -41,7 +76,7 @@ const newDefaultPanel = (): Panel => ({
|
||||||
activeTab: "table",
|
activeTab: "table",
|
||||||
endTime: null,
|
endTime: null,
|
||||||
range: 3600 * 1000,
|
range: 3600 * 1000,
|
||||||
resolution: null,
|
resolution: { type: "auto", density: "medium" },
|
||||||
displayMode: GraphDisplayMode.Lines,
|
displayMode: GraphDisplayMode.Lines,
|
||||||
showExemplars: false,
|
showExemplars: false,
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in a new issue