prometheus/web/ui/mantine-ui/src/pages/query/Graph.tsx

1230 lines
19 KiB
TypeScript
Raw Normal View History

import { FC, useEffect, useId } from "react";
import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
import { InstantQueryResult } from "../../api/responseTypes/query";
import { useAPIQuery } from "../../api/api";
import classes from "./Graph.module.css";
import { GraphDisplayMode } from "../../state/queryPageSlice";
import { formatSeries } from "../../lib/formatSeries";
import uPlot from "uplot";
import UplotReact from "uplot-react";
import "uplot/dist/uPlot.min.css";
import "./uplot.css";
import { useElementSize } from "@mantine/hooks";
export interface GraphProps {
expr: string;
endTime: number | null;
range: number;
resolution: number | null;
showExemplars: boolean;
displayMode: GraphDisplayMode;
retriggerIdx: number;
}
export const colorPool = [
"#008000",
"#008080",
"#800000",
"#800080",
"#808000",
"#808080",
"#0000c0",
"#008040",
"#0080c0",
"#800040",
"#8000c0",
"#808040",
"#8080c0",
"#00c000",
"#00c080",
"#804000",
"#804080",
"#80c000",
"#80c080",
"#0040c0",
"#00c040",
"#00c0c0",
"#804040",
"#8040c0",
"#80c040",
"#80c0c0",
"#408000",
"#408080",
"#c00000",
"#c00080",
"#c08000",
"#c08080",
"#4000c0",
"#408040",
"#4080c0",
"#c00040",
"#c000c0",
"#c08040",
"#c080c0",
"#404000",
"#404080",
"#40c000",
"#40c080",
"#c04000",
"#c04080",
"#c0c000",
"#c0c080",
"#404040",
"#4040c0",
"#40c040",
"#40c0c0",
"#c04040",
"#c040c0",
"#c0c040",
"#0000a0",
"#008020",
"#0080a0",
"#800020",
"#8000a0",
"#808020",
"#8080a0",
"#0000e0",
"#008060",
"#0080e0",
"#800060",
"#8000e0",
"#808060",
"#8080e0",
"#0040a0",
"#00c020",
"#00c0a0",
"#804020",
"#8040a0",
"#80c020",
"#80c0a0",
"#0040e0",
"#00c060",
"#00c0e0",
"#804060",
"#8040e0",
"#80c060",
"#80c0e0",
"#4000a0",
"#408020",
"#4080a0",
"#c00020",
"#c000a0",
"#c08020",
"#c080a0",
"#4000e0",
"#408060",
"#4080e0",
"#c00060",
"#c000e0",
"#c08060",
"#c080e0",
"#404020",
"#4040a0",
"#40c020",
"#40c0a0",
"#c04020",
"#c040a0",
"#c0c020",
"#c0c0a0",
"#404060",
"#4040e0",
"#40c060",
"#40c0e0",
"#c04060",
"#c040e0",
"#c0c060",
"#00a000",
"#00a080",
"#802000",
"#802080",
"#80a000",
"#80a080",
"#0020c0",
"#00a040",
"#00a0c0",
"#802040",
"#8020c0",
"#80a040",
"#80a0c0",
"#006000",
"#006080",
"#00e000",
"#00e080",
"#806000",
"#806080",
"#80e000",
"#80e080",
"#006040",
"#0060c0",
"#00e040",
"#00e0c0",
"#806040",
"#8060c0",
"#80e040",
"#80e0c0",
"#40a000",
"#40a080",
"#c02000",
"#c02080",
"#c0a000",
"#c0a080",
"#4020c0",
"#40a040",
"#40a0c0",
"#c02040",
"#c020c0",
"#c0a040",
"#c0a0c0",
"#406000",
"#406080",
"#40e000",
"#40e080",
"#c06000",
"#c06080",
"#c0e000",
"#c0e080",
"#406040",
"#4060c0",
"#40e040",
"#40e0c0",
"#c06040",
"#c060c0",
"#c0e040",
"#c0e0c0",
"#0020a0",
"#00a020",
"#00a0a0",
"#802020",
"#8020a0",
"#80a020",
"#80a0a0",
"#0020e0",
"#00a060",
"#00a0e0",
"#802060",
"#8020e0",
"#80a060",
"#80a0e0",
"#006020",
"#0060a0",
"#00e020",
"#00e0a0",
"#806020",
"#8060a0",
"#80e020",
"#80e0a0",
"#006060",
"#0060e0",
"#00e060",
"#00e0e0",
"#806060",
"#8060e0",
"#80e060",
"#80e0e0",
"#4020a0",
"#40a020",
"#40a0a0",
"#c02020",
"#c020a0",
"#c0a020",
"#c0a0a0",
"#4020e0",
"#40a060",
"#40a0e0",
"#c02060",
"#c020e0",
"#c0a060",
"#c0a0e0",
"#406020",
"#4060a0",
"#40e020",
"#40e0a0",
"#c06020",
"#c060a0",
"#c0e020",
"#c0e0a0",
"#406060",
"#4060e0",
"#40e060",
"#40e0e0",
"#c06060",
"#c060e0",
"#c0e060",
"#208000",
"#208080",
"#a00000",
"#a00080",
"#a08000",
"#a08080",
"#208040",
"#2080c0",
"#a00040",
"#a000c0",
"#a08040",
"#a080c0",
"#204080",
"#20c000",
"#20c080",
"#a04000",
"#a04080",
"#a0c000",
"#a0c080",
"#2040c0",
"#20c040",
"#20c0c0",
"#a04040",
"#a040c0",
"#a0c040",
"#a0c0c0",
"#608000",
"#608080",
"#e00000",
"#e00080",
"#e08000",
"#e08080",
"#6000c0",
"#608040",
"#6080c0",
"#e00040",
"#e000c0",
"#e08040",
"#e080c0",
"#604080",
"#60c000",
"#60c080",
"#e04000",
"#e04080",
"#e0c000",
"#e0c080",
"#604040",
"#6040c0",
"#60c040",
"#60c0c0",
"#e04040",
"#e040c0",
"#e0c040",
"#e0c0c0",
"#208020",
"#2080a0",
"#a00020",
"#a000a0",
"#a08020",
"#a080a0",
"#2000e0",
"#208060",
"#2080e0",
"#a00060",
"#a000e0",
"#a08060",
"#a080e0",
"#2040a0",
"#20c020",
"#20c0a0",
"#a04020",
"#a040a0",
"#a0c020",
"#2040e0",
"#20c060",
"#20c0e0",
"#a04060",
"#a040e0",
"#a0c060",
"#a0c0e0",
"#6000a0",
"#608020",
"#6080a0",
"#e00020",
"#e000a0",
"#e08020",
"#e080a0",
"#6000e0",
"#608060",
"#6080e0",
"#e00060",
"#e000e0",
"#e08060",
"#e080e0",
"#604020",
"#6040a0",
"#60c020",
"#60c0a0",
"#e04020",
"#e040a0",
"#e0c020",
"#e0c0a0",
"#604060",
"#6040e0",
"#60c060",
"#60c0e0",
"#e04060",
"#e040e0",
"#e0c060",
"#e0c0e0",
"#20a000",
"#20a080",
"#a02000",
"#a02080",
"#a0a000",
"#a0a080",
"#2020c0",
"#20a040",
"#20a0c0",
"#a02040",
"#a020c0",
"#a0a040",
"#a0a0c0",
"#206000",
"#206080",
"#20e000",
"#20e080",
"#a06000",
"#a06080",
"#a0e000",
"#a0e080",
"#206040",
"#2060c0",
"#20e040",
"#20e0c0",
"#a06040",
"#a060c0",
"#a0e040",
"#a0e0c0",
"#602080",
"#60a000",
"#60a080",
"#e02000",
"#e02080",
"#e0a000",
"#e0a080",
"#6020c0",
"#60a040",
"#60a0c0",
"#e02040",
"#e020c0",
"#e0a040",
"#e0a0c0",
"#606000",
"#606080",
"#60e000",
"#60e080",
"#e06000",
"#e06080",
"#e0e000",
"#e0e080",
"#606040",
"#6060c0",
"#60e040",
"#60e0c0",
"#e06040",
"#e060c0",
"#e0e040",
"#e0e0c0",
"#20a020",
"#20a0a0",
"#a02020",
"#a020a0",
"#a0a020",
"#a0a0a0",
"#2020e0",
"#20a060",
"#20a0e0",
"#a02060",
"#a020e0",
"#a0a060",
"#a0a0e0",
"#206020",
"#2060a0",
"#20e020",
"#20e0a0",
"#a06020",
"#a060a0",
"#a0e020",
"#a0e0a0",
"#206060",
"#2060e0",
"#20e060",
"#20e0e0",
"#a06060",
"#a060e0",
"#a0e060",
"#a0e0e0",
"#6020a0",
"#60a020",
"#60a0a0",
"#e02020",
"#e020a0",
"#e0a020",
"#e0a0a0",
"#602060",
"#6020e0",
"#60a060",
"#60a0e0",
"#e02060",
"#e020e0",
"#e0a060",
"#e0a0e0",
"#606020",
"#6060a0",
"#60e020",
"#60e0a0",
"#e06020",
"#e060a0",
"#e0e020",
"#e0e0a0",
"#606060",
"#6060e0",
"#60e060",
"#60e0e0",
"#e06060",
"#e060e0",
"#e0e060",
"#008010",
"#008090",
"#800010",
"#800090",
"#808010",
"#808090",
"#0000d0",
"#008050",
"#0080d0",
"#800050",
"#8000d0",
"#808050",
"#8080d0",
"#004010",
"#004090",
"#00c010",
"#00c090",
"#804010",
"#804090",
"#80c010",
"#80c090",
"#004050",
"#0040d0",
"#00c050",
"#00c0d0",
"#804050",
"#8040d0",
"#80c050",
"#80c0d0",
"#400090",
"#408010",
"#408090",
"#c00010",
"#c00090",
"#c08010",
"#c08090",
"#4000d0",
"#408050",
"#4080d0",
"#c00050",
"#c000d0",
"#c08050",
"#c080d0",
"#404010",
"#404090",
"#40c010",
"#40c090",
"#c04010",
"#c04090",
"#c0c010",
"#c0c090",
"#404050",
"#4040d0",
"#40c050",
"#40c0d0",
"#c04050",
"#c040d0",
"#c0c050",
"#0000b0",
"#008030",
"#0080b0",
"#800030",
"#8000b0",
"#808030",
"#8080b0",
"#0000f0",
"#008070",
"#0080f0",
"#800070",
"#8000f0",
"#808070",
"#8080f0",
"#004030",
"#0040b0",
"#00c030",
"#00c0b0",
"#804030",
"#8040b0",
"#80c030",
"#80c0b0",
"#004070",
"#0040f0",
"#00c070",
"#00c0f0",
"#804070",
"#8040f0",
"#80c070",
"#80c0f0",
"#4000b0",
"#408030",
"#4080b0",
"#c00030",
"#c000b0",
"#c08030",
"#c080b0",
"#400070",
"#4000f0",
"#408070",
"#4080f0",
"#c00070",
"#c000f0",
"#c08070",
"#c080f0",
"#404030",
"#4040b0",
"#40c030",
"#40c0b0",
"#c04030",
"#c040b0",
"#c0c030",
"#c0c0b0",
"#404070",
"#4040f0",
"#40c070",
"#40c0f0",
"#c04070",
"#c040f0",
"#c0c070",
"#c0c0f0",
"#002090",
"#00a010",
"#00a090",
"#802010",
"#802090",
"#80a010",
"#80a090",
"#0020d0",
"#00a050",
"#00a0d0",
"#802050",
"#8020d0",
"#80a050",
"#80a0d0",
"#006010",
"#006090",
"#00e010",
"#00e090",
"#806010",
"#806090",
"#80e010",
"#80e090",
"#006050",
"#0060d0",
"#00e050",
"#00e0d0",
"#806050",
"#8060d0",
"#80e050",
"#80e0d0",
"#402090",
"#40a010",
"#40a090",
"#c02010",
"#c02090",
"#c0a010",
"#c0a090",
"#402050",
"#4020d0",
"#40a050",
"#40a0d0",
"#c02050",
"#c020d0",
"#c0a050",
"#c0a0d0",
"#406010",
"#406090",
"#40e010",
"#40e090",
"#c06010",
"#c06090",
"#c0e010",
"#c0e090",
"#406050",
"#4060d0",
"#40e050",
"#40e0d0",
"#c06050",
"#c060d0",
"#c0e050",
"#c0e0d0",
"#0020b0",
"#00a030",
"#00a0b0",
"#802030",
"#8020b0",
"#80a030",
"#80a0b0",
"#0020f0",
"#00a070",
"#00a0f0",
"#802070",
"#8020f0",
"#80a070",
"#80a0f0",
"#006030",
"#0060b0",
"#00e030",
"#00e0b0",
"#806030",
"#8060b0",
"#80e030",
"#80e0b0",
"#006070",
"#0060f0",
"#00e070",
"#00e0f0",
"#806070",
"#8060f0",
"#80e070",
"#80e0f0",
"#4020b0",
"#40a030",
"#40a0b0",
"#c02030",
"#c020b0",
"#c0a030",
"#c0a0b0",
"#4020f0",
"#40a070",
"#40a0f0",
"#c02070",
"#c020f0",
"#c0a070",
"#c0a0f0",
"#406030",
"#4060b0",
"#40e030",
"#40e0b0",
"#c06030",
"#c060b0",
"#c0e030",
"#c0e0b0",
"#406070",
"#4060f0",
"#40e070",
"#40e0f0",
"#c06070",
"#c060f0",
"#c0e070",
"#208010",
"#208090",
"#a00010",
"#a00090",
"#a08010",
"#a08090",
"#2000d0",
"#208050",
"#2080d0",
"#a00050",
"#a000d0",
"#a08050",
"#a080d0",
"#204010",
"#204090",
"#20c010",
"#20c090",
"#a04010",
"#a04090",
"#a0c010",
"#a0c090",
"#204050",
"#2040d0",
"#20c050",
"#20c0d0",
"#a04050",
"#a040d0",
"#a0c050",
"#a0c0d0",
"#600090",
"#608010",
"#608090",
"#e00010",
"#e00090",
"#e08010",
"#e08090",
"#600050",
"#6000d0",
"#608050",
"#6080d0",
"#e00050",
"#e000d0",
"#e08050",
"#e080d0",
"#604010",
"#604090",
"#60c010",
"#60c090",
"#e04010",
"#e04090",
"#e0c010",
"#e0c090",
"#604050",
"#6040d0",
"#60c050",
"#60c0d0",
"#e04050",
"#e040d0",
"#e0c050",
"#e0c0d0",
"#2000b0",
"#208030",
"#2080b0",
"#a00030",
"#a000b0",
"#a08030",
"#a080b0",
"#2000f0",
"#208070",
"#2080f0",
"#a00070",
"#a000f0",
"#a08070",
"#a080f0",
"#204030",
"#2040b0",
"#20c030",
"#20c0b0",
"#a04030",
"#a040b0",
"#a0c030",
"#a0c0b0",
"#204070",
"#2040f0",
"#20c070",
"#20c0f0",
"#a04070",
"#a040f0",
"#a0c070",
"#a0c0f0",
"#6000b0",
"#608030",
"#6080b0",
"#e00030",
"#e000b0",
"#e08030",
"#e080b0",
"#600070",
"#6000f0",
"#608070",
"#e00070",
"#e000f0",
"#e08070",
"#e080f0",
"#604030",
"#6040b0",
"#60c030",
"#60c0b0",
"#e04030",
"#e040b0",
"#e0c030",
"#e0c0b0",
"#604070",
"#6040f0",
"#60c070",
"#60c0f0",
"#e04070",
"#e040f0",
"#e0c070",
"#e0c0f0",
"#20a010",
"#20a090",
"#a02010",
"#a02090",
"#a0a010",
"#a0a090",
"#2020d0",
"#20a050",
"#20a0d0",
"#a02050",
"#a020d0",
"#a0a050",
"#a0a0d0",
"#206010",
"#206090",
"#20e010",
"#20e090",
"#a06010",
"#a06090",
"#a0e010",
"#a0e090",
"#206050",
"#2060d0",
"#20e050",
"#20e0d0",
"#a06050",
"#a060d0",
"#a0e050",
"#a0e0d0",
"#602090",
"#60a010",
"#60a090",
"#e02010",
"#e02090",
"#e0a010",
"#e0a090",
"#602050",
"#6020d0",
"#60a050",
"#60a0d0",
"#e02050",
"#e020d0",
"#e0a050",
"#e0a0d0",
"#606010",
"#606090",
"#60e010",
"#60e090",
"#e06010",
"#e06090",
"#e0e010",
"#e0e090",
"#606050",
"#6060d0",
"#60e050",
"#60e0d0",
"#e06050",
"#e060d0",
"#e0e050",
"#2020b0",
"#20a030",
"#20a0b0",
"#a02030",
"#a020b0",
"#a0a030",
"#a0a0b0",
"#2020f0",
"#20a070",
"#20a0f0",
"#a02070",
"#a020f0",
"#a0a070",
"#a0a0f0",
"#206030",
"#2060b0",
"#20e030",
"#20e0b0",
"#a06030",
"#a060b0",
"#a0e030",
"#a0e0b0",
"#206070",
"#2060f0",
"#20e070",
"#20e0f0",
"#a06070",
"#a060f0",
"#a0e070",
"#a0e0f0",
"#6020b0",
"#60a030",
"#60a0b0",
"#e02030",
"#e020b0",
"#e0a030",
"#e0a0b0",
"#6020f0",
"#60a070",
"#60a0f0",
"#e02070",
"#e020f0",
"#e0a070",
"#e0a0f0",
"#606030",
"#6060b0",
"#60e030",
"#60e0b0",
"#e06030",
"#e060b0",
"#e0e030",
"#e0e0b0",
"#606070",
"#6060f0",
"#60e070",
"#60e0f0",
"#e06070",
"#e060f0",
"#e0e070",
];
const Graph: FC<GraphProps> = ({
expr,
endTime,
range,
resolution,
displayMode,
retriggerIdx,
}) => {
const { ref, width, height } = useElementSize();
const realEndTime = (endTime !== null ? endTime : Date.now()) / 1000;
const { data, error, isFetching, isLoading, refetch } =
useAPIQuery<InstantQueryResult>({
key: useId(),
path: "/query_range",
params: {
query: expr,
step: (
resolution || Math.max(Math.floor(range / 250000), 1)
).toString(),
start: (realEndTime - range / 1000).toString(),
end: realEndTime.toString(),
},
enabled: expr !== "",
});
useEffect(() => {
expr !== "" && refetch();
}, [retriggerIdx, refetch, expr, endTime, range, resolution]);
// TODO: Share all the loading/error/empty data notices with the DataTable.
// Show a skeleton only on the first load, not on subsequent ones.
if (isLoading) {
return (
<Box>
{Array.from(Array(5), (_, i) => (
<Skeleton key={i} height={30} mb={15} />
))}
</Box>
);
}
if (error) {
return (
<Alert
color="red"
title="Error executing query"
icon={<IconAlertTriangle size={14} />}
>
{error.message}
</Alert>
);
}
if (data === undefined) {
return <Alert variant="transparent">No data queried yet</Alert>;
}
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) {
return (
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}>
This query returned no data.
</Alert>
);
}
// 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,
// })),
// };
function autoPadRight(self, side, sidesWithAxes, cycleNum) {
const xAxis = self.axes[0];
const xVals = xAxis._values;
if (xVals != null) {
// bail out, force convergence
if (cycleNum > 2) return self._padding[1];
const xSplits = xAxis._splits;
const rightSplit = xSplits[xSplits.length - 1];
const rightSplitCoord = self.valToPos(rightSplit, "x");
const leftPlotEdge = self.bbox.left / devicePixelRatio;
const rightPlotEdge = leftPlotEdge + self.bbox.width / devicePixelRatio;
const rightChartEdge = rightPlotEdge + self._padding[1];
const pxPerChar = 8;
const rightVal = xVals[xVals.length - 1] + "";
const valHalfWidth = pxPerChar * (rightVal.length / 2);
const rightValEdge = leftPlotEdge + rightSplitCoord + valHalfWidth;
if (rightValEdge >= rightChartEdge) {
return rightValEdge - rightPlotEdge;
}
}
// default size
return 8;
}
const options: uPlot.Options = {
id: "chart",
width: width - 30,
height: 500,
padding: [null, autoPadRight, null, null],
// plugins: [tooltipPlugin()],
scales: {
x: {
time: false,
},
},
legend: {
show: true,
live: false,
markers: {
fill: (
self: uPlot,
seriesIdx: number
): CSSStyleDeclaration["borderColor"] => {
return colorPool[seriesIdx % colorPool.length];
},
},
},
axes: [
{
labelSize: 20,
stroke: "#333",
values(self, splits) {
return splits.map((s) => s);
},
},
{
labelGap: 8,
labelSize: 8 + 12 + 8,
stroke: "#333",
size(self, values, axisIdx, cycleNum) {
const axis = self.axes[axisIdx];
// bail out, force convergence
if (cycleNum > 1) {
return axis._size;
}
let axisSize = axis.ticks.size + axis.gap;
// find longest value
const longestVal = (values ?? []).reduce(
(acc, val) => (val.length > acc.length ? val : acc),
""
);
if (longestVal != "") {
self.ctx.font = axis.font[0];
axisSize +=
self.ctx.measureText(longestVal).width / devicePixelRatio;
}
return Math.ceil(axisSize);
},
},
],
series: [
...result.map((r, idx) => ({
label: formatSeries(r.metric),
width: 2,
stroke: colorPool[idx % colorPool.length],
})),
],
};
const seriesData: uPlot.AlignedData = [
result[0].values?.map((v) => v[0]),
...result.map((r) => r.values?.map((v) => parseFloat(v[1]))),
];
return (
<Box pos="relative" ref={ref} className={classes.chartWrapper}>
<LoadingOverlay
visible={isFetching}
zIndex={1000}
overlayProps={{ radius: "sm", blur: 1 }}
loaderProps={{
children: <Skeleton m={0} w="100%" h="100%" />,
}}
styles={{ loader: { width: "100%", height: "100%" } }}
/>
<UplotReact
options={options}
data={seriesData}
className={classes.uplotChart}
/>
</Box>
);
};
export default Graph;