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

171 lines
4.7 KiB
TypeScript
Raw Normal View History

import { FC, useEffect, useId } from "react";
import { Table, Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core";
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
import {
InstantQueryResult,
InstantSample,
RangeSamples,
} from "../../api/responseTypes/query";
import SeriesName from "./SeriesName";
import { useAPIQuery } from "../../api/api";
import classes from "./DataTable.module.css";
import { GraphDisplayMode } from "../../state/queryPageSlice";
const maxFormattableSeries = 1000;
const maxDisplayableSeries = 10000;
const limitSeries = <S extends InstantSample | RangeSamples>(
series: S[]
): S[] => {
if (series.length > maxDisplayableSeries) {
return series.slice(0, maxDisplayableSeries);
}
return series;
};
export interface GraphProps {
expr: string;
endTime: number | null;
range: number;
resolution: number | null;
showExemplars: boolean;
displayMode: GraphDisplayMode;
retriggerIdx: number;
}
const Graph: FC<GraphProps> = ({
expr,
endTime,
range,
resolution,
retriggerIdx,
}) => {
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 (result.length === 0) {
return (
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}>
This query returned no data.
</Alert>
);
}
const doFormat = result.length <= maxFormattableSeries;
return (
<Box pos="relative" className={classes.tableWrapper}>
<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%" } }}
/>
<Table highlightOnHover fz="xs">
<Table.Tbody>
{resultType === "vector" ? (
limitSeries<InstantSample>(result).map((s, idx) => (
<Table.Tr key={idx}>
<Table.Td>
<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>
);
};
export default Graph;