mirror of
https://github.com/prometheus/prometheus.git
synced 2024-12-25 13:44:05 -08:00
ECharts experiment
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
ea54dac130
commit
9252826659
|
@ -27,8 +27,11 @@
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
"@tabler/icons-react": "^2.47.0",
|
"@tabler/icons-react": "^2.47.0",
|
||||||
"@tanstack/react-query": "^5.22.2",
|
"@tanstack/react-query": "^5.22.2",
|
||||||
|
"@types/lodash": "^4.17.7",
|
||||||
"@uiw/react-codemirror": "^4.21.22",
|
"@uiw/react-codemirror": "^4.21.22",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
|
|
@ -27,10 +27,7 @@ import {
|
||||||
IconChevronDown,
|
IconChevronDown,
|
||||||
IconChevronRight,
|
IconChevronRight,
|
||||||
IconCloudDataConnection,
|
IconCloudDataConnection,
|
||||||
IconCpu,
|
|
||||||
IconDatabase,
|
IconDatabase,
|
||||||
IconDatabaseSearch,
|
|
||||||
IconFileAnalytics,
|
|
||||||
IconFlag,
|
IconFlag,
|
||||||
IconHeartRateMonitor,
|
IconHeartRateMonitor,
|
||||||
IconInfoCircle,
|
IconInfoCircle,
|
||||||
|
|
272
web/ui/mantine-ui/src/pages/query/EChart.tsx
Normal file
272
web/ui/mantine-ui/src/pages/query/EChart.tsx
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
// Copyright 2023 The Perses Authors
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
import React, { useEffect, useLayoutEffect, useRef } from "react";
|
||||||
|
import {
|
||||||
|
ECharts,
|
||||||
|
EChartsCoreOption,
|
||||||
|
EChartsOption,
|
||||||
|
init,
|
||||||
|
connect,
|
||||||
|
BarSeriesOption,
|
||||||
|
LineSeriesOption,
|
||||||
|
GaugeSeriesOption,
|
||||||
|
} from "echarts";
|
||||||
|
import isEqual from "lodash/isEqual";
|
||||||
|
import debounce from "lodash/debounce";
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
|
||||||
|
// https://github.com/apache/echarts/issues/12489#issuecomment-643185207
|
||||||
|
export interface EChartsTheme extends EChartsOption {
|
||||||
|
bar?: BarSeriesOption;
|
||||||
|
line?: LineSeriesOption;
|
||||||
|
gauge?: GaugeSeriesOption;
|
||||||
|
}
|
||||||
|
|
||||||
|
// see docs for info about each property: https://echarts.apache.org/en/api.html#events
|
||||||
|
export interface MouseEventsParameters<T> {
|
||||||
|
componentType: string;
|
||||||
|
seriesType: string;
|
||||||
|
seriesIndex: number;
|
||||||
|
seriesName: string;
|
||||||
|
name: string;
|
||||||
|
dataIndex: number;
|
||||||
|
data: Record<string, unknown> & T;
|
||||||
|
dataType: string;
|
||||||
|
value: number | number[];
|
||||||
|
color: string;
|
||||||
|
info: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnEventFunction<T> = (
|
||||||
|
params: MouseEventsParameters<T>,
|
||||||
|
// This is potentially undefined for testing purposes
|
||||||
|
instance?: ECharts
|
||||||
|
) => void;
|
||||||
|
|
||||||
|
const mouseEvents = [
|
||||||
|
"click",
|
||||||
|
"dblclick",
|
||||||
|
"mousedown",
|
||||||
|
"mousemove",
|
||||||
|
"mouseup",
|
||||||
|
"mouseover",
|
||||||
|
"mouseout",
|
||||||
|
"globalout",
|
||||||
|
"contextmenu",
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export type MouseEventName = (typeof mouseEvents)[number];
|
||||||
|
|
||||||
|
// batch event types
|
||||||
|
export interface DataZoomPayloadBatchItem {
|
||||||
|
dataZoomId: string;
|
||||||
|
// start and end not returned unless dataZoom is based on percentProp,
|
||||||
|
// which is for cases when a dataZoom component controls multiple axes
|
||||||
|
start?: number;
|
||||||
|
end?: number;
|
||||||
|
// startValue and endValue return data index for 'category' axes,
|
||||||
|
// for axis types 'value' and 'time', actual values are returned
|
||||||
|
startValue?: number;
|
||||||
|
endValue?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HighlightPayloadBatchItem {
|
||||||
|
dataIndex: number;
|
||||||
|
dataIndexInside: number;
|
||||||
|
seriesIndex: number;
|
||||||
|
// highlight action can effect multiple connected charts
|
||||||
|
escapeConnect?: boolean;
|
||||||
|
// whether blur state was triggered
|
||||||
|
notBlur?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BatchEventsParameters {
|
||||||
|
type: BatchEventName;
|
||||||
|
batch: DataZoomPayloadBatchItem[] & HighlightPayloadBatchItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
type OnBatchEventFunction = (params: BatchEventsParameters) => void;
|
||||||
|
|
||||||
|
const batchEvents = ["datazoom", "downplay", "highlight"] as const;
|
||||||
|
|
||||||
|
export type BatchEventName = (typeof batchEvents)[number];
|
||||||
|
|
||||||
|
type ChartEventName = "finished";
|
||||||
|
|
||||||
|
type EventName = MouseEventName | ChartEventName | BatchEventName;
|
||||||
|
|
||||||
|
export type OnEventsType<T> = {
|
||||||
|
[mouseEventName in MouseEventName]?: OnEventFunction<T>;
|
||||||
|
} & {
|
||||||
|
[batchEventName in BatchEventName]?: OnBatchEventFunction;
|
||||||
|
} & {
|
||||||
|
[eventName in ChartEventName]?: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface EChartsProps<T> {
|
||||||
|
option: EChartsCoreOption;
|
||||||
|
theme?: string | EChartsTheme;
|
||||||
|
renderer?: "canvas" | "svg";
|
||||||
|
onEvents?: OnEventsType<T>;
|
||||||
|
_instance?: React.MutableRefObject<ECharts | undefined>;
|
||||||
|
syncGroup?: string;
|
||||||
|
onChartInitialized?: (instance: ECharts) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const chartHeight = 500;
|
||||||
|
export const legendHeight = (numSeries: number) => numSeries * 24;
|
||||||
|
export const legendMargin = 25;
|
||||||
|
|
||||||
|
export const EChart = React.memo(function EChart<T>({
|
||||||
|
option,
|
||||||
|
theme,
|
||||||
|
renderer,
|
||||||
|
onEvents,
|
||||||
|
_instance,
|
||||||
|
syncGroup,
|
||||||
|
onChartInitialized,
|
||||||
|
}: EChartsProps<T>) {
|
||||||
|
const initialOption = useRef<EChartsCoreOption>(option);
|
||||||
|
const prevOption = useRef<EChartsCoreOption>(option);
|
||||||
|
const containerRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const chartElement = useRef<ECharts | null>(null);
|
||||||
|
|
||||||
|
// Initialize chart, dispose on unmount
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (containerRef.current === null || chartElement.current !== null) return;
|
||||||
|
chartElement.current = init(containerRef.current, theme, {
|
||||||
|
renderer: renderer ?? "canvas",
|
||||||
|
});
|
||||||
|
if (chartElement.current === undefined) return;
|
||||||
|
chartElement.current.setOption(initialOption.current, true);
|
||||||
|
onChartInitialized?.(chartElement.current);
|
||||||
|
if (_instance !== undefined) {
|
||||||
|
_instance.current = chartElement.current;
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
if (chartElement.current === null) return;
|
||||||
|
chartElement.current.dispose();
|
||||||
|
chartElement.current = null;
|
||||||
|
};
|
||||||
|
}, [_instance, onChartInitialized, theme, renderer]);
|
||||||
|
|
||||||
|
// When syncGroup is explicitly set, charts within same group share interactions such as crosshair
|
||||||
|
useEffect(() => {
|
||||||
|
if (!chartElement.current || !syncGroup) return;
|
||||||
|
chartElement.current.group = syncGroup;
|
||||||
|
connect([chartElement.current]); // more info: https://echarts.apache.org/en/api.html#echarts.connect
|
||||||
|
}, [syncGroup, chartElement]);
|
||||||
|
|
||||||
|
// Update chart data when option changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (prevOption.current === undefined || isEqual(prevOption.current, option))
|
||||||
|
return;
|
||||||
|
if (!chartElement.current) return;
|
||||||
|
chartElement.current.setOption(option, true);
|
||||||
|
prevOption.current = option;
|
||||||
|
}, [option]);
|
||||||
|
|
||||||
|
// Resize chart, cleanup listener on unmount
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const updateSize = debounce(() => {
|
||||||
|
if (!chartElement.current) return;
|
||||||
|
chartElement.current.resize();
|
||||||
|
}, 200);
|
||||||
|
window.addEventListener("resize", updateSize);
|
||||||
|
updateSize();
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener("resize", updateSize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Bind and unbind chart events passed as prop
|
||||||
|
useEffect(() => {
|
||||||
|
const chart = chartElement.current;
|
||||||
|
if (!chart || onEvents === undefined) return;
|
||||||
|
bindEvents(chart, onEvents);
|
||||||
|
return () => {
|
||||||
|
if (chart === undefined) return;
|
||||||
|
if (chart.isDisposed() === true) return;
|
||||||
|
for (const event in onEvents) {
|
||||||
|
chart.off(event);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [onEvents]);
|
||||||
|
|
||||||
|
// // TODO: re-evaluate how this is triggered. It's technically working right
|
||||||
|
// // now because the sx prop is an object that gets re-created, but that also
|
||||||
|
// // means it runs unnecessarily some of the time and theoretically might
|
||||||
|
// // not run in some other cases. Maybe it should use a resize observer?
|
||||||
|
// useEffect(() => {
|
||||||
|
// // TODO: fix this debouncing. This likely isn't working as intended because
|
||||||
|
// // the debounced function is re-created every time this useEffect is called.
|
||||||
|
// const updateSize = debounce(
|
||||||
|
// () => {
|
||||||
|
// if (!chartElement.current) return;
|
||||||
|
// chartElement.current.resize();
|
||||||
|
// },
|
||||||
|
// 200,
|
||||||
|
// {
|
||||||
|
// leading: true,
|
||||||
|
// }
|
||||||
|
// );
|
||||||
|
// updateSize();
|
||||||
|
// }, [sx]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
w="100%"
|
||||||
|
h={
|
||||||
|
chartHeight +
|
||||||
|
legendMargin +
|
||||||
|
legendHeight((option as { series: unknown[] }).series.length)
|
||||||
|
}
|
||||||
|
ref={containerRef}
|
||||||
|
></Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate event config and bind custom events
|
||||||
|
function bindEvents<T>(instance: ECharts, events?: OnEventsType<T>) {
|
||||||
|
if (events === undefined) return;
|
||||||
|
|
||||||
|
function bindEvent(eventName: EventName, OnEventFunction: unknown) {
|
||||||
|
if (typeof OnEventFunction === "function") {
|
||||||
|
if (isMouseEvent(eventName)) {
|
||||||
|
instance.on(eventName, (params) => OnEventFunction(params, instance));
|
||||||
|
} else if (isBatchEvent(eventName)) {
|
||||||
|
instance.on(eventName, (params) => OnEventFunction(params));
|
||||||
|
} else {
|
||||||
|
instance.on(eventName, () => OnEventFunction(null, instance));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const eventName in events) {
|
||||||
|
if (Object.prototype.hasOwnProperty.call(events, eventName)) {
|
||||||
|
const customEvent = events[eventName as EventName] ?? null;
|
||||||
|
if (customEvent) {
|
||||||
|
bindEvent(eventName as EventName, customEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isMouseEvent(eventName: EventName): eventName is MouseEventName {
|
||||||
|
return (mouseEvents as readonly string[]).includes(eventName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isBatchEvent(eventName: EventName): eventName is BatchEventName {
|
||||||
|
return (batchEvents as readonly string[]).includes(eventName);
|
||||||
|
}
|
|
@ -1,27 +1,13 @@
|
||||||
import { FC, useEffect, useId } from "react";
|
import { FC, useEffect, useId } from "react";
|
||||||
import { Table, 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,
|
|
||||||
InstantSample,
|
|
||||||
RangeSamples,
|
|
||||||
} from "../../api/responseTypes/query";
|
|
||||||
import SeriesName from "./SeriesName";
|
|
||||||
import { useAPIQuery } from "../../api/api";
|
import { useAPIQuery } from "../../api/api";
|
||||||
import classes from "./DataTable.module.css";
|
import classes from "./DataTable.module.css";
|
||||||
import { GraphDisplayMode } from "../../state/queryPageSlice";
|
import { GraphDisplayMode } from "../../state/queryPageSlice";
|
||||||
|
import { EChart, chartHeight, legendMargin } from "./EChart";
|
||||||
const maxFormattableSeries = 1000;
|
import { formatSeries } from "../../lib/formatSeries";
|
||||||
const maxDisplayableSeries = 10000;
|
import { EChartsOption } from "echarts";
|
||||||
|
|
||||||
const limitSeries = <S extends InstantSample | RangeSamples>(
|
|
||||||
series: S[]
|
|
||||||
): S[] => {
|
|
||||||
if (series.length > maxDisplayableSeries) {
|
|
||||||
return series.slice(0, maxDisplayableSeries);
|
|
||||||
}
|
|
||||||
return series;
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface GraphProps {
|
export interface GraphProps {
|
||||||
expr: string;
|
expr: string;
|
||||||
|
@ -38,6 +24,7 @@ const Graph: FC<GraphProps> = ({
|
||||||
endTime,
|
endTime,
|
||||||
range,
|
range,
|
||||||
resolution,
|
resolution,
|
||||||
|
displayMode,
|
||||||
retriggerIdx,
|
retriggerIdx,
|
||||||
}) => {
|
}) => {
|
||||||
const realEndTime = (endTime !== null ? endTime : Date.now()) / 1000;
|
const realEndTime = (endTime !== null ? endTime : Date.now()) / 1000;
|
||||||
|
@ -91,6 +78,18 @@ const Graph: FC<GraphProps> = ({
|
||||||
|
|
||||||
const { result, resultType } = data.data;
|
const { result, resultType } = data.data;
|
||||||
|
|
||||||
|
if (resultType !== "matrix") {
|
||||||
|
return (
|
||||||
|
<Alert
|
||||||
|
title="Invalid query result"
|
||||||
|
icon={<IconAlertTriangle size={14} />}
|
||||||
|
>
|
||||||
|
This query returned a result of type "{resultType}", but a matrix was
|
||||||
|
expected.
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (result.length === 0) {
|
if (result.length === 0) {
|
||||||
return (
|
return (
|
||||||
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}>
|
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}>
|
||||||
|
@ -99,7 +98,68 @@ const Graph: FC<GraphProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const doFormat = result.length <= maxFormattableSeries;
|
const option: EChartsOption = {
|
||||||
|
animation: false,
|
||||||
|
grid: {
|
||||||
|
left: 20,
|
||||||
|
top: 20,
|
||||||
|
right: 20,
|
||||||
|
bottom: 20 + result.length * 24,
|
||||||
|
containLabel: true,
|
||||||
|
},
|
||||||
|
legend: {
|
||||||
|
type: "scroll",
|
||||||
|
icon: "square",
|
||||||
|
orient: "vertical",
|
||||||
|
top: chartHeight + legendMargin,
|
||||||
|
bottom: 20,
|
||||||
|
left: 30,
|
||||||
|
right: 20,
|
||||||
|
},
|
||||||
|
xAxis: {
|
||||||
|
type: "category",
|
||||||
|
// min: realEndTime * 1000 - range,
|
||||||
|
// max: realEndTime * 1000,
|
||||||
|
data: result[0].values?.map((v) => Math.round(v[0] * 1000)),
|
||||||
|
axisLine: {
|
||||||
|
show: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: "value",
|
||||||
|
axisLabel: {
|
||||||
|
formatter: formatValue,
|
||||||
|
},
|
||||||
|
axisLine: {
|
||||||
|
// symbol: "arrow",
|
||||||
|
show: true,
|
||||||
|
// lineStyle: {
|
||||||
|
// type: "dashed",
|
||||||
|
// color: "rgba(0, 0, 0, 0.5)",
|
||||||
|
// },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
show: true,
|
||||||
|
trigger: "item",
|
||||||
|
transitionDuration: 0,
|
||||||
|
axisPointer: {
|
||||||
|
type: "cross",
|
||||||
|
// snap: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
series: result.map((series) => ({
|
||||||
|
name: formatSeries(series.metric),
|
||||||
|
// data: series.values?.map((v) => [v[0] * 1000, parseFloat(v[1])]),
|
||||||
|
data: series.values?.map((v) => parseFloat(v[1])),
|
||||||
|
type: "line",
|
||||||
|
stack: displayMode === "stacked" ? "total" : undefined,
|
||||||
|
// showSymbol: false,
|
||||||
|
// fill: displayMode === "stacked" ? "tozeroy" : undefined,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(option);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pos="relative" className={classes.tableWrapper}>
|
<Box pos="relative" className={classes.tableWrapper}>
|
||||||
|
@ -112,59 +172,63 @@ const Graph: FC<GraphProps> = ({
|
||||||
}}
|
}}
|
||||||
styles={{ loader: { width: "100%", height: "100%" } }}
|
styles={{ loader: { width: "100%", height: "100%" } }}
|
||||||
/>
|
/>
|
||||||
<Table highlightOnHover fz="xs">
|
<EChart
|
||||||
<Table.Tbody>
|
option={option}
|
||||||
{resultType === "vector" ? (
|
// theme={chartsTheme.echartsTheme}
|
||||||
limitSeries<InstantSample>(result).map((s, idx) => (
|
// onEvents={handleEvents}
|
||||||
<Table.Tr key={idx}>
|
// _instance={chartRef}
|
||||||
<Table.Td>
|
// syncGroup={syncGroup}
|
||||||
<SeriesName labels={s.metric} format={doFormat} />
|
/>
|
||||||
</Table.Td>
|
|
||||||
<Table.Td className={classes.numberCell}>
|
|
||||||
{s.value && s.value[1]}
|
|
||||||
{s.histogram && "TODO HISTOGRAM DISPLAY"}
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))
|
|
||||||
) : resultType === "matrix" ? (
|
|
||||||
limitSeries<RangeSamples>(result).map((s, idx) => (
|
|
||||||
<Table.Tr key={idx}>
|
|
||||||
<Table.Td>
|
|
||||||
<SeriesName labels={s.metric} format={doFormat} />
|
|
||||||
</Table.Td>
|
|
||||||
<Table.Td className={classes.numberCell}>
|
|
||||||
{s.values &&
|
|
||||||
s.values.map((v, idx) => (
|
|
||||||
<div key={idx}>
|
|
||||||
{v[1]} @ {v[0]}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
))
|
|
||||||
) : resultType === "scalar" ? (
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Td>Scalar value</Table.Td>
|
|
||||||
<Table.Td className={classes.numberCell}>{result[1]}</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
) : resultType === "string" ? (
|
|
||||||
<Table.Tr>
|
|
||||||
<Table.Td>String value</Table.Td>
|
|
||||||
<Table.Td>{result[1]}</Table.Td>
|
|
||||||
</Table.Tr>
|
|
||||||
) : (
|
|
||||||
<Alert
|
|
||||||
color="red"
|
|
||||||
title="Invalid query response"
|
|
||||||
icon={<IconAlertTriangle size={14} />}
|
|
||||||
>
|
|
||||||
Invalid result value type
|
|
||||||
</Alert>
|
|
||||||
)}
|
|
||||||
</Table.Tbody>
|
|
||||||
</Table>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const formatValue = (y: number | null): string => {
|
||||||
|
if (y === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
const absY = Math.abs(y);
|
||||||
|
|
||||||
|
if (absY >= 1e24) {
|
||||||
|
return (y / 1e24).toFixed(2) + "Y";
|
||||||
|
} else if (absY >= 1e21) {
|
||||||
|
return (y / 1e21).toFixed(2) + "Z";
|
||||||
|
} else if (absY >= 1e18) {
|
||||||
|
return (y / 1e18).toFixed(2) + "E";
|
||||||
|
} else if (absY >= 1e15) {
|
||||||
|
return (y / 1e15).toFixed(2) + "P";
|
||||||
|
} else if (absY >= 1e12) {
|
||||||
|
return (y / 1e12).toFixed(2) + "T";
|
||||||
|
} else if (absY >= 1e9) {
|
||||||
|
return (y / 1e9).toFixed(2) + "G";
|
||||||
|
} else if (absY >= 1e6) {
|
||||||
|
return (y / 1e6).toFixed(2) + "M";
|
||||||
|
} else if (absY >= 1e3) {
|
||||||
|
return (y / 1e3).toFixed(2) + "k";
|
||||||
|
} else if (absY >= 1) {
|
||||||
|
return y.toFixed(2);
|
||||||
|
} else if (absY === 0) {
|
||||||
|
return y.toFixed(2);
|
||||||
|
} else if (absY < 1e-23) {
|
||||||
|
return (y / 1e-24).toFixed(2) + "y";
|
||||||
|
} else if (absY < 1e-20) {
|
||||||
|
return (y / 1e-21).toFixed(2) + "z";
|
||||||
|
} else if (absY < 1e-17) {
|
||||||
|
return (y / 1e-18).toFixed(2) + "a";
|
||||||
|
} else if (absY < 1e-14) {
|
||||||
|
return (y / 1e-15).toFixed(2) + "f";
|
||||||
|
} else if (absY < 1e-11) {
|
||||||
|
return (y / 1e-12).toFixed(2) + "p";
|
||||||
|
} else if (absY < 1e-8) {
|
||||||
|
return (y / 1e-9).toFixed(2) + "n";
|
||||||
|
} else if (absY < 1e-5) {
|
||||||
|
return (y / 1e-6).toFixed(2) + "µ";
|
||||||
|
} else if (absY < 1e-2) {
|
||||||
|
return (y / 1e-3).toFixed(2) + "m";
|
||||||
|
} else if (absY <= 1) {
|
||||||
|
return y.toFixed(2);
|
||||||
|
}
|
||||||
|
throw Error("couldn't format a value, this is a bug");
|
||||||
|
};
|
||||||
|
|
||||||
export default Graph;
|
export default Graph;
|
||||||
|
|
46
web/ui/package-lock.json
generated
46
web/ui/package-lock.json
generated
|
@ -124,8 +124,11 @@
|
||||||
"@reduxjs/toolkit": "^2.2.1",
|
"@reduxjs/toolkit": "^2.2.1",
|
||||||
"@tabler/icons-react": "^2.47.0",
|
"@tabler/icons-react": "^2.47.0",
|
||||||
"@tanstack/react-query": "^5.22.2",
|
"@tanstack/react-query": "^5.22.2",
|
||||||
|
"@types/lodash": "^4.17.7",
|
||||||
"@uiw/react-codemirror": "^4.21.22",
|
"@uiw/react-codemirror": "^4.21.22",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
|
"echarts": "^5.5.1",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
|
@ -2706,6 +2709,12 @@
|
||||||
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/lodash": {
|
||||||
|
"version": "4.17.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz",
|
||||||
|
"integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/@types/node": {
|
"node_modules/@types/node": {
|
||||||
"version": "20.11.19",
|
"version": "20.11.19",
|
||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.19.tgz",
|
||||||
|
@ -3846,6 +3855,22 @@
|
||||||
"csstype": "^3.0.2"
|
"csstype": "^3.0.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/echarts": {
|
||||||
|
"version": "5.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.5.1.tgz",
|
||||||
|
"integrity": "sha512-Fce8upazaAXUVUVsjgV6mBnGuqgO+JNDlcgF79Dksy4+wgGpQB2lmYoO4TSweFg/mZITdpGHomw/cNBJZj1icA==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0",
|
||||||
|
"zrender": "5.6.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/echarts/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"node_modules/electron-to-chromium": {
|
"node_modules/electron-to-chromium": {
|
||||||
"version": "1.4.678",
|
"version": "1.4.678",
|
||||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.678.tgz",
|
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.678.tgz",
|
||||||
|
@ -5619,6 +5644,12 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/lodash": {
|
||||||
|
"version": "4.17.21",
|
||||||
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/lodash.memoize": {
|
"node_modules/lodash.memoize": {
|
||||||
"version": "4.1.2",
|
"version": "4.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
|
||||||
|
@ -7662,6 +7693,21 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/zrender": {
|
||||||
|
"version": "5.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.6.0.tgz",
|
||||||
|
"integrity": "sha512-uzgraf4njmmHAbEUxMJ8Oxg+P3fT04O+9p7gY+wJRVxo8Ge+KmYv0WJev945EH4wFuc4OY2NLXz46FZrWS9xJg==",
|
||||||
|
"license": "BSD-3-Clause",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "2.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zrender/node_modules/tslib": {
|
||||||
|
"version": "2.3.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||||
|
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||||
|
"license": "0BSD"
|
||||||
|
},
|
||||||
"react-app": {
|
"react-app": {
|
||||||
"name": "@prometheus-io/react-app",
|
"name": "@prometheus-io/react-app",
|
||||||
"version": "0.51.2",
|
"version": "0.51.2",
|
||||||
|
|
Loading…
Reference in a new issue