mirror of
https://github.com/SigNoz/signoz.git
synced 2026-07-06 22:50:38 +01:00
Some checks failed
build-staging / prepare (push) Has been cancelled
build-staging / js-build (push) Has been cancelled
build-staging / go-build (push) Has been cancelled
build-staging / staging (push) Has been cancelled
Release Drafter / update_release_draft (push) Has been cancelled
* fix: update 47 stale docs links in frontend to current URLs Update documentation links across 19 frontend files to match current signoz.io docs structure after product module restructures. Key changes: - Instrumentation links updated to new OpenTelemetry-prefixed paths - product-features/* links replaced with current locations - Query builder links point to new querying module pages - Alert notification channel links point to setup-alerts-notification - SSO, infra monitoring, and version upgrade links corrected Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: update 278 more stale docs links and broken anchors in frontend - Onboarding md-docs: update instrumentation URLs to canonical paths (django/flask/fastapi/falcon → opentelemetry-python, express/nestjs → javascript/opentelemetry-nodejs, springboot → java/opentelemetry-java, tomcat → java/opentelemetry-tomcat, jboss → java/opentelemetry-jboss, golang → opentelemetry-golang, elixir → opentelemetry-elixir, reactjs → frontend-monitoring/sending-traces-with-opentelemetry) - Onboarding md-docs: update tutorial/* → opentelemetry-collection-agents/*, userguide/hostmetrics → infrastructure-monitoring/hostmetrics, userguide/logs# → userguide/logs_query_builder# - Query builder UI: fix broken anchors after query-builder-v5 page restructure (Having → result-manipulation, Order By → result-manipulation, Limit → result-manipulation, Legend → aggregation-grouping, Group By → aggregation-grouping, Formula → multi-query-analysis, Trace Matching → multi-query-analysis, Reduce → result-manipulation, Aggregation functions → aggregation-grouping, Time aggregation → temporal-aggregation) - Fix Apdex link → alerts-management/apdex-alerts - Fix missing spans link → traces-management/troubleshooting/faqs - Fix cost meter, ClickHouse traces, k8s pod logs anchors - Drop broken anchors where sections were removed from docs Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: format CreateAlertRule files after anchor removal Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix: revert OnboardingContainer changes (deprecated module) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: Vishal Sharma <makeavish786@gmail.com> Co-authored-by: Ubuntu <ubuntu@ip-172-31-24-8.eu-central-1.compute.internal>
565 lines
16 KiB
TypeScript
565 lines
16 KiB
TypeScript
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
import { Button, Tooltip } from 'antd';
|
|
import { ToggleGroupSimple } from '@signozhq/ui/toggle-group';
|
|
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 { BarChart, ChevronUp, ExternalLink, ScrollText } from '@signozhq/icons';
|
|
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 { buildDefaultLegendFromGroupBy } from './utils';
|
|
|
|
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: <BarChart size={14} />,
|
|
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/querying/aggregation-grouping/#grouping',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
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/querying/result-manipulation/#conditional-filtering-with-having',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
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/querying/result-manipulation/#sorting--limiting',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
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/querying/result-manipulation/#how-limit-works-for-time-series',
|
|
},
|
|
{
|
|
icon: <ScrollText size={14} />,
|
|
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/querying/aggregation-grouping/#legend-formatting',
|
|
},
|
|
];
|
|
|
|
const REDUCE_TO = {
|
|
icon: <ScrollText size={14} />,
|
|
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/#result-manipulation',
|
|
};
|
|
|
|
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 (
|
|
<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 initializedRef = useRef(false);
|
|
const prevAvailableKeysRef = useRef<Set<string> | 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 = (clickedAddOn: AddOn): void => {
|
|
const isAlreadySelected = selectedViews.some(
|
|
(view) => view.key === clickedAddOn.key,
|
|
);
|
|
|
|
if (isAlreadySelected) {
|
|
setSelectedViews((prev) =>
|
|
prev.filter((view) => view.key !== clickedAddOn.key),
|
|
);
|
|
} else {
|
|
// When enabling Legend format for the first time with an empty legend
|
|
// and existing group-by keys, prefill the legend using all group-by keys.
|
|
// This keeps existing custom legends intact and only helps seed a sensible default.
|
|
if (
|
|
clickedAddOn.key === ADD_ONS_KEYS.LEGEND_FORMAT &&
|
|
isEmpty(query?.legend) &&
|
|
Array.isArray(query.groupBy) &&
|
|
query.groupBy.length > 0
|
|
) {
|
|
const defaultLegend = buildDefaultLegendFromGroupBy(query.groupBy);
|
|
|
|
if (defaultLegend) {
|
|
handleChangeQueryLegend(defaultLegend);
|
|
}
|
|
}
|
|
|
|
setSelectedViews((prev) => [...prev, clickedAddOn]);
|
|
}
|
|
};
|
|
|
|
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((prev) => prev.filter((view) => view.key !== key));
|
|
}, []);
|
|
|
|
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" data-testid="query-add-ons">
|
|
{selectedViews.length > 0 && (
|
|
<div className="selected-add-ons-content">
|
|
{selectedViews.find((view) => view.key === 'group_by') && (
|
|
<div className="add-on-content" data-testid="group-by-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/querying/aggregation-grouping/#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" data-testid="having-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/querying/result-manipulation/#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((prev) =>
|
|
prev.filter((view) => view.key !== 'having'),
|
|
);
|
|
}}
|
|
onChange={handleChangeHaving}
|
|
queryData={query}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === 'limit') && (
|
|
<div className="add-on-content" data-testid="limit-content">
|
|
<InputWithLabel
|
|
label="Limit"
|
|
type="number"
|
|
onChange={handleChangeLimit}
|
|
initialValue={query?.limit ?? undefined}
|
|
placeholder="Enter limit"
|
|
onClose={(): void => {
|
|
setSelectedViews((prev) =>
|
|
prev.filter((view) => view.key !== 'limit'),
|
|
);
|
|
}}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
{selectedViews.find((view) => view.key === 'order_by') && (
|
|
<div className="add-on-content" data-testid="order-by-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/querying/result-manipulation/#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" data-testid="reduce-to-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/#result-manipulation"
|
|
/>
|
|
}
|
|
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" data-testid="legend-format-content">
|
|
<InputWithLabel
|
|
label="Legend format"
|
|
placeholder="Write legend format"
|
|
onChange={handleChangeQueryLegend}
|
|
initialValue={isEmpty(query?.legend) ? undefined : query?.legend}
|
|
onClose={(): void => {
|
|
setSelectedViews((prev) =>
|
|
prev.filter((view) => view.key !== 'legend_format'),
|
|
);
|
|
}}
|
|
closeIcon={<ChevronUp size={16} />}
|
|
/>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
<ToggleGroupSimple
|
|
type="multiple"
|
|
className="add-ons-tabs"
|
|
value={selectedViews.map((view) => view.key)}
|
|
onChange={(newKeys: string[]): void => {
|
|
const oldKeys = selectedViews.map((view) => view.key);
|
|
const toggledKey =
|
|
newKeys.find((k) => !oldKeys.includes(k)) ??
|
|
oldKeys.find((k) => !newKeys.includes(k));
|
|
if (!toggledKey) {
|
|
return;
|
|
}
|
|
const clickedAddOn = addOns.find((a) => a.key === toggledKey);
|
|
if (clickedAddOn) {
|
|
handleOptionClick(clickedAddOn);
|
|
}
|
|
}}
|
|
items={addOns.map((addOn) => ({
|
|
value: addOn.key,
|
|
label: (
|
|
<Tooltip
|
|
title={
|
|
<TooltipContent
|
|
label={addOn.label}
|
|
description={addOn.description}
|
|
docLink={addOn.docLink}
|
|
/>
|
|
}
|
|
placement="top"
|
|
mouseEnterDelay={0.5}
|
|
>
|
|
<span
|
|
className="add-on-tab-title"
|
|
data-testid={`query-add-on-${addOn.key}`}
|
|
>
|
|
{addOn.icon}
|
|
{addOn.label}
|
|
</span>
|
|
</Tooltip>
|
|
),
|
|
}))}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default QueryAddOns;
|