import { FC, useEffect, useId, useState } from "react"; import { Alert, Skeleton, Box, LoadingOverlay } from "@mantine/core"; import { IconAlertTriangle, IconInfoCircle } from "@tabler/icons-react"; import { RangeQueryResult } from "../../api/responseTypes/query"; import { SuccessAPIResponse, useAPIQuery } from "../../api/api"; import classes from "./Graph.module.css"; import { GraphDisplayMode, GraphResolution, getEffectiveResolution, } from "../../state/queryPageSlice"; import "uplot/dist/uPlot.min.css"; import "./uplot.css"; import { useElementSize } from "@mantine/hooks"; import UPlotChart, { UPlotChartRange } from "./UPlotChart"; export interface GraphProps { expr: string; endTime: number | null; range: number; resolution: GraphResolution; showExemplars: boolean; displayMode: GraphDisplayMode; retriggerIdx: number; onSelectRange: (start: number, end: number) => void; } const Graph: FC = ({ expr, endTime, range, resolution, showExemplars, displayMode, retriggerIdx, onSelectRange, }) => { const { ref, width } = useElementSize(); const [rerender, setRerender] = useState(true); const effectiveEndTime = (endTime !== null ? endTime : Date.now()) / 1000; const startTime = effectiveEndTime - range / 1000; const effectiveResolution = getEffectiveResolution(resolution, range) / 1000; const { data, error, isFetching, isLoading, refetch } = useAPIQuery({ key: useId(), path: "/query_range", params: { query: expr, step: effectiveResolution.toString(), start: startTime.toString(), end: effectiveEndTime.toString(), }, enabled: expr !== "", }); // Bundle the chart data and the displayed range together. This has two purposes: // 1. If we update them separately, we cause unnecessary rerenders of the uPlot chart itself. // 2. We want to keep displaying the old range in the chart while a query for a new range // is still in progress. const [dataAndRange, setDataAndRange] = useState<{ data: SuccessAPIResponse; range: UPlotChartRange; } | null>(null); useEffect(() => { if (data !== undefined) { setDataAndRange({ data: data, range: { startTime: startTime, endTime: effectiveEndTime, resolution: effectiveResolution, }, }); } // We actually want to update the displayed range only once the new data is there, // so we don't want to include any of the range-related parameters in the dependencies. // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); // Re-execute the query when the user presses Enter (or hits the Execute button). useEffect(() => { expr !== "" && refetch(); }, [retriggerIdx, refetch, expr, endTime, range, resolution]); // The useElementSize hook above only gets a valid size on the second render, so this // is a workaround to make the component render twice after mount. useEffect(() => { if (dataAndRange !== null && rerender) { setRerender(false); } }, [dataAndRange, rerender, setRerender]); // 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 ( {Array.from(Array(5), (_, i) => ( ))} ); } if (error) { return ( } > {error.message} ); } if (dataAndRange === null) { return No data queried yet; } const { result } = dataAndRange.data.data; if (result.length === 0) { return ( }> This query returned no data. ); } return ( , // }} // styles={{ loader: { width: "100%", height: "100%" } }} /> ); }; export default Graph;