|
|
|
|
@@ -1,27 +1,48 @@
|
|
|
|
|
import { useEffect, useMemo } from 'react';
|
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
|
|
|
import { useQueries } from 'react-query';
|
|
|
|
|
// eslint-disable-next-line no-restricted-imports
|
|
|
|
|
import { useSelector } from 'react-redux';
|
|
|
|
|
import { useDispatch, useSelector } from 'react-redux';
|
|
|
|
|
import { useLocation } from 'react-router-dom';
|
|
|
|
|
import { isAxiosError } from 'axios';
|
|
|
|
|
import QueryCancelledPlaceholder from 'components/QueryCancelledPlaceholder';
|
|
|
|
|
import { ENTITY_VERSION_V5 } from 'constants/app';
|
|
|
|
|
import { QueryParams } from 'constants/query';
|
|
|
|
|
import { initialQueryMeterWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
|
|
|
|
import { MAX_QUERY_RETRIES } from 'constants/reactQuery';
|
|
|
|
|
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
|
|
|
|
import BarChart from 'container/DashboardContainer/visualization/charts/BarChart/BarChart';
|
|
|
|
|
import { PanelMode } from 'container/DashboardContainer/visualization/panels/types';
|
|
|
|
|
import { prepareBarPanelConfig } from 'container/DashboardContainer/visualization/panels/BarPanel/utils';
|
|
|
|
|
import EmptyMetricsSearch from 'container/MetricsExplorer/Explorer/EmptyMetricsSearch';
|
|
|
|
|
import { MetricsLoading } from 'container/MetricsExplorer/MetricsLoading/MetricsLoading';
|
|
|
|
|
import { BuilderUnitsFilter } from 'container/QueryBuilder/filters/BuilderUnitsFilter';
|
|
|
|
|
import TimeSeriesView from 'container/TimeSeriesView/TimeSeriesView';
|
|
|
|
|
import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
|
|
|
|
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
|
|
|
|
import { useIsDarkMode } from 'hooks/useDarkMode';
|
|
|
|
|
import { useResizeObserver } from 'hooks/useDimensions';
|
|
|
|
|
import useUrlQuery from 'hooks/useUrlQuery';
|
|
|
|
|
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
|
|
|
|
import GetMinMax from 'lib/getMinMax';
|
|
|
|
|
import getTimeString from 'lib/getTimeString';
|
|
|
|
|
import history from 'lib/history';
|
|
|
|
|
import { GetMetricQueryRange } from 'lib/dashboard/getQueryResults';
|
|
|
|
|
import { LegendPosition } from 'lib/uPlotV2/components/types';
|
|
|
|
|
import { prepareChartData } from 'lib/uPlotV2/utils/dataUtils';
|
|
|
|
|
import { useErrorModal } from 'providers/ErrorModalProvider';
|
|
|
|
|
import { useTimezone } from 'providers/Timezone';
|
|
|
|
|
import { UpdateTimeInterval } from 'store/actions';
|
|
|
|
|
import { AppState } from 'store/reducers';
|
|
|
|
|
import { SuccessResponse } from 'types/api';
|
|
|
|
|
import { Widgets } from 'types/api/dashboard/getAll';
|
|
|
|
|
import APIError from 'types/api/error';
|
|
|
|
|
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
|
|
|
|
|
import { DataSource } from 'types/common/queryBuilder';
|
|
|
|
|
import { GlobalReducer } from 'types/reducer/globalTime';
|
|
|
|
|
import uPlot from 'uplot';
|
|
|
|
|
import { getTimeRange } from 'utils/getTimeRange';
|
|
|
|
|
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
|
|
|
|
|
|
|
|
|
const WIDGET_ID = 'meter-explorer-bar-chart';
|
|
|
|
|
|
|
|
|
|
interface TimeSeriesProps {
|
|
|
|
|
onFetchingStateChange?: (isFetching: boolean) => void;
|
|
|
|
|
@@ -32,9 +53,21 @@ function TimeSeries({
|
|
|
|
|
onFetchingStateChange,
|
|
|
|
|
isCancelled = false,
|
|
|
|
|
}: TimeSeriesProps): JSX.Element {
|
|
|
|
|
const dispatch = useDispatch();
|
|
|
|
|
const urlQuery = useUrlQuery();
|
|
|
|
|
const location = useLocation();
|
|
|
|
|
const graphRef = useRef<HTMLDivElement>(null);
|
|
|
|
|
|
|
|
|
|
const { stagedQuery, currentQuery } = useQueryBuilder();
|
|
|
|
|
const { yAxisUnit, onUnitChange } = useUrlYAxisUnit('');
|
|
|
|
|
|
|
|
|
|
const isDarkMode = useIsDarkMode();
|
|
|
|
|
const { timezone } = useTimezone();
|
|
|
|
|
const containerDimensions = useResizeObserver(graphRef);
|
|
|
|
|
|
|
|
|
|
const [minTimeScale, setMinTimeScale] = useState<number>();
|
|
|
|
|
const [maxTimeScale, setMaxTimeScale] = useState<number>();
|
|
|
|
|
|
|
|
|
|
const {
|
|
|
|
|
selectedTime: globalSelectedTime,
|
|
|
|
|
maxTime,
|
|
|
|
|
@@ -128,48 +161,187 @@ function TimeSeries({
|
|
|
|
|
onFetchingStateChange?.(isFetching);
|
|
|
|
|
}, [isFetching, onFetchingStateChange]);
|
|
|
|
|
|
|
|
|
|
const data = useMemo(() => queries.map(({ data }) => data) ?? [], [queries]);
|
|
|
|
|
|
|
|
|
|
const responseData = useMemo(
|
|
|
|
|
() =>
|
|
|
|
|
data.map((datapoint) =>
|
|
|
|
|
isValidToConvertToMs ? convertDataValueToMs(datapoint) : datapoint,
|
|
|
|
|
),
|
|
|
|
|
[data, isValidToConvertToMs],
|
|
|
|
|
);
|
|
|
|
|
const responseData = useMemo(() => {
|
|
|
|
|
const data = queries.map(({ data }) => data) ?? [];
|
|
|
|
|
return data.map((datapoint) =>
|
|
|
|
|
isValidToConvertToMs ? convertDataValueToMs(datapoint) : datapoint,
|
|
|
|
|
);
|
|
|
|
|
}, [queries, isValidToConvertToMs]);
|
|
|
|
|
|
|
|
|
|
const hasMetricSelected = useMemo(
|
|
|
|
|
() => currentQuery.builder.queryData.some((q) => q.aggregateAttribute?.key),
|
|
|
|
|
[currentQuery],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
useEffect((): void => {
|
|
|
|
|
const { startTime, endTime } = getTimeRange();
|
|
|
|
|
|
|
|
|
|
setMinTimeScale(startTime);
|
|
|
|
|
setMaxTimeScale(endTime);
|
|
|
|
|
}, [maxTime, minTime, globalSelectedTime, responseData]);
|
|
|
|
|
|
|
|
|
|
const onDragSelect = useCallback(
|
|
|
|
|
(start: number, end: number): void => {
|
|
|
|
|
const startTimestamp = Math.trunc(start);
|
|
|
|
|
const endTimestamp = Math.trunc(end);
|
|
|
|
|
|
|
|
|
|
if (startTimestamp !== endTimestamp) {
|
|
|
|
|
dispatch(UpdateTimeInterval('custom', [startTimestamp, endTimestamp]));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { maxTime, minTime } = GetMinMax('custom', [
|
|
|
|
|
startTimestamp,
|
|
|
|
|
endTimestamp,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
urlQuery.set(QueryParams.startTime, minTime.toString());
|
|
|
|
|
urlQuery.set(QueryParams.endTime, maxTime.toString());
|
|
|
|
|
urlQuery.delete(QueryParams.relativeTime);
|
|
|
|
|
const generatedUrl = `${location.pathname}?${urlQuery.toString()}`;
|
|
|
|
|
history.push(generatedUrl);
|
|
|
|
|
},
|
|
|
|
|
[dispatch, location.pathname, urlQuery],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const handleBackNavigation = useCallback((): void => {
|
|
|
|
|
const searchParams = new URLSearchParams(window.location.search);
|
|
|
|
|
const startTime = searchParams.get(QueryParams.startTime);
|
|
|
|
|
const endTime = searchParams.get(QueryParams.endTime);
|
|
|
|
|
const relativeTime = searchParams.get(
|
|
|
|
|
QueryParams.relativeTime,
|
|
|
|
|
) as CustomTimeType;
|
|
|
|
|
|
|
|
|
|
if (relativeTime) {
|
|
|
|
|
dispatch(UpdateTimeInterval(relativeTime));
|
|
|
|
|
} else if (startTime && endTime && startTime !== endTime) {
|
|
|
|
|
dispatch(
|
|
|
|
|
UpdateTimeInterval('custom', [
|
|
|
|
|
parseInt(getTimeString(startTime), 10),
|
|
|
|
|
parseInt(getTimeString(endTime), 10),
|
|
|
|
|
]),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}, [dispatch]);
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
window.addEventListener('popstate', handleBackNavigation);
|
|
|
|
|
|
|
|
|
|
return (): void => {
|
|
|
|
|
window.removeEventListener('popstate', handleBackNavigation);
|
|
|
|
|
};
|
|
|
|
|
}, [handleBackNavigation]);
|
|
|
|
|
|
|
|
|
|
const handleChartClick = useCallback((): void => {
|
|
|
|
|
// noop for explorer view
|
|
|
|
|
}, []);
|
|
|
|
|
|
|
|
|
|
const chartsData = useMemo(() => {
|
|
|
|
|
return responseData.map((response, index) => {
|
|
|
|
|
const apiResponse = response?.payload;
|
|
|
|
|
|
|
|
|
|
const widget: Widgets = {
|
|
|
|
|
id: `${WIDGET_ID}-${index}`,
|
|
|
|
|
panelTypes: PANEL_TYPES.BAR,
|
|
|
|
|
title: '',
|
|
|
|
|
description: '',
|
|
|
|
|
opacity: '1',
|
|
|
|
|
nullZeroValues: 'zero',
|
|
|
|
|
timePreferance: 'GLOBAL_TIME',
|
|
|
|
|
selectedLogFields: null,
|
|
|
|
|
selectedTracesFields: null,
|
|
|
|
|
query: currentQuery,
|
|
|
|
|
yAxisUnit: yAxisUnit || 'short',
|
|
|
|
|
stackedBarChart: true,
|
|
|
|
|
thresholds: [],
|
|
|
|
|
softMin: null,
|
|
|
|
|
softMax: null,
|
|
|
|
|
isLogScale: false,
|
|
|
|
|
customLegendColors: {},
|
|
|
|
|
legendPosition: LegendPosition.BOTTOM,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const config = prepareBarPanelConfig({
|
|
|
|
|
widget,
|
|
|
|
|
isDarkMode,
|
|
|
|
|
currentQuery,
|
|
|
|
|
onClick: handleChartClick,
|
|
|
|
|
onDragSelect,
|
|
|
|
|
apiResponse,
|
|
|
|
|
timezone,
|
|
|
|
|
panelMode: PanelMode.DASHBOARD_EDIT, // Use DASHBOARD_EDIT to avoid localStorage visibility preferences
|
|
|
|
|
minTimeScale,
|
|
|
|
|
maxTimeScale,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const chartData = apiResponse ? prepareChartData(apiResponse) : [];
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
config,
|
|
|
|
|
chartData,
|
|
|
|
|
hasData: chartData.length > 0 && chartData[0]?.length > 0,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}, [
|
|
|
|
|
responseData,
|
|
|
|
|
currentQuery,
|
|
|
|
|
yAxisUnit,
|
|
|
|
|
isDarkMode,
|
|
|
|
|
handleChartClick,
|
|
|
|
|
onDragSelect,
|
|
|
|
|
timezone,
|
|
|
|
|
minTimeScale,
|
|
|
|
|
maxTimeScale,
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
const isLoading = queries.some((q) => q.isLoading);
|
|
|
|
|
const isError = queries.some((q) => q.isError);
|
|
|
|
|
const hasAnyData = chartsData.some((chart) => chart.hasData);
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="meter-time-series-container">
|
|
|
|
|
<BuilderUnitsFilter onChange={onUnitChange} yAxisUnit={yAxisUnit} />
|
|
|
|
|
<div className="time-series-container">
|
|
|
|
|
<div className="time-series-container" ref={graphRef}>
|
|
|
|
|
{!hasMetricSelected && <EmptyMetricsSearch />}
|
|
|
|
|
{isCancelled && hasMetricSelected && (
|
|
|
|
|
<QueryCancelledPlaceholder subText='Click "Run Query" to load metrics.' />
|
|
|
|
|
)}
|
|
|
|
|
{isLoading && hasMetricSelected && !isCancelled && <MetricsLoading />}
|
|
|
|
|
{!isCancelled &&
|
|
|
|
|
hasMetricSelected &&
|
|
|
|
|
responseData.map((datapoint, index) => (
|
|
|
|
|
<div
|
|
|
|
|
className="time-series-view-panel"
|
|
|
|
|
// eslint-disable-next-line react/no-array-index-key
|
|
|
|
|
key={index}
|
|
|
|
|
>
|
|
|
|
|
<TimeSeriesView
|
|
|
|
|
isFilterApplied={false}
|
|
|
|
|
isError={queries[index].isError}
|
|
|
|
|
isLoading={queries[index].isLoading}
|
|
|
|
|
data={datapoint}
|
|
|
|
|
dataSource={DataSource.METRICS}
|
|
|
|
|
yAxisUnit={yAxisUnit}
|
|
|
|
|
panelType={PANEL_TYPES.BAR}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
!isLoading &&
|
|
|
|
|
!isError &&
|
|
|
|
|
!hasAnyData && (
|
|
|
|
|
<EmptyMetricsSearch hasQueryResult={responseData[0] !== undefined} />
|
|
|
|
|
)}
|
|
|
|
|
{!isCancelled &&
|
|
|
|
|
hasMetricSelected &&
|
|
|
|
|
!isLoading &&
|
|
|
|
|
!isError &&
|
|
|
|
|
containerDimensions.width > 0 &&
|
|
|
|
|
containerDimensions.height > 0 &&
|
|
|
|
|
chartsData.map(
|
|
|
|
|
(chart, index) =>
|
|
|
|
|
chart.hasData && (
|
|
|
|
|
<div
|
|
|
|
|
className="time-series-view-panel"
|
|
|
|
|
// oxlint-disable-next-line react/no-array-index-key -- query responses have no stable ID
|
|
|
|
|
key={`${WIDGET_ID}-${index}`}
|
|
|
|
|
>
|
|
|
|
|
<BarChart
|
|
|
|
|
config={chart.config}
|
|
|
|
|
legendConfig={{
|
|
|
|
|
position: LegendPosition.BOTTOM,
|
|
|
|
|
}}
|
|
|
|
|
data={chart.chartData as uPlot.AlignedData}
|
|
|
|
|
width={containerDimensions.width}
|
|
|
|
|
height={containerDimensions.height}
|
|
|
|
|
isStackedBarChart
|
|
|
|
|
yAxisUnit={yAxisUnit || 'short'}
|
|
|
|
|
timezone={timezone}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
),
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
|