/* eslint-disable react/require-default-props */ import './QueryAddOns.styles.scss'; 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 { 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'; import { MetricAggregation } from 'types/api/v5/queryRange'; import { DataSource, ReduceOperators } from 'types/common/queryBuilder'; import HavingFilter from './HavingFilter/HavingFilter'; 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', }; const ADD_ONS = [ { icon: , label: 'Group By', key: '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: '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: '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: '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: '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: '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', }; // 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, }: { query: IBuilderQuery; version: string; isListViewPanel: boolean; showReduceTo: boolean; panelType: PANEL_TYPES | null; index: number; }): JSX.Element { const [addOns, setAddOns] = useState(ADD_ONS); const [selectedViews, setSelectedViews] = useState([]); const { handleChangeQueryData } = useQueryOperations({ index, query, entityVersion: '', }); 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); // Filter out group_by for metrics data source if (query.dataSource === DataSource.METRICS) { filteredAddOns = filteredAddOns.filter( (addOn) => addOn.key !== ADD_ONS_KEYS.GROUP_BY, ); } } // 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), ), ); // eslint-disable-next-line react-hooks/exhaustive-deps }, [panelType, isListViewPanel, query.dataSource]); 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
)} {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 && (
)} {selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
} placement="top" mouseEnterDelay={0.5} >
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;