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');
+ });
+});