mirror of
https://github.com/prometheus/prometheus.git
synced 2024-11-09 23:24:05 -08:00
Completely refactor DataTable, show query infos and warnings
Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
parent
0f951774b8
commit
f30a58afaf
|
@ -7,6 +7,7 @@ export type SuccessAPIResponse<T> = {
|
|||
status: "success";
|
||||
data: T;
|
||||
warnings?: string[];
|
||||
infos?: string[];
|
||||
};
|
||||
|
||||
export type ErrorAPIResponse = {
|
||||
|
|
|
@ -1,17 +1,8 @@
|
|||
import {
|
||||
FC,
|
||||
ReactNode,
|
||||
useEffect,
|
||||
useId,
|
||||
useLayoutEffect,
|
||||
useState,
|
||||
} from "react";
|
||||
import { FC, ReactNode, useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
Alert,
|
||||
Skeleton,
|
||||
Box,
|
||||
LoadingOverlay,
|
||||
SegmentedControl,
|
||||
ScrollArea,
|
||||
Group,
|
||||
|
@ -26,7 +17,6 @@ import {
|
|||
RangeSamples,
|
||||
} from "../../api/responseTypes/query";
|
||||
import SeriesName from "./SeriesName";
|
||||
import { useAPIQuery } from "../../api/api";
|
||||
import classes from "./DataTable.module.css";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
|
@ -35,13 +25,10 @@ import HistogramChart from "./HistogramChart";
|
|||
import { Histogram } from "../../types/types";
|
||||
import { bucketRangeString } from "./HistogramHelpers";
|
||||
import { useSettings } from "../../state/settingsSlice";
|
||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||
import { setVisualizer } from "../../state/queryPageSlice";
|
||||
import TimeInput from "./TimeInput";
|
||||
dayjs.extend(timezone);
|
||||
|
||||
const maxFormattableSeries = 1000;
|
||||
const maxDisplayableSeries = 10000;
|
||||
const maxFormattableSeries = 10;
|
||||
const maxDisplayableSeries = 100;
|
||||
|
||||
const limitSeries = <S extends InstantSample | RangeSamples>(
|
||||
series: S[],
|
||||
|
@ -54,256 +41,139 @@ const limitSeries = <S extends InstantSample | RangeSamples>(
|
|||
};
|
||||
|
||||
export interface DataTableProps {
|
||||
panelIdx: number;
|
||||
retriggerIdx: number;
|
||||
data: InstantQueryResult;
|
||||
limitResults: boolean;
|
||||
setLimitResults: (limit: boolean) => void;
|
||||
}
|
||||
|
||||
const DataTable: FC<DataTableProps> = ({ panelIdx, retriggerIdx }) => {
|
||||
const DataTable: FC<DataTableProps> = ({
|
||||
data,
|
||||
limitResults,
|
||||
setLimitResults,
|
||||
}) => {
|
||||
const [scale, setScale] = useState<string>("exponential");
|
||||
const [limitResults, setLimitResults] = useState<boolean>(true);
|
||||
const [responseTime, setResponseTime] = useState<number>(0);
|
||||
|
||||
const { expr, visualizer } = useAppSelector(
|
||||
(state) => state.queryPage.panels[panelIdx]
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { endTime, range } = visualizer;
|
||||
|
||||
const { data, error, isFetching, isLoading, refetch } =
|
||||
useAPIQuery<InstantQueryResult>({
|
||||
key: useId(),
|
||||
path: "/query",
|
||||
params: {
|
||||
query: expr,
|
||||
time: `${(endTime !== null ? endTime : Date.now()) / 1000}`,
|
||||
},
|
||||
enabled: expr !== "",
|
||||
recordResponseTime: setResponseTime,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
expr !== "" && refetch();
|
||||
}, [retriggerIdx, refetch, expr, endTime]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setLimitResults(true);
|
||||
}, [data, isFetching]);
|
||||
|
||||
const { useLocalTime } = useSettings();
|
||||
|
||||
// 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 { result, resultType } = data;
|
||||
const doFormat = result.length <= maxFormattableSeries;
|
||||
|
||||
return (
|
||||
<Stack gap="lg" mt="sm">
|
||||
{isLoading ? (
|
||||
<Box>
|
||||
{Array.from(Array(5), (_, i) => (
|
||||
<Skeleton key={i} height={30} mb={15} />
|
||||
))}
|
||||
</Box>
|
||||
) : data === undefined ? (
|
||||
<Alert variant="transparent">No data queried yet</Alert>
|
||||
) : result.length === 0 ? (
|
||||
<Alert title="Empty query result" icon={<IconInfoCircle size={14} />}>
|
||||
This query returned no data.
|
||||
</Alert>
|
||||
) : (
|
||||
<>
|
||||
<Group justify="space-between">
|
||||
<TimeInput
|
||||
time={endTime}
|
||||
range={range}
|
||||
description="Evaluation time"
|
||||
onChangeTime={(time) =>
|
||||
dispatch(
|
||||
setVisualizer({
|
||||
idx: panelIdx,
|
||||
visualizer: { ...visualizer, endTime: time },
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
<Text size="xs" c="gray">
|
||||
Load time: {responseTime}ms   Result series: {result.length}
|
||||
</Text>
|
||||
</Group>
|
||||
<Stack gap="lg" mt={0}>
|
||||
{limitResults &&
|
||||
["vector", "matrix"].includes(resultType) &&
|
||||
result.length > maxDisplayableSeries && (
|
||||
<Alert
|
||||
color="orange"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
title="Showing limited results"
|
||||
>
|
||||
Fetched {data.result.length} metrics, only displaying first{" "}
|
||||
{maxDisplayableSeries} for performance reasons.
|
||||
<Anchor ml="md" fz="1em" onClick={() => setLimitResults(false)}>
|
||||
Show all results
|
||||
</Anchor>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{limitResults &&
|
||||
["vector", "matrix"].includes(resultType) &&
|
||||
result.length > maxDisplayableSeries && (
|
||||
{!doFormat && (
|
||||
<Alert
|
||||
title="Formatting turned off"
|
||||
icon={<IconInfoCircle size={14} />}
|
||||
>
|
||||
Showing more than {maxFormattableSeries} series, turning off label
|
||||
formatting to improve rendering performance.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Box pos="relative" className={classes.tableWrapper}>
|
||||
<Table fz="xs">
|
||||
<Table.Tbody>
|
||||
{resultType === "vector" ? (
|
||||
limitSeries<InstantSample>(result, limitResults).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 && (
|
||||
<Stack>
|
||||
<HistogramChart
|
||||
histogram={s.histogram[1]}
|
||||
index={idx}
|
||||
scale={scale}
|
||||
/>
|
||||
<Group justify="space-between" align="center" p={10}>
|
||||
<Group align="center" gap="1rem">
|
||||
<span>
|
||||
<strong>Count:</strong> {s.histogram[1].count}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Sum:</strong> {s.histogram[1].sum}
|
||||
</span>
|
||||
</Group>
|
||||
<Group align="center" gap="1rem">
|
||||
<span>x-axis scale:</span>
|
||||
<SegmentedControl
|
||||
size={"xs"}
|
||||
value={scale}
|
||||
onChange={setScale}
|
||||
data={["exponential", "linear"]}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
{histogramTable(s.histogram[1])}
|
||||
</Stack>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
))
|
||||
) : resultType === "matrix" ? (
|
||||
limitSeries<RangeSamples>(result, limitResults).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]}{" "}
|
||||
<Text
|
||||
span
|
||||
c="gray.7"
|
||||
size="1em"
|
||||
title={formatTimestamp(v[0], useLocalTime)}
|
||||
>
|
||||
@ {v[0]}
|
||||
</Text>
|
||||
</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} />}
|
||||
title="Showing limited results"
|
||||
>
|
||||
Fetched {data.data.result.length} metrics, only displaying first{" "}
|
||||
{maxDisplayableSeries} for performance reasons.
|
||||
<Anchor ml="md" fz="1em" onClick={() => setLimitResults(false)}>
|
||||
Show all results
|
||||
</Anchor>
|
||||
Invalid result value type
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{!doFormat && (
|
||||
<Alert
|
||||
title="Formatting turned off"
|
||||
icon={<IconInfoCircle size={14} />}
|
||||
>
|
||||
Showing more than {maxFormattableSeries} series, turning off label
|
||||
formatting for performance reasons.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<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 fz="xs">
|
||||
<Table.Tbody>
|
||||
{resultType === "vector" ? (
|
||||
limitSeries<InstantSample>(result, limitResults).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 && (
|
||||
<Stack>
|
||||
<HistogramChart
|
||||
histogram={s.histogram[1]}
|
||||
index={idx}
|
||||
scale={scale}
|
||||
/>
|
||||
<Group
|
||||
justify="space-between"
|
||||
align="center"
|
||||
p={10}
|
||||
>
|
||||
<Group align="center" gap="1rem">
|
||||
<span>
|
||||
<strong>Count:</strong>{" "}
|
||||
{s.histogram[1].count}
|
||||
</span>
|
||||
<span>
|
||||
<strong>Sum:</strong> {s.histogram[1].sum}
|
||||
</span>
|
||||
</Group>
|
||||
<Group align="center" gap="1rem">
|
||||
<span>x-axis scale:</span>
|
||||
<SegmentedControl
|
||||
size={"xs"}
|
||||
value={scale}
|
||||
onChange={setScale}
|
||||
data={["exponential", "linear"]}
|
||||
/>
|
||||
</Group>
|
||||
</Group>
|
||||
{histogramTable(s.histogram[1])}
|
||||
</Stack>
|
||||
)}
|
||||
</Table.Td>
|
||||
</Table.Tr>
|
||||
)
|
||||
)
|
||||
) : resultType === "matrix" ? (
|
||||
limitSeries<RangeSamples>(result, limitResults).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]}{" "}
|
||||
<Text
|
||||
span
|
||||
c="gray.7"
|
||||
size="1em"
|
||||
title={formatTimestamp(v[0], useLocalTime)}
|
||||
>
|
||||
@ {v[0]}
|
||||
</Text>
|
||||
</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>
|
||||
</>
|
||||
)}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</Box>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -23,12 +23,12 @@ import {
|
|||
setExpr,
|
||||
setVisualizer,
|
||||
} from "../../state/queryPageSlice";
|
||||
import DataTable from "./DataTable";
|
||||
import TimeInput from "./TimeInput";
|
||||
import RangeInput from "./RangeInput";
|
||||
import ExpressionInput from "./ExpressionInput";
|
||||
import Graph from "./Graph";
|
||||
import ResolutionInput from "./ResolutionInput";
|
||||
import TableTab from "./TableTab";
|
||||
|
||||
export interface PanelProps {
|
||||
idx: number;
|
||||
|
@ -84,7 +84,7 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
|
|||
</Tabs.Tab>
|
||||
</Tabs.List>
|
||||
<Tabs.Panel pt="sm" value="table">
|
||||
<DataTable panelIdx={idx} retriggerIdx={retriggerIdx} />
|
||||
<TableTab panelIdx={idx} retriggerIdx={retriggerIdx} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel
|
||||
pt="sm"
|
||||
|
|
131
web/ui/mantine-ui/src/pages/query/TableTab.tsx
Normal file
131
web/ui/mantine-ui/src/pages/query/TableTab.tsx
Normal file
|
@ -0,0 +1,131 @@
|
|||
import { FC, useEffect, useId, useLayoutEffect, useState } from "react";
|
||||
import { Alert, Skeleton, Box, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react";
|
||||
import { InstantQueryResult } from "../../api/responseTypes/query";
|
||||
import { useAPIQuery } from "../../api/api";
|
||||
import dayjs from "dayjs";
|
||||
import timezone from "dayjs/plugin/timezone";
|
||||
import { useAppDispatch, useAppSelector } from "../../state/hooks";
|
||||
import { setVisualizer } from "../../state/queryPageSlice";
|
||||
import TimeInput from "./TimeInput";
|
||||
import DataTable from "./DataTable";
|
||||
dayjs.extend(timezone);
|
||||
|
||||
export interface TableTabProps {
|
||||
panelIdx: number;
|
||||
retriggerIdx: number;
|
||||
}
|
||||
|
||||
const TableTab: FC<TableTabProps> = ({ panelIdx, retriggerIdx }) => {
|
||||
const [responseTime, setResponseTime] = useState<number>(0);
|
||||
const [limitResults, setLimitResults] = useState<boolean>(true);
|
||||
|
||||
const { expr, visualizer } = useAppSelector(
|
||||
(state) => state.queryPage.panels[panelIdx]
|
||||
);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const { endTime, range } = visualizer;
|
||||
|
||||
const { data, error, isFetching, refetch } = useAPIQuery<InstantQueryResult>({
|
||||
key: useId(),
|
||||
path: "/query",
|
||||
params: {
|
||||
query: expr,
|
||||
time: `${(endTime !== null ? endTime : Date.now()) / 1000}`,
|
||||
},
|
||||
enabled: expr !== "",
|
||||
recordResponseTime: setResponseTime,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
expr !== "" && refetch();
|
||||
}, [retriggerIdx, refetch, expr, endTime]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
setLimitResults(true);
|
||||
}, [data, isFetching]);
|
||||
|
||||
return (
|
||||
<Stack gap="lg" mt="sm">
|
||||
<Group justify="space-between">
|
||||
<TimeInput
|
||||
time={endTime}
|
||||
range={range}
|
||||
description="Evaluation time"
|
||||
onChangeTime={(time) =>
|
||||
dispatch(
|
||||
setVisualizer({
|
||||
idx: panelIdx,
|
||||
visualizer: { ...visualizer, endTime: time },
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
{!isFetching && data !== undefined && (
|
||||
<Text size="xs" c="gray">
|
||||
Load time: {responseTime}ms   Result series:{" "}
|
||||
{data.data.result.length}
|
||||
</Text>
|
||||
)}
|
||||
</Group>
|
||||
{isFetching ? (
|
||||
<Box>
|
||||
{Array.from(Array(5), (_, i) => (
|
||||
<Skeleton key={i} height={30} mb={15} />
|
||||
))}
|
||||
</Box>
|
||||
) : error !== null ? (
|
||||
<Alert
|
||||
color="red"
|
||||
title="Error executing query"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
{error.message}
|
||||
</Alert>
|
||||
) : data === undefined ? (
|
||||
<Alert variant="transparent">No data queried yet</Alert>
|
||||
) : (
|
||||
<>
|
||||
{data.data.result.length === 0 && (
|
||||
<Alert
|
||||
title="Empty query result"
|
||||
icon={<IconInfoCircle size={14} />}
|
||||
>
|
||||
This query returned no data.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
{data.warnings?.map((w, idx) => (
|
||||
<Alert
|
||||
key={idx}
|
||||
color="red"
|
||||
title="Query warning"
|
||||
icon={<IconAlertTriangle size={14} />}
|
||||
>
|
||||
{w}
|
||||
</Alert>
|
||||
))}
|
||||
|
||||
{data.infos?.map((w, idx) => (
|
||||
<Alert
|
||||
key={idx}
|
||||
color="yellow"
|
||||
title="Query notice"
|
||||
icon={<IconInfoCircle size={14} />}
|
||||
>
|
||||
{w}
|
||||
</Alert>
|
||||
))}
|
||||
<DataTable
|
||||
data={data.data}
|
||||
limitResults={limitResults}
|
||||
setLimitResults={setLimitResults}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
export default TableTab;
|
Loading…
Reference in a new issue