* feat: added traceoperator component and styles * chore: minor style improvments * feat: added conditions for traceoperator * chore: minor UI fixes * chore: type changes * chore: added initialvalue for trace operators * chore: Added changes to prepare request payload * chore: fixed minor styles + minor ux fix * feat: added span selector * chore: added ui changes in the editor * chore: removed traceoperations and reused queryoperations * chore: minor changes in queryaddon and aggregation for support * chore: added traceoperators in alerts * chore: minor pr review change * chore: linter fix * fix: fixed minor ts issues * fix: added limit support in traceoperator * chore: minor fix in traceoperator styles * chore: linting fix + icon changes * chore: updated type * chore: lint fixes * feat: added changes for showing querynames in alerts * feat: added trace operator grammer + antlr files * feat: added traceoperator context util * chore: added traceoperator validation function * feat: added traceoperator editor * feat: added queryname boost + operator constants * fix: pr reviews * chore: minor ui fix * fix: updated grammer files * test: added test for traceoperatorcontext * chore: removed check for multiple queries in traceexplorer * test: minor test fix * test: fixed breaking mapQueryDataFromApi test * chore: fixed logic to show trace operator * chore: updated docs link * chore: minor ui issue fix * chore: changed trace operator query name * chore: removed using spans from in trace opeartors * fix: added fix for order by in trace opeartor * feat: added changes related to saved in views in trace opeartor * chore: added changes to keep indirect descendent operator at bottom * chore: removed returnspansfrom field from traceoperator * chore: updated file names + regenerated grammer * chore: added beta tag in trace opeartor * chore: pr review fixes * Fix/tsc trace operator + tests (#8942) * fix: added tsc fixes for trace operator * chore: moved traceoperator utils * test: added test for traceopertor util * chore: tsc fix * fix: fixed tsc issue * Feat/trace operator dashboards (#8992) * chore: added callout message for multiple queries without trace operators * feat: added changes for supporting trace operators in dashboards * chore: minor changes for list panel
498 lines
14 KiB
TypeScript
498 lines
14 KiB
TypeScript
/* 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: <BarChart2 size={14} />,
|
|
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: <ScrollText size={14} />,
|
|
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: <ScrollText size={14} />,
|
|
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: <ScrollText size={14} />,
|
|
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: <ScrollText size={14} />,
|
|
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: <ScrollText size={14} />,
|
|
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 (
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: '8px',
|
|
maxWidth: '300px',
|
|
}}
|
|
>
|
|
<strong style={{ fontSize: '14px' }}>{label}</strong>
|
|
{description && (
|
|
<span style={{ fontSize: '12px', lineHeight: '1.5' }}>{description}</span>
|
|
)}
|
|
{docLink && (
|
|
<a
|
|
href={docLink}
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
onClick={(e): void => e.stopPropagation()}
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '4px',
|
|
color: '#4096ff',
|
|
fontSize: '12px',
|
|
marginTop: '4px',
|
|
}}
|
|
>
|
|
Learn more
|
|
<ExternalLink size={12} />
|
|
</a>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
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<AddOn[]>(ADD_ONS);
|
|
|
|
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
|
|
|
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);
|
|
|
|
// 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 (
|
|
<div className="query-add-ons">
|
|
{selectedViews.length > 0 && (
|
|
<div className="selected-add-ons-content">
|
|
{selectedViews.find((view) => view.key === 'group_by') && (
|
|
<div className="add-on-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="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"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Group By
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<GroupByFilter
|
|
disabled={
|
|
query.dataSource === DataSource.METRICS &&
|
|
!(query.aggregations?.[0] as MetricAggregation)?.metricName
|
|
}
|
|
query={query}
|
|
onChange={handleChangeGroupByKeys}
|
|
/>
|
|
</div>
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView('group_by')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === 'having') && (
|
|
<div className="add-on-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="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"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Having
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<HavingFilter
|
|
onClose={(): void => {
|
|
setSelectedViews(
|
|
selectedViews.filter((view) => view.key !== 'having'),
|
|
);
|
|
}}
|
|
onChange={handleChangeHaving}
|
|
queryData={query}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === 'limit') && (
|
|
<div className="add-on-content">
|
|
<InputWithLabel
|
|
label="Limit"
|
|
onChange={handleChangeLimit}
|
|
initialValue={query?.limit ?? undefined}
|
|
placeholder="Enter limit"
|
|
onClose={(): void => {
|
|
setSelectedViews(selectedViews.filter((view) => view.key !== 'limit'));
|
|
}}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === 'order_by') && (
|
|
<div className="add-on-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="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"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Order By
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<OrderByFilter
|
|
entityVersion={version}
|
|
query={query}
|
|
onChange={handleChangeOrderByKeys}
|
|
isListViewPanel={isListViewPanel}
|
|
isNewQueryV2
|
|
/>
|
|
</div>
|
|
{!isListViewPanel && (
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView('order_by')}
|
|
/>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedViews.find((view) => view.key === 'reduce_to') && showReduceTo && (
|
|
<div className="add-on-content">
|
|
<div className="periscope-input-with-label">
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label="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"
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<div className="label" style={{ cursor: 'help' }}>
|
|
Reduce to
|
|
</div>
|
|
</Tooltip>
|
|
<div className="input">
|
|
<ReduceToFilter query={query} onChange={handleChangeReduceToV5} />
|
|
</div>
|
|
|
|
<Button
|
|
className="close-btn periscope-btn ghost"
|
|
icon={<ChevronUp size={16} />}
|
|
onClick={(): void => handleRemoveView('reduce_to')}
|
|
/>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{selectedViews.find((view) => view.key === 'legend_format') && (
|
|
<div className="add-on-content">
|
|
<InputWithLabel
|
|
label="Legend format"
|
|
placeholder="Write legend format"
|
|
onChange={handleChangeQueryLegend}
|
|
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
|
onClose={(): void => {
|
|
setSelectedViews(
|
|
selectedViews.filter((view) => view.key !== 'legend_format'),
|
|
);
|
|
}}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<div className="add-ons-list">
|
|
<Radio.Group
|
|
className="add-ons-tabs"
|
|
onChange={handleOptionClick}
|
|
value={selectedViews}
|
|
>
|
|
{addOns.map((addOn) => (
|
|
<Tooltip
|
|
key={addOn.key}
|
|
title={
|
|
<TooltipContent
|
|
label={addOn.label}
|
|
description={addOn.description}
|
|
docLink={addOn.docLink}
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<Radio.Button
|
|
className={
|
|
selectedViews.find((view) => view.key === addOn.key)
|
|
? 'selected-view tab'
|
|
: 'tab'
|
|
}
|
|
value={addOn}
|
|
>
|
|
<div className="add-on-tab-title">
|
|
{addOn.icon}
|
|
{addOn.label}
|
|
</div>
|
|
</Radio.Button>
|
|
</Tooltip>
|
|
))}
|
|
</Radio.Group>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QueryAddOns;
|