Compare commits

...

2 Commits

Author SHA1 Message Date
Ashwin Bhatkal
a139915f4e fix: guard against undefined spread in useGetQueryLabels (#10550)
Some checks are pending
build-staging / prepare (push) Waiting to run
build-staging / js-build (push) Blocked by required conditions
build-staging / go-build (push) Blocked by required conditions
build-staging / staging (push) Blocked by required conditions
Release Drafter / update_release_draft (push) Waiting to run
* fix: guard against undefined spread in useGetQueryLabels

* chore: add tests
2026-03-10 18:30:31 +00:00
Abhi kumar
b69bcd63ba fix: added fix for apiresponse being undefined in panel config creation (#10549)
* fix: added fix for apiresponse being undefined in panel config creation

* chore: pr review changes
2026-03-10 17:35:34 +00:00
9 changed files with 158 additions and 20 deletions

View File

@@ -6,7 +6,6 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import ContextMenu from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -62,7 +61,7 @@ function BarPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -11,7 +11,6 @@ import { get } from 'lodash-es';
import { Widgets } from 'types/api/dashboard/getAll';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { QueryData } from 'types/api/widgets/getQuery';
import { AlignedData } from 'uplot';
import { PanelMode } from '../types';
@@ -44,7 +43,7 @@ export function prepareBarPanelConfig({
currentQuery: Query;
onClick: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
@@ -76,13 +75,17 @@ export function prepareBarPanelConfig({
stepInterval: minStepInterval,
});
if (!(apiResponse && apiResponse?.data?.result)) {
// if no data, return the builder without adding any series
return builder;
}
if (widget.stackedBarChart) {
const seriesCount = (apiResponse?.data?.result?.length ?? 0) + 1; // +1 for 1-based uPlot series indices
const seriesCount = (apiResponse.data.result.length ?? 0) + 1; // +1 for 1-based uPlot series indices
builder.setBands(getInitialStackedBands(seriesCount));
}
const seriesList: QueryData[] = apiResponse?.data?.result || [];
seriesList.forEach((series) => {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,
series.queryName || '', // query

View File

@@ -6,7 +6,6 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { DashboardCursorSync } from 'lib/uPlotV2/plugins/TooltipPlugin/types';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import Histogram from '../../charts/Histogram/Histogram';
@@ -39,7 +38,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return prepareHistogramPanelConfig({
widget,
isDarkMode,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
panelMode,
});
}, [widget, isDarkMode, queryResponse?.data?.payload, panelMode]);
@@ -49,7 +48,7 @@ function HistogramPanel(props: PanelWrapperProps): JSX.Element {
return [];
}
return prepareHistogramPanelData({
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
bucketWidth: widget?.bucketWidth,
bucketCount: widget?.bucketCount,
mergeAllActiveQueries: widget?.mergeAllActiveQueries,

View File

@@ -149,7 +149,7 @@ export function prepareHistogramPanelConfig({
isDarkMode,
}: {
widget: Widgets;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
panelMode: PanelMode;
isDarkMode: boolean;
}): UPlotConfigBuilder {
@@ -204,7 +204,7 @@ export function prepareHistogramPanelConfig({
fillColor: '#4E74F8',
isDarkMode,
});
} else {
} else if (apiResponse && apiResponse?.data?.result) {
apiResponse.data.result.forEach((series) => {
const baseLabelName = getLabelName(
series.metric,

View File

@@ -9,7 +9,6 @@ import { useResizeObserver } from 'hooks/useDimensions';
import { LegendPosition } from 'lib/uPlotV2/components/types';
import { ContextMenu } from 'periscope/components/ContextMenu';
import { useTimezone } from 'providers/Timezone';
import { MetricRangePayloadProps } from 'types/api/metrics/getQueryRange';
import uPlot from 'uplot';
import { getTimeRange } from 'utils/getTimeRange';
@@ -68,7 +67,7 @@ function TimeSeriesPanel(props: PanelWrapperProps): JSX.Element {
currentQuery: widget.query,
onClick: clickHandlerWithContextMenu,
onDragSelect,
apiResponse: queryResponse?.data?.payload as MetricRangePayloadProps,
apiResponse: queryResponse?.data?.payload,
timezone,
panelMode,
minTimeScale: minTimeScale,

View File

@@ -68,11 +68,12 @@ export const prepareUPlotConfig = ({
currentQuery: Query;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect: (startTime: number, endTime: number) => void;
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
timezone: Timezone;
panelMode: PanelMode;
minTimeScale?: number;
maxTimeScale?: number;
// eslint-disable-next-line sonarjs/cognitive-complexity
}): UPlotConfigBuilder => {
const stepIntervals: ExecStats['stepIntervals'] = get(
apiResponse,
@@ -100,7 +101,12 @@ export const prepareUPlotConfig = ({
stepInterval: minStepInterval,
});
apiResponse.data?.result?.forEach((series) => {
if (!(apiResponse && apiResponse.data.result)) {
// if no data, return the builder without adding any series
return builder;
}
apiResponse.data.result.forEach((series) => {
const hasSingleValidPoint = hasSingleVisiblePointForSeries(series);
const baseLabelName = getLabelName(
series.metric,

View File

@@ -18,7 +18,7 @@ import { PanelMode } from '../types';
export interface BaseConfigBuilderProps {
id: string;
thresholds?: ThresholdProps[];
apiResponse: MetricRangePayloadProps;
apiResponse?: MetricRangePayloadProps;
isDarkMode: boolean;
onClick?: OnClickPluginOpts['onClick'];
onDragSelect?: (startTime: number, endTime: number) => void;

View File

@@ -0,0 +1,129 @@
import { renderHook } from '@testing-library/react';
import {
IBuilderFormula,
IClickHouseQuery,
IPromQLQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { useGetQueryLabels } from './useGetQueryLabels';
jest.mock('components/QueryBuilderV2/utils', () => ({
getQueryLabelWithAggregation: jest.fn(() => []),
}));
function buildQuery(overrides: Partial<Query> = {}): Query {
return {
id: 'test-id',
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [],
clickhouse_sql: [],
...overrides,
};
}
describe('useGetQueryLabels', () => {
describe('QUERY_BUILDER type', () => {
it('returns empty array when queryFormulas is undefined', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: (undefined as unknown) as IBuilderFormula[],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns formula labels when queryFormulas is populated', () => {
const query = buildQuery({
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [],
queryFormulas: [
({ queryName: 'F1' } as unknown) as IBuilderFormula,
({ queryName: 'F2' } as unknown) as IBuilderFormula,
],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'F1', value: 'F1' },
{ label: 'F2', value: 'F2' },
]);
});
});
describe('CLICKHOUSE type', () => {
it('returns empty array when clickhouse_sql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: (undefined as unknown) as IClickHouseQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from clickhouse_sql when populated', () => {
const query = buildQuery({
queryType: EQueryType.CLICKHOUSE,
clickhouse_sql: [
({ name: 'query_a' } as unknown) as IClickHouseQuery,
({ name: 'query_b' } as unknown) as IClickHouseQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'query_a', value: 'query_a' },
{ label: 'query_b', value: 'query_b' },
]);
});
});
describe('PROM type (default)', () => {
it('returns empty array when promql is undefined', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: (undefined as unknown) as IPromQLQuery[],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([]);
});
it('returns labels from promql when populated', () => {
const query = buildQuery({
queryType: EQueryType.PROM,
promql: [
({ name: 'prom_1' } as unknown) as IPromQLQuery,
({ name: 'prom_2' } as unknown) as IPromQLQuery,
],
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toEqual([
{ label: 'prom_1', value: 'prom_1' },
{ label: 'prom_2', value: 'prom_2' },
]);
});
});
});

View File

@@ -11,7 +11,7 @@ export const useGetQueryLabels = (
const queryLabels = getQueryLabelWithAggregation(
currentQuery?.builder?.queryData || [],
);
const formulaLabels = currentQuery?.builder?.queryFormulas?.map(
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
(formula) => ({
label: formula.queryName,
value: formula.queryName,
@@ -20,10 +20,13 @@ export const useGetQueryLabels = (
return [...queryLabels, ...formulaLabels];
}
if (currentQuery?.queryType === EQueryType.CLICKHOUSE) {
return currentQuery?.clickhouse_sql?.map((q) => ({
return (currentQuery?.clickhouse_sql ?? []).map((q) => ({
label: q.name,
value: q.name,
}));
}
return currentQuery?.promql?.map((q) => ({ label: q.name, value: q.name }));
return (currentQuery?.promql ?? []).map((q) => ({
label: q.name,
value: q.name,
}));
}, [currentQuery]);