chore: automatically show query addon when the value is present even after refresh (#9024)

* chore: automatically show query addon when the value is present even after refresh

* chore: minor cleanup

* test: added tests for queryAddon

* test: removed inputwithlabel mock
This commit is contained in:
Abhi kumar 2025-09-10 19:53:06 +05:30 committed by GitHub
parent 31e042adf7
commit b1ea7eab70
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 220 additions and 14 deletions

View File

@ -49,6 +49,7 @@ function InputWithLabel({
value={inputValue} value={inputValue}
onChange={handleChange} onChange={handleChange}
name={label.toLowerCase()} name={label.toLowerCase()}
data-testid={`input-${label}`}
/> />
{labelAfter && <Typography.Text className="label">{label}</Typography.Text>} {labelAfter && <Typography.Text className="label">{label}</Typography.Text>}
{onClose && ( {onClose && (

View File

@ -9,7 +9,7 @@ import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/Orde
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter'; import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations'; import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { isEmpty } from 'lodash-es'; import { get, isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react'; import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@ -34,6 +34,14 @@ const ADD_ONS_KEYS = {
LEGEND_FORMAT: 'legend_format', LEGEND_FORMAT: 'legend_format',
}; };
const ADD_ONS_KEYS_TO_QUERY_PATH = {
[ADD_ONS_KEYS.GROUP_BY]: 'groupBy',
[ADD_ONS_KEYS.HAVING]: 'having.expression',
[ADD_ONS_KEYS.ORDER_BY]: 'orderBy',
[ADD_ONS_KEYS.LIMIT]: 'limit',
[ADD_ONS_KEYS.LEGEND_FORMAT]: 'legend',
};
const ADD_ONS = [ const ADD_ONS = [
{ {
icon: <BarChart2 size={14} />, icon: <BarChart2 size={14} />,
@ -91,6 +99,9 @@ const REDUCE_TO = {
'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations', 'https://signoz.io/docs/userguide/query-builder-v5/#reduce-operations',
}; };
const hasValue = (value: unknown): boolean =>
value != null && value !== '' && !(Array.isArray(value) && value.length === 0);
// Custom tooltip content component // Custom tooltip content component
function TooltipContent({ function TooltipContent({
label, label,
@ -195,21 +206,29 @@ function QueryAddOns({
} }
} }
// add reduce to if showReduceTo is true
if (showReduceTo) { if (showReduceTo) {
filteredAddOns = [...filteredAddOns, REDUCE_TO]; filteredAddOns = [...filteredAddOns, REDUCE_TO];
} }
setAddOns(filteredAddOns); setAddOns(filteredAddOns);
// Filter selectedViews to only include add-ons present in filteredAddOns const activeAddOnKeys = new Set(
setSelectedViews((prevSelectedViews) => Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
prevSelectedViews.filter((view) => .filter(([, path]) => hasValue(get(query, path)))
filteredAddOns.some((addOn) => addOn.key === view.key), .map(([key]) => key),
);
const availableAddOnKeys = new Set(filteredAddOns.map((addOn) => addOn.key));
// Filter and set selected views: add-ons that are both active and available
setSelectedViews(
ADD_ONS.filter(
(addOn) =>
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
), ),
); );
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [panelType, isListViewPanel, query.dataSource]); }, [panelType, isListViewPanel, query]);
const handleOptionClick = (e: RadioChangeEvent): void => { const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) { if (selectedViews.find((view) => view.key === e.target.value.key)) {
@ -285,7 +304,7 @@ function QueryAddOns({
{selectedViews.length > 0 && ( {selectedViews.length > 0 && (
<div className="selected-add-ons-content"> <div className="selected-add-ons-content">
{selectedViews.find((view) => view.key === 'group_by') && ( {selectedViews.find((view) => view.key === 'group_by') && (
<div className="add-on-content"> <div className="add-on-content" data-testid="group-by-content">
<div className="periscope-input-with-label"> <div className="periscope-input-with-label">
<Tooltip <Tooltip
title={ title={
@ -321,7 +340,7 @@ function QueryAddOns({
</div> </div>
)} )}
{selectedViews.find((view) => view.key === 'having') && ( {selectedViews.find((view) => view.key === 'having') && (
<div className="add-on-content"> <div className="add-on-content" data-testid="having-content">
<div className="periscope-input-with-label"> <div className="periscope-input-with-label">
<Tooltip <Tooltip
title={ title={
@ -353,7 +372,7 @@ function QueryAddOns({
</div> </div>
)} )}
{selectedViews.find((view) => view.key === 'limit') && ( {selectedViews.find((view) => view.key === 'limit') && (
<div className="add-on-content"> <div className="add-on-content" data-testid="limit-content">
<InputWithLabel <InputWithLabel
label="Limit" label="Limit"
onChange={handleChangeLimit} onChange={handleChangeLimit}
@ -367,7 +386,7 @@ function QueryAddOns({
</div> </div>
)} )}
{selectedViews.find((view) => view.key === 'order_by') && ( {selectedViews.find((view) => view.key === 'order_by') && (
<div className="add-on-content"> <div className="add-on-content" data-testid="order-by-content">
<div className="periscope-input-with-label"> <div className="periscope-input-with-label">
<Tooltip <Tooltip
title={ title={
@ -405,7 +424,7 @@ function QueryAddOns({
)} )}
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && ( {selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
<div className="add-on-content"> <div className="add-on-content" data-testid="reduce-to-content">
<div className="periscope-input-with-label"> <div className="periscope-input-with-label">
<Tooltip <Tooltip
title={ title={
@ -436,7 +455,7 @@ function QueryAddOns({
)} )}
{selectedViews.find((view) => view.key === 'legend_format') && ( {selectedViews.find((view) => view.key === 'legend_format') && (
<div className="add-on-content"> <div className="add-on-content" data-testid="legend-format-content">
<InputWithLabel <InputWithLabel
label="Legend format" label="Legend format"
placeholder="Write legend format" placeholder="Write legend format"

View File

@ -0,0 +1,186 @@
/* eslint-disable */
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import QueryAddOns from '../QueryV2/QueryAddOns/QueryAddOns';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { DataSource } from 'types/common/queryBuilder';
// Mocks: only what is required for this component to render and for us to assert handler calls
const mockHandleChangeQueryData = jest.fn();
const mockHandleSetQueryData = jest.fn();
jest.mock('hooks/queryBuilder/useQueryBuilderOperations', () => ({
useQueryOperations: () => ({
handleChangeQueryData: mockHandleChangeQueryData,
}),
}));
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: () => ({
handleSetQueryData: mockHandleSetQueryData,
}),
}));
jest.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({
GroupByFilter: ({ onChange }: any) => (
<button data-testid="groupby" onClick={() => onChange(['service.name'])}>
GroupByFilter
</button>
),
}));
jest.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({
OrderByFilter: ({ onChange }: any) => (
<button
data-testid="orderby"
onClick={() => onChange([{ columnName: 'duration', order: 'desc' }])}
>
OrderByFilter
</button>
),
}));
jest.mock('../QueryV2/QueryAddOns/HavingFilter/HavingFilter', () => ({
__esModule: true,
default: ({ onChange, onClose }: any) => (
<div>
<button data-testid="having-change" onClick={() => onChange('p99 > 500')}>
HavingFilter
</button>
<button data-testid="having-close" onClick={onClose}>
close
</button>
</div>
),
}));
jest.mock(
'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter',
() => ({
ReduceToFilter: ({ onChange }: any) => (
<button data-testid="reduce-to" onClick={() => onChange('sum')}>
ReduceToFilter
</button>
),
}),
);
function baseQuery(overrides: Partial<any> = {}): any {
return {
dataSource: DataSource.TRACES,
aggregations: [{ id: 'a', operator: 'count' }],
groupBy: [],
orderBy: [],
legend: '',
limit: null,
having: { expression: '' },
...overrides,
};
}
describe('QueryAddOns', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('VALUE panel: no sections auto-open when query has no active add-ons', () => {
render(
<QueryAddOns
query={baseQuery()}
version="v5"
isListViewPanel={false}
showReduceTo
panelType={PANEL_TYPES.VALUE}
index={0}
isForTraceOperator={false}
/>,
);
expect(screen.queryByTestId('legend-format-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('reduce-to-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('order-by-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('limit-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('group-by-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('having-content')).not.toBeInTheDocument();
});
it('hides group-by section for METRICS even if groupBy is set in query', () => {
render(
<QueryAddOns
query={baseQuery({
dataSource: DataSource.METRICS,
groupBy: ['service.name'],
})}
version="v5"
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
expect(screen.queryByTestId('group-by-content')).not.toBeInTheDocument();
});
it('defaults to Order By open in list view panel', () => {
render(
<QueryAddOns
query={baseQuery()}
version="v5"
isListViewPanel
showReduceTo={false}
panelType={PANEL_TYPES.LIST}
index={0}
isForTraceOperator={false}
/>,
);
expect(screen.getByTestId('order-by-content')).toBeInTheDocument();
});
it('limit input auto-opens when limit is set and changing it calls handler', () => {
render(
<QueryAddOns
query={baseQuery({ limit: 5 })}
version="v5"
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
const input = screen.getByTestId('input-Limit') as HTMLInputElement;
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
expect(input.value).toBe('5');
fireEvent.change(input, { target: { value: '10' } });
expect(mockHandleChangeQueryData).toHaveBeenCalledWith('limit', 10);
});
it('auto-opens Order By and Limit when present in query', () => {
const query = baseQuery({
orderBy: [{ columnName: 'duration', order: 'desc' }],
limit: 7,
});
render(
<QueryAddOns
query={query}
version="v5"
isListViewPanel={false}
showReduceTo={false}
panelType={PANEL_TYPES.TIME_SERIES}
index={0}
isForTraceOperator={false}
/>,
);
expect(screen.getByTestId('order-by-content')).toBeInTheDocument();
const limitInput = screen.getByTestId('input-Limit') as HTMLInputElement;
expect(screen.getByTestId('limit-content')).toBeInTheDocument();
expect(limitInput.value).toBe('7');
});
});