import { useCallback, useEffect, useRef, useState } from 'react';
import { Button, Radio, RadioChangeEvent, Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GroupByFilter } from 'container/QueryBuilder/filters/GroupByFilter/GroupByFilter';
import { OrderByFilter } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter';
import { ReduceToFilter } from 'container/QueryBuilder/filters/ReduceToFilter/ReduceToFilter';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { get, isEmpty } from 'lodash-es';
import { BarChart2, ChevronUp, ExternalLink, ScrollText } from 'lucide-react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { MetricAggregation } from 'types/api/v5/queryRange';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import HavingFilter from './HavingFilter/HavingFilter';
import './QueryAddOns.styles.scss';
interface AddOn {
icon: React.ReactNode;
label: string;
key: string;
description?: string;
docLink?: string;
}
const ADD_ONS_KEYS = {
GROUP_BY: 'group_by',
HAVING: 'having',
ORDER_BY: 'order_by',
LIMIT: 'limit',
LEGEND_FORMAT: 'legend_format',
REDUCE_TO: 'reduce_to',
};
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',
[ADD_ONS_KEYS.REDUCE_TO]: 'reduceTo',
};
const ADD_ONS = [
{
icon: ,
label: 'Group By',
key: ADD_ONS_KEYS.GROUP_BY,
description:
'Break down data by attributes like service name, endpoint, status code, or region. Essential for spotting patterns and comparing performance across different segments.',
docLink: 'https://signoz.io/docs/userguide/query-builder-v5/#grouping',
},
{
icon: ,
label: 'Having',
key: ADD_ONS_KEYS.HAVING,
description:
'Filter grouped results based on aggregate conditions. Show only groups meeting specific criteria, like error rates > 5% or p99 latency > 500',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#conditional-filtering-with-having',
},
{
icon: ,
label: 'Order By',
key: ADD_ONS_KEYS.ORDER_BY,
description:
'Sort results to surface what matters most. Quickly identify slowest operations, most frequent errors, or highest resource consumers.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: ,
label: 'Limit',
key: ADD_ONS_KEYS.LIMIT,
description:
'Show only the top/bottom N results. Perfect for focusing on outliers, reducing noise, and improving dashboard performance.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#sorting--limiting',
},
{
icon: ,
label: 'Legend format',
key: ADD_ONS_KEYS.LEGEND_FORMAT,
description:
'Customize series labels using variables like {{service.name}}-{{endpoint}}. Makes charts readable at a glance during incident investigation.',
docLink:
'https://signoz.io/docs/userguide/query-builder-v5/#legend-formatting',
},
];
const REDUCE_TO = {
icon: ,
label: 'Reduce to',
key: ADD_ONS_KEYS.REDUCE_TO,
description:
'Apply mathematical operations like sum, average, min, max, or percentiles to reduce multiple time series into a single value.',
docLink:
'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,
description,
docLink,
}: {
label: string;
description?: string;
docLink?: string;
}): JSX.Element {
return (
);
}
function QueryAddOns({
query,
version,
isListViewPanel,
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
isListViewPanel: boolean;
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState(ADD_ONS);
const [selectedViews, setSelectedViews] = useState([]);
const initializedRef = useRef(false);
const prevAvailableKeysRef = useRef | null>(null);
const { handleChangeQueryData } = useQueryOperations({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();
useEffect(() => {
if (isListViewPanel) {
setAddOns([]);
setSelectedViews([
ADD_ONS.find((addOn) => addOn.key === ADD_ONS_KEYS.ORDER_BY) as AddOn,
]);
return;
}
let filteredAddOns: AddOn[];
if (panelType === PANEL_TYPES.VALUE) {
// Filter out all add-ons except legend format
filteredAddOns = ADD_ONS.filter(
(addOn) => addOn.key === ADD_ONS_KEYS.LEGEND_FORMAT,
);
} else {
filteredAddOns = Object.values(ADD_ONS);
if (query.dataSource === DataSource.METRICS) {
// Filter out group_by for metrics data source (handled in MetricsAggregateSection)
filteredAddOns = filteredAddOns.filter(
(addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY,
);
}
}
if (showReduceTo) {
filteredAddOns = [...filteredAddOns, REDUCE_TO];
}
setAddOns(filteredAddOns);
const availableAddOnKeys = new Set(filteredAddOns.map((a) => a.key));
const previousKeys = prevAvailableKeysRef.current;
const hasAvailabilityItemsChanged =
previousKeys !== null &&
(previousKeys.size !== availableAddOnKeys.size ||
[...availableAddOnKeys].some((key) => !previousKeys.has(key)));
prevAvailableKeysRef.current = availableAddOnKeys;
if (!initializedRef.current || hasAvailabilityItemsChanged) {
initializedRef.current = true;
const activeAddOnKeys = new Set(
Object.entries(ADD_ONS_KEYS_TO_QUERY_PATH)
.filter(([, path]) => hasValue(get(query, path)))
.map(([key]) => key),
);
// Initial seeding from query values on mount
setSelectedViews(
filteredAddOns.filter(
(addOn) =>
activeAddOnKeys.has(addOn.key) && availableAddOnKeys.has(addOn.key),
),
);
return;
}
setSelectedViews((prev) =>
prev.filter((view) =>
filteredAddOns.some((addOn) => addOn.key === view.key),
),
);
}, [panelType, isListViewPanel, query, showReduceTo]);
const handleOptionClick = (e: RadioChangeEvent): void => {
if (selectedViews.find((view) => view.key === e.target.value.key)) {
setSelectedViews(
selectedViews.filter((view) => view.key !== e.target.value.key),
);
} else {
setSelectedViews([...selectedViews, e.target.value]);
}
};
const handleChangeGroupByKeys = useCallback(
(value: IBuilderQuery['groupBy']) => {
handleChangeQueryData('groupBy', value);
},
[handleChangeQueryData],
);
const handleChangeOrderByKeys = useCallback(
(value: IBuilderQuery['orderBy']) => {
handleChangeQueryData('orderBy', value);
},
[handleChangeQueryData],
);
const handleChangeReduceToV5 = useCallback(
(value: ReduceOperators) => {
handleSetQueryData(index, {
...query,
aggregations: [
{
...(query.aggregations?.[0] as MetricAggregation),
reduceTo: value,
},
],
});
},
[handleSetQueryData, index, query],
);
const handleRemoveView = useCallback(
(key: string): void => {
setSelectedViews(selectedViews.filter((view) => view.key !== key));
},
[selectedViews],
);
const handleChangeQueryLegend = useCallback(
(value: string) => {
handleChangeQueryData('legend', value);
},
[handleChangeQueryData],
);
const handleChangeLimit = useCallback(
(value: string) => {
handleChangeQueryData('limit', Number(value) || null);
},
[handleChangeQueryData],
);
const handleChangeHaving = useCallback(
(value: string) => {
handleChangeQueryData('having', {
expression: value,
});
},
[handleChangeQueryData],
);
return (
{selectedViews.length > 0 && (
{selectedViews.find((view) => view.key === 'group_by') && (
}
placement="top"
mouseEnterDelay={0.5}
>
Group By
}
onClick={(): void => handleRemoveView('group_by')}
/>
)}
{selectedViews.find((view) => view.key === 'having') && (
}
placement="top"
mouseEnterDelay={0.5}
>
Having
{
setSelectedViews(
selectedViews.filter((view) => view.key !== 'having'),
);
}}
onChange={handleChangeHaving}
queryData={query}
/>
)}
{selectedViews.find((view) => view.key === 'limit') && (
{
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
}}
closeIcon={}
/>
)}
{selectedViews.find((view) => view.key === 'order_by') && (
}
placement="top"
mouseEnterDelay={0.5}
>
Order By
{!isListViewPanel && (
}
onClick={(): void => handleRemoveView('order_by')}
/>
)}
)}
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
}
placement="top"
mouseEnterDelay={0.5}
>
Reduce to
}
onClick={(): void => handleRemoveView('reduce_to')}
/>
)}
{selectedViews.find((view) => view.key === 'legend_format') && (
{
setSelectedViews(
selectedViews.filter((view) => view.key !== 'legend_format'),
);
}}
closeIcon={}
/>
)}
)}
{addOns.map((addOn) => (
}
placement="top"
mouseEnterDelay={0.5}
>
view.key === addOn.key)
? 'selected-view tab'
: 'tab'
}
value={addOn}
>
{addOn.icon}
{addOn.label}
))}
);
}
export default QueryAddOns;