Compare commits

...

1 Commits

Author SHA1 Message Date
Abhi Kumar
f00d52f8bb chore: added changes to auto-run query when disabled, changed groupby and changed orderBy 2026-05-19 15:55:49 +05:30
8 changed files with 285 additions and 52 deletions

View File

@@ -66,7 +66,7 @@ const MetricsAggregateSection = memo(function MetricsAggregateSection({
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
handleChangeQueryData('groupBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);

View File

@@ -283,14 +283,14 @@ function QueryAddOns({
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
handleChangeQueryData('groupBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
handleChangeQueryData('orderBy', value, { runAfterUpdate: true });
},
[handleChangeQueryData],
);

View File

@@ -73,8 +73,8 @@ export const QueryV2 = forwardRef(function QueryV2(
});
const handleToggleDisableQuery = useCallback(() => {
handleChangeQueryData('disabled', !query.disabled);
}, [handleChangeQueryData, query]);
handleChangeQueryData('disabled', !query.disabled, { runAfterUpdate: true });
}, [handleChangeQueryData, query.disabled]);
const handleToggleCollapsQuery = (): void => {
setIsCollapsed(!isCollapsed);

View File

@@ -5,7 +5,8 @@ import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
DataSource,
MetricAggregateOperator,
@@ -333,3 +334,196 @@ describe('useQueryBuilderOperations - Empty Aggregate Attribute Type', () => {
});
});
});
describe('useQueryBuilderOperations - handleChangeQueryData runAfterUpdate', () => {
const mockHandleSetQueryData = jest.fn();
const mockHandleSetTraceOperatorData = jest.fn();
const mockHandleRunQuery = jest.fn();
const baseQuery: IBuilderQuery = {
dataSource: DataSource.METRICS,
aggregateOperator: MetricAggregateOperator.AVG,
aggregateAttribute: {
key: 'system.cpu.load',
dataType: DataTypes.Float64,
type: ATTRIBUTE_TYPES.GAUGE,
} as BaseAutocompleteData,
timeAggregation: MetricAggregateOperator.AVG,
spaceAggregation: '',
aggregations: [],
having: [],
limit: null,
queryName: 'A',
functions: [],
filters: { items: [], op: 'AND' },
groupBy: [],
orderBy: [],
stepInterval: 60,
expression: '',
disabled: false,
reduceTo: ReduceOperators.AVG,
legend: '',
};
const otherQuery: IBuilderQuery = { ...baseQuery, queryName: 'B' };
const buildCurrentQuery = (): Query => ({
queryType: EQueryType.QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
id: 'q-1',
unit: '',
builder: {
queryData: [baseQuery, otherQuery],
queryFormulas: [],
queryTraceOperator: [],
},
});
const setupMock = (overrides: Record<string, unknown> = {}): void => {
(useQueryBuilder as jest.Mock).mockReturnValue({
handleSetQueryData: mockHandleSetQueryData,
handleSetTraceOperatorData: mockHandleSetTraceOperatorData,
handleSetFormulaData: jest.fn(),
removeQueryBuilderEntityByIndex: jest.fn(),
setLastUsedQuery: jest.fn(),
redirectWithQueryBuilderData: jest.fn(),
handleRunQuery: mockHandleRunQuery,
panelType: 'time_series',
currentQuery: buildCurrentQuery(),
...overrides,
});
};
beforeEach(() => {
jest.clearAllMocks();
setupMock();
});
it('does not call handleRunQuery when options is omitted', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('legend', 'cpu-load');
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({ legend: 'cpu-load' }),
);
expect(mockHandleRunQuery).not.toHaveBeenCalled();
});
it('calls handleRunQuery with the freshly-changed query when runAfterUpdate is true', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('disabled', true, {
runAfterUpdate: true,
});
});
expect(mockHandleSetQueryData).toHaveBeenCalledWith(
0,
expect.objectContaining({ disabled: true }),
);
expect(mockHandleRunQuery).toHaveBeenCalledTimes(1);
const [override] = mockHandleRunQuery.mock.calls[0];
// Index 0 reflects the new value...
expect(override.builder.queryData[0]).toStrictEqual(
expect.objectContaining({ queryName: 'A', disabled: true }),
);
// ...siblings stay untouched.
expect(override.builder.queryData[1]).toStrictEqual(
expect.objectContaining({ queryName: 'B', disabled: false }),
);
});
it('applies the change at the correct index without disturbing other queries', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: otherQuery,
index: 1,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData(
'groupBy',
[
{
key: 'host.name',
type: 'tag',
dataType: DataTypes.String,
} as BaseAutocompleteData,
],
{ runAfterUpdate: true },
);
});
const [override] = mockHandleRunQuery.mock.calls[0];
expect(override.builder.queryData[0]).toStrictEqual(
expect.objectContaining({ queryName: 'A', groupBy: [] }),
);
expect(override.builder.queryData[1]).toStrictEqual(
expect.objectContaining({
queryName: 'B',
groupBy: [expect.objectContaining({ key: 'host.name' })],
}),
);
});
it('keeps handleSetQueryData and handleRunQuery in sync for legend formatting', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
}),
);
act(() => {
result.current.handleChangeQueryData('legend', '{{service.name}}', {
runAfterUpdate: true,
});
});
const [override] = mockHandleRunQuery.mock.calls[0];
const setCallLegend = mockHandleSetQueryData.mock.calls[0][1].legend;
expect(override.builder.queryData[0].legend).toBe(setCallLegend);
});
it('does not call handleRunQuery for trace-operator queries (early return)', () => {
const { result } = renderHook(() =>
useQueryOperations({
query: baseQuery,
index: 0,
entityVersion: ENTITY_VERSION_V4,
isForTraceOperator: true,
}),
);
act(() => {
result.current.handleChangeQueryData('disabled', true, {
runAfterUpdate: true,
});
});
expect(mockHandleSetTraceOperatorData).toHaveBeenCalledTimes(1);
expect(mockHandleSetQueryData).not.toHaveBeenCalled();
expect(mockHandleRunQuery).not.toHaveBeenCalled();
});
});

View File

@@ -45,6 +45,7 @@ import {
import {
HandleChangeFormulaData,
HandleChangeQueryData,
HandleChangeQueryDataOptions,
HandleChangeQueryDataV5,
UseQueryOperations,
} from 'types/common/operations.types';
@@ -76,6 +77,7 @@ export const useQueryOperations: UseQueryOperations = ({
currentQuery,
setLastUsedQuery,
redirectWithQueryBuilderData,
handleRunQuery,
} = useQueryBuilder();
const [operators, setOperators] = useState<SelectOption<string, string>[]>([]);
@@ -530,7 +532,7 @@ export const useQueryOperations: UseQueryOperations = ({
const handleChangeQueryData: HandleChangeQueryData | HandleChangeQueryDataV5 =
useCallback(
(key: string, value: any) => {
(key: string, value: any, options?: HandleChangeQueryDataOptions) => {
const newQuery = {
...query,
[key]:
@@ -541,8 +543,24 @@ export const useQueryOperations: UseQueryOperations = ({
if (isForTraceOperator) {
handleSetTraceOperatorData(index, newQuery);
} else {
handleSetQueryData(index, newQuery);
return;
}
handleSetQueryData(index, newQuery);
// `runAfterUpdate` lets callers stage-and-run inline. We pass the
// freshly-computed query straight to `handleRunQuery` because the
// setState above hasn't flushed yet.
if (options?.runAfterUpdate) {
handleRunQuery({
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, i) =>
i === index ? newQuery : item,
),
},
});
}
},
[
@@ -551,6 +569,8 @@ export const useQueryOperations: UseQueryOperations = ({
handleSetQueryData,
handleSetTraceOperatorData,
isForTraceOperator,
handleRunQuery,
currentQuery,
],
);

View File

@@ -1024,49 +1024,57 @@ export function QueryBuilderProvider({
[],
);
const handleRunQuery = useCallback(() => {
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
setCalledFromHandleRunQuery(true);
}
const currentQueryData = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
})),
},
};
// `overrideQuery` lets callers run a query value that hasn't been committed
// to `currentQuery` state yet — e.g. a click handler that toggles a flag
// and wants to stage-and-run in the same tick, without waiting for the
// state update to flush.
const handleRunQuery = useCallback(
(overrideQuery?: Query) => {
const isExplorer =
location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER;
if (isExplorer) {
setCalledFromHandleRunQuery(true);
}
const sourceQuery = overrideQuery ?? currentQuery;
const currentQueryData = {
...sourceQuery,
builder: {
...sourceQuery.builder,
queryData: sourceQuery.builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
})),
},
};
redirectWithQueryBuilderData({
...{
...currentQueryData,
...updateStepInterval({
builder: currentQueryData.builder,
clickhouse_sql: currentQueryData.clickhouse_sql,
promql: currentQueryData.promql,
id: currentQueryData.id,
queryType,
unit: currentQueryData.unit,
}),
},
queryType,
});
}, [currentQuery, location.pathname, queryType, redirectWithQueryBuilderData]);
redirectWithQueryBuilderData({
...{
...currentQueryData,
...updateStepInterval({
builder: currentQueryData.builder,
clickhouse_sql: currentQueryData.clickhouse_sql,
promql: currentQueryData.promql,
id: currentQueryData.id,
queryType,
unit: currentQueryData.unit,
}),
},
queryType,
});
},
[currentQuery, location.pathname, queryType, redirectWithQueryBuilderData],
);
useEffect(() => {
if (location.pathname !== currentPathnameRef.current) {

View File

@@ -26,6 +26,16 @@ type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
savePreviousQuery?: boolean;
};
export interface HandleChangeQueryDataOptions {
/**
* When true, stage-and-run the query immediately after the local state
* update — no need to wait for the user to click "Stage and Run".
* Useful for inline toggles (visibility, disable, etc.) where the panel
* should reflect the change without an extra click.
*/
runAfterUpdate?: boolean;
}
// Generic type that can work with both legacy and V5 query types
export type HandleChangeQueryData<T = IBuilderQuery> = <
Key extends keyof T,
@@ -33,6 +43,7 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
>(
key: Key,
value: Value,
options?: HandleChangeQueryDataOptions,
) => void;
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <

View File

@@ -280,7 +280,7 @@ export type QueryBuilderContextType = {
shallStringify?: boolean,
newTab?: boolean,
) => void;
handleRunQuery: () => void;
handleRunQuery: (overrideQuery?: Query) => void;
resetQuery: (newCurrentQuery?: QueryState) => void;
handleOnUnitsChange: (units: Format['id']) => void;
updateAllQueriesOperators: (