mirror of https://github.com/prometheus/prometheus
Try out uPlot for new UI
Signed-off-by: Julius Volz <julius.volz@gmail.com>mantine-ui-uplot
parent
9252826659
commit
d9520b1a79
|
@ -30,13 +30,14 @@
|
||||||
"@types/lodash": "^4.17.7",
|
"@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",
|
"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",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
"react-router-dom": "^6.22.1"
|
"react-router-dom": "^6.22.1",
|
||||||
|
"uplot": "^1.6.30",
|
||||||
|
"uplot-react": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
|
|
|
@ -1,272 +0,0 @@
|
||||||
// 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);
|
|
||||||
}
|
|
|
@ -0,0 +1,11 @@
|
||||||
|
.chartWrapper {
|
||||||
|
border: 1px solid
|
||||||
|
light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-5));
|
||||||
|
border-radius: var(--mantine-radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.uplotChart {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding: 15px;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,26 @@
|
||||||
|
.uplot {
|
||||||
|
.u-legend {
|
||||||
|
text-align: left;
|
||||||
|
margin-left: 25px;
|
||||||
|
|
||||||
|
.u-marker {
|
||||||
|
margin-right: 8px;
|
||||||
|
height: 0.8em;
|
||||||
|
width: 0.8em;
|
||||||
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: var(--mantine-font-size-xs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.u-inline tr {
|
||||||
|
display: block;
|
||||||
|
/* display: table;
|
||||||
|
|
||||||
|
* {
|
||||||
|
display: table-cell;
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
}
|
|
@ -127,13 +127,14 @@
|
||||||
"@types/lodash": "^4.17.7",
|
"@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",
|
"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",
|
||||||
"react-redux": "^9.1.0",
|
"react-redux": "^9.1.0",
|
||||||
"react-router-dom": "^6.22.1"
|
"react-router-dom": "^6.22.1",
|
||||||
|
"uplot": "^1.6.30",
|
||||||
|
"uplot-react": "^1.2.2"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "^18.2.55",
|
"@types/react": "^18.2.55",
|
||||||
|
@ -3855,22 +3856,6 @@
|
||||||
"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",
|
||||||
|
@ -7365,6 +7350,25 @@
|
||||||
"browserslist": ">= 4.21.0"
|
"browserslist": ">= 4.21.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/uplot": {
|
||||||
|
"version": "1.6.30",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot/-/uplot-1.6.30.tgz",
|
||||||
|
"integrity": "sha512-48oVVRALM/128ttW19F2a2xobc2WfGdJ0VJFX00099CfqbCTuML7L2OrTKxNzeFP34eo1+yJbqFSoFAp2u28/Q==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
|
"node_modules/uplot-react": {
|
||||||
|
"version": "1.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/uplot-react/-/uplot-react-1.2.2.tgz",
|
||||||
|
"integrity": "sha512-fCe48HsE0sJmHVUs4TC49roTK3FYNXfCxA44g8pe20TMZ8GD3OT/mtXN/S0gJ8bYVOUcheOZ5u7f1Vw09JbTrw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=8.10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.6",
|
||||||
|
"uplot": "^1.6.20"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/uri-js": {
|
"node_modules/uri-js": {
|
||||||
"version": "4.4.1",
|
"version": "4.4.1",
|
||||||
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
|
||||||
|
@ -7693,21 +7697,6 @@
|
||||||
"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 New Issue