Refactor DataTable, record & show query stats

Signed-off-by: Julius Volz <julius.volz@gmail.com>
This commit is contained in:
Julius Volz 2024-07-31 16:19:38 +02:00
parent 5dca5a4718
commit a6b085ee5a
5 changed files with 196 additions and 153 deletions

View file

@ -22,10 +22,12 @@ const createQueryFn =
pathPrefix, pathPrefix,
path, path,
params, params,
recordResponseTime,
}: { }: {
pathPrefix: string; pathPrefix: string;
path: string; path: string;
params?: Record<string, string>; params?: Record<string, string>;
recordResponseTime?: (time: number) => void;
}) => }) =>
async ({ signal }: { signal: AbortSignal }) => { async ({ signal }: { signal: AbortSignal }) => {
const queryString = params const queryString = params
@ -33,6 +35,8 @@ const createQueryFn =
: ""; : "";
try { try {
const startTime = Date.now();
const res = await fetch( const res = await fetch(
`${pathPrefix}/${API_PATH}${path}${queryString}`, `${pathPrefix}/${API_PATH}${path}${queryString}`,
{ {
@ -54,6 +58,10 @@ const createQueryFn =
const apiRes = (await res.json()) as APIResponse<T>; const apiRes = (await res.json()) as APIResponse<T>;
if (recordResponseTime) {
recordResponseTime(Date.now() - startTime);
}
if (apiRes.status === "error") { if (apiRes.status === "error") {
throw new Error( throw new Error(
apiRes.error !== undefined apiRes.error !== undefined
@ -84,6 +92,7 @@ type QueryOptions = {
path: string; path: string;
params?: Record<string, string>; params?: Record<string, string>;
enabled?: boolean; enabled?: boolean;
recordResponseTime?: (time: number) => void;
}; };
export const useAPIQuery = <T>({ export const useAPIQuery = <T>({
@ -91,6 +100,7 @@ export const useAPIQuery = <T>({
path, path,
params, params,
enabled, enabled,
recordResponseTime,
}: QueryOptions) => { }: QueryOptions) => {
const { pathPrefix } = useSettings(); const { pathPrefix } = useSettings();
@ -100,7 +110,7 @@ export const useAPIQuery = <T>({
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
gcTime: 0, gcTime: 0,
enabled, enabled,
queryFn: createQueryFn({ pathPrefix, path, params }), queryFn: createQueryFn({ pathPrefix, path, params, recordResponseTime }),
}); });
}; };

View file

@ -35,6 +35,9 @@ import HistogramChart from "./HistogramChart";
import { Histogram } from "../../types/types"; import { Histogram } from "../../types/types";
import { bucketRangeString } from "./HistogramHelpers"; import { bucketRangeString } from "./HistogramHelpers";
import { useSettings } from "../../state/settingsSlice"; import { useSettings } from "../../state/settingsSlice";
import { useAppDispatch, useAppSelector } from "../../state/hooks";
import { setVisualizer } from "../../state/queryPageSlice";
import TimeInput from "./TimeInput";
dayjs.extend(timezone); dayjs.extend(timezone);
const maxFormattableSeries = 1000; const maxFormattableSeries = 1000;
@ -51,14 +54,21 @@ const limitSeries = <S extends InstantSample | RangeSamples>(
}; };
export interface DataTableProps { export interface DataTableProps {
expr: string; panelIdx: number;
evalTime: number | null;
retriggerIdx: number; retriggerIdx: number;
} }
const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => { const DataTable: FC<DataTableProps> = ({ panelIdx, retriggerIdx }) => {
const [scale, setScale] = useState<string>("exponential"); const [scale, setScale] = useState<string>("exponential");
const [limitResults, setLimitResults] = useState<boolean>(true); 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 } = const { data, error, isFetching, isLoading, refetch } =
useAPIQuery<InstantQueryResult>({ useAPIQuery<InstantQueryResult>({
@ -66,14 +76,15 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
path: "/query", path: "/query",
params: { params: {
query: expr, query: expr,
time: `${(evalTime !== null ? evalTime : Date.now()) / 1000}`, time: `${(endTime !== null ? endTime : Date.now()) / 1000}`,
}, },
enabled: expr !== "", enabled: expr !== "",
recordResponseTime: setResponseTime,
}); });
useEffect(() => { useEffect(() => {
expr !== "" && refetch(); expr !== "" && refetch();
}, [retriggerIdx, refetch, expr, evalTime]); }, [retriggerIdx, refetch, expr, endTime]);
useLayoutEffect(() => { useLayoutEffect(() => {
setLimitResults(true); setLimitResults(true);
@ -121,7 +132,40 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
const doFormat = result.length <= maxFormattableSeries; const doFormat = result.length <= maxFormattableSeries;
return ( 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 &ensp; Result series: {result.length}
</Text>
</Group>
{limitResults && {limitResults &&
["vector", "matrix"].includes(resultType) && ["vector", "matrix"].includes(resultType) &&
result.length > maxDisplayableSeries && ( result.length > maxDisplayableSeries && (
@ -137,6 +181,7 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
</Anchor> </Anchor>
</Alert> </Alert>
)} )}
{!doFormat && ( {!doFormat && (
<Alert <Alert
title="Formatting turned off" title="Formatting turned off"
@ -146,6 +191,7 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
formatting for performance reasons. formatting for performance reasons.
</Alert> </Alert>
)} )}
<Box pos="relative" className={classes.tableWrapper}> <Box pos="relative" className={classes.tableWrapper}>
<LoadingOverlay <LoadingOverlay
visible={isFetching} visible={isFetching}
@ -156,10 +202,12 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
}} }}
styles={{ loader: { width: "100%", height: "100%" } }} styles={{ loader: { width: "100%", height: "100%" } }}
/> />
<Table fz="xs"> <Table fz="xs">
<Table.Tbody> <Table.Tbody>
{resultType === "vector" ? ( {resultType === "vector" ? (
limitSeries<InstantSample>(result, limitResults).map((s, idx) => ( limitSeries<InstantSample>(result, limitResults).map(
(s, idx) => (
<Table.Tr key={idx}> <Table.Tr key={idx}>
<Table.Td> <Table.Td>
<SeriesName labels={s.metric} format={doFormat} /> <SeriesName labels={s.metric} format={doFormat} />
@ -173,10 +221,15 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
index={idx} index={idx}
scale={scale} scale={scale}
/> />
<Group justify="space-between" align="center" p={10}> <Group
justify="space-between"
align="center"
p={10}
>
<Group align="center" gap="1rem"> <Group align="center" gap="1rem">
<span> <span>
<strong>Count:</strong> {s.histogram[1].count} <strong>Count:</strong>{" "}
{s.histogram[1].count}
</span> </span>
<span> <span>
<strong>Sum:</strong> {s.histogram[1].sum} <strong>Sum:</strong> {s.histogram[1].sum}
@ -197,9 +250,11 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
)} )}
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
)) )
)
) : resultType === "matrix" ? ( ) : resultType === "matrix" ? (
limitSeries<RangeSamples>(result, limitResults).map((s, idx) => ( limitSeries<RangeSamples>(result, limitResults).map(
(s, idx) => (
<Table.Tr key={idx}> <Table.Tr key={idx}>
<Table.Td> <Table.Td>
<SeriesName labels={s.metric} format={doFormat} /> <SeriesName labels={s.metric} format={doFormat} />
@ -221,11 +276,14 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
))} ))}
</Table.Td> </Table.Td>
</Table.Tr> </Table.Tr>
)) )
)
) : resultType === "scalar" ? ( ) : resultType === "scalar" ? (
<Table.Tr> <Table.Tr>
<Table.Td>Scalar value</Table.Td> <Table.Td>Scalar value</Table.Td>
<Table.Td className={classes.numberCell}>{result[1]}</Table.Td> <Table.Td className={classes.numberCell}>
{result[1]}
</Table.Td>
</Table.Tr> </Table.Tr>
) : resultType === "string" ? ( ) : resultType === "string" ? (
<Table.Tr> <Table.Tr>
@ -245,6 +303,8 @@ const DataTable: FC<DataTableProps> = ({ expr, evalTime, retriggerIdx }) => {
</Table> </Table>
</Box> </Box>
</> </>
)}
</Stack>
); );
}; };

View file

@ -84,26 +84,7 @@ const QueryPanel: FC<PanelProps> = ({ idx, metricNames }) => {
</Tabs.Tab> </Tabs.Tab>
</Tabs.List> </Tabs.List>
<Tabs.Panel pt="sm" value="table"> <Tabs.Panel pt="sm" value="table">
<Stack gap="lg" mt="sm"> <DataTable panelIdx={idx} retriggerIdx={retriggerIdx} />
<TimeInput
time={panel.visualizer.endTime}
range={panel.visualizer.range}
description="Evaluation time"
onChangeTime={(time) =>
dispatch(
setVisualizer({
idx,
visualizer: { ...panel.visualizer, endTime: time },
})
)
}
/>
<DataTable
expr={panel.expr}
evalTime={panel.visualizer.endTime}
retriggerIdx={retriggerIdx}
/>
</Stack>
</Tabs.Panel> </Tabs.Panel>
<Tabs.Panel <Tabs.Panel
pt="sm" pt="sm"

View file

@ -1,12 +1,4 @@
import { import { ActionIcon, Box, Group, Input, Select, Skeleton } from "@mantine/core";
ActionIcon,
Box,
Checkbox,
Group,
Input,
Select,
Skeleton,
} from "@mantine/core";
import { import {
IconLayoutNavbarCollapse, IconLayoutNavbarCollapse,
IconLayoutNavbarExpand, IconLayoutNavbarExpand,

View file

@ -36,7 +36,7 @@ export const getEffectiveResolution = (
: resolution.density === "medium" : resolution.density === "medium"
? 250 ? 250
: 100; : 100;
return Math.max(Math.floor(range / factor), 1); return Math.max(Math.floor(range / factor / 1000) * 1000, 1000);
} }
case "fixed": case "fixed":
return resolution.value; return resolution.value;