diff --git a/frontend/src/components/InputWithLabel/InputWithLabel.tsx b/frontend/src/components/InputWithLabel/InputWithLabel.tsx index 0e089acea986..a95318fe9cdf 100644 --- a/frontend/src/components/InputWithLabel/InputWithLabel.tsx +++ b/frontend/src/components/InputWithLabel/InputWithLabel.tsx @@ -49,6 +49,7 @@ function InputWithLabel({ value={inputValue} onChange={handleChange} name={label.toLowerCase()} + data-testid={`input-${label}`} /> {labelAfter && {label}} {onClose && ( diff --git a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/QueryAddOns.tsx b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/QueryAddOns.tsx index 997390f20989..f0fecfea2cbc 100644 --- a/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/QueryAddOns.tsx +++ b/frontend/src/components/QueryBuilderV2/QueryV2/QueryAddOns/QueryAddOns.tsx @@ -9,7 +9,7 @@ import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/Orde import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter'; import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder'; 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 { useCallback, useEffect, useState } from 'react'; import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData'; @@ -34,6 +34,14 @@ const ADD_ONS_KEYS = { 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 = [ { icon: , @@ -91,6 +99,9 @@ const REDUCE_TO = { '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 function TooltipContent({ label, @@ -195,21 +206,29 @@ function QueryAddOns({ } } - // add reduce to if showReduceTo is true if (showReduceTo) { filteredAddOns = [...filteredAddOns, REDUCE_TO]; } - setAddOns(filteredAddOns); - // Filter selectedViews to only include add-ons present in filteredAddOns - setSelectedViews((prevSelectedViews) => - prevSelectedViews.filter((view) => - filteredAddOns.some((addOn) => addOn.key === view.key), + const activeAddOnKeys = new Set( + Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH) + .filter(([, path]) => hasValue(get(query, path))) + .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 - }, [panelType, isListViewPanel, query.dataSource]); + }, [panelType, isListViewPanel, query]); const handleOptionClick = (e: RadioChangeEvent): void => { if (selectedViews.find((view) => view.key === e.target.value.key)) { @@ -285,7 +304,7 @@ function QueryAddOns({ {selectedViews.length > 0 && (
{selectedViews.find((view) => view.key === 'group_by') && ( -
+
)} {selectedViews.find((view) => view.key === 'having') && ( -
+
)} {selectedViews.find((view) => view.key === 'limit') && ( -
+
)} {selectedViews.find((view) => view.key === 'order_by') && ( -
+
view.key === 'reduce_to') && showReduceTo && ( -
+
view.key === 'legend_format') && ( -
+
({ + useQueryOperations: () => ({ + handleChangeQueryData: mockHandleChangeQueryData, + }), +})); + +jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({ + useQueryBuilder: () => ({ + handleSetQueryData: mockHandleSetQueryData, + }), +})); + +jest.mock('container/QueryBuilder/filters/GroupByFilter/GroupByFilter', () => ({ + GroupByFilter: ({ onChange }: any) => ( + + ), +})); + +jest.mock('container/QueryBuilder/filters/OrderByFilter/OrderByFilter', () => ({ + OrderByFilter: ({ onChange }: any) => ( + + ), +})); + +jest.mock('../QueryV2/QueryAddOns/HavingFilter/HavingFilter', () => ({ + __esModule: true, + default: ({ onChange, onClose }: any) => ( +
+ + +
+ ), +})); + +jest.mock( + 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter', + () => ({ + ReduceToFilter: ({ onChange }: any) => ( + + ), + }), +); + +function baseQuery(overrides: Partial = {}): 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( + , + ); + + 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( + , + ); + + expect(screen.queryByTestId('group-by-content')).not.toBeInTheDocument(); + }); + + it('defaults to Order By open in list view panel', () => { + render( + , + ); + + expect(screen.getByTestId('order-by-content')).toBeInTheDocument(); + }); + + it('limit input auto-opens when limit is set and changing it calls handler', () => { + render( + , + ); + + 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( + , + ); + + 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'); + }); +});