mirror of
https://github.com/SigNoz/signoz.git
synced 2025-12-18 16:07:10 +00:00
* fix: removed unused code for querycontext (#8674) * Update frontend/src/utils/queryValidationUtils.ts Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> * feat: added tooltips in metric aggregations * feat: enabled legend enhancement for explorer pages and alert page * feat: updated the error state in explorer pages with new APIError * fix: cloned panel query shows previous query (#8681) * fix: cloned panel query shows previous query * chore: removed comments * chore: added null check * fix: added fix for auto run query in dashboard panel + trace view issue --------- Co-authored-by: Abhi Kumar <abhikumar@Mac.lan> * feat: added new SubstituteVars api and enable v5 for creating new alerts (#8683) * feat: added new SubstituteVars api and enable v5 for creating new alerts * feat: add warning notification for query response * feat: fixed failing test case * fix: metric histogram UI config state in edit mode * fix: fixed table columns getting duplicate data (#8685) * fix: added fix for conversion of QB function to filter expression. (#8684) * fix: added fix for QB filters for functions * chore: minor fix --------- Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> * feat: query builder fixes and enhancement (#8692) * feat: legend format fixes around single and multiple aggregation * feat: fixed table unit and metric units * feat: add fallbacks to columnWidth and columnUnits for old-dashboards * feat: fixed metric edit issue and having filter suggestion duplications * feat: fix and cleanup functions across product for v5 * chore: add tooltips with links to documentation (#8676) * fix: added fix for query validation and empty query error (#8694) * fix: added fix for selected columns being empty in logs explorer (#8709) * feat: added columnUnit changes for old dashboard migrations (#8706) * fix: fixed keyfetching logic (#8712) * chore: lint fix * fix: fixed logs explorer test * feat: fix type checks --------- Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com> Co-authored-by: SagarRajput-7 <sagar@signoz.io> Co-authored-by: Abhi Kumar <abhikumar@Mac.lan> Co-authored-by: SagarRajput-7 <162284829+SagarRajput-7@users.noreply.github.com> Co-authored-by: Srikanth Chekuri <srikanth.chekuri92@gmail.com>
495 lines
14 KiB
TypeScript
495 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,
|
|
}: {
|
|
query: IBuilderQuery;
|
|
version: string;
|
|
isListViewPanel: boolean;
|
|
showReduceTo: boolean;
|
|
panelType: PANEL_TYPES | null;
|
|
index: number;
|
|
}): JSX.Element {
|
|
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
|
|
|
|
const [selectedViews, setSelectedViews] = useState<AddOn[]>([]);
|
|
|
|
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 (
|
|
<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;
|