Feature/trace operators (#8869)

* 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
This commit is contained in:
Abhi kumar 2025-09-05 21:30:24 +05:30 committed by GitHub
parent f63f175a77
commit bf704333b3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
89 changed files with 3616 additions and 147 deletions

View File

@ -1,4 +1,5 @@
node_modules
build
*.typegen.ts
i18-generate-hash.js
i18-generate-hash.js
src/parser/TraceOperatorParser/**

View File

@ -10,4 +10,6 @@ public/
**/*.json
# Ignore all files in parser folder:
src/parser/**
src/parser/**
src/TraceOperator/parser/**

View File

@ -45,6 +45,7 @@
"@sentry/webpack-plugin": "2.22.6",
"@signozhq/badge": "0.0.2",
"@signozhq/calendar": "0.0.0",
"@signozhq/callout": "0.0.2",
"@signozhq/design-tokens": "1.1.4",
"@signozhq/input": "0.0.2",
"@signozhq/popover": "0.0.0",

View File

@ -92,6 +92,7 @@ describe('prepareQueryRangePayloadV5', () => {
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [baseFormula()],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
@ -215,7 +216,7 @@ describe('prepareQueryRangePayloadV5', () => {
},
],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [] },
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
originalGraphType: PANEL_TYPES.TABLE,
@ -286,7 +287,7 @@ describe('prepareQueryRangePayloadV5', () => {
legend: 'LC',
},
],
builder: { queryData: [], queryFormulas: [] },
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TABLE,
selectedTime: 'GLOBAL_TIME',
@ -345,7 +346,7 @@ describe('prepareQueryRangePayloadV5', () => {
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: { queryData: [], queryFormulas: [] },
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
@ -386,6 +387,7 @@ describe('prepareQueryRangePayloadV5', () => {
builder: {
queryData: [baseBuilderQuery()],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TABLE,
@ -459,6 +461,7 @@ describe('prepareQueryRangePayloadV5', () => {
builder: {
queryData: [logsQuery],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.LIST,
@ -572,6 +575,7 @@ describe('prepareQueryRangePayloadV5', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,

View File

@ -1,11 +1,15 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
FieldContext,
@ -332,6 +336,109 @@ export function convertBuilderQueriesToV5(
);
}
function createTraceOperatorBaseSpec(
queryData: IBuilderTraceOperator,
requestType: RequestType,
panelType?: PANEL_TYPES,
): BaseBuilderQuery {
const nonEmptySelectColumns = (queryData.selectColumns as (
| BaseAutocompleteData
| TelemetryFieldKey
)[])?.filter((c) => ('key' in c ? c?.key : c?.name));
const {
stepInterval,
groupBy,
limit,
offset,
legend,
having,
orderBy,
pageSize,
} = queryData;
return {
stepInterval: stepInterval || undefined,
groupBy:
groupBy?.length > 0
? groupBy.map(
(item: any): GroupByKey => ({
name: item.key,
fieldDataType: item?.dataType,
fieldContext: item?.type,
description: item?.description,
unit: item?.unit,
signal: item?.signal,
materialized: item?.materialized,
}),
)
: undefined,
limit:
panelType === PANEL_TYPES.TABLE || panelType === PANEL_TYPES.LIST
? limit || pageSize || undefined
: limit || undefined,
offset: requestType === 'raw' || requestType === 'trace' ? offset : undefined,
order:
orderBy?.length > 0
? orderBy.map(
(order: any): OrderBy => ({
key: {
name: order.columnName,
},
direction: order.order,
}),
)
: undefined,
legend: isEmpty(legend) ? undefined : legend,
having: isEmpty(having) ? undefined : (having as Having),
selectFields: isEmpty(nonEmptySelectColumns)
? undefined
: nonEmptySelectColumns?.map(
(column: any): TelemetryFieldKey => ({
name: column.name ?? column.key,
fieldDataType:
column?.fieldDataType ?? (column?.dataType as FieldDataType),
fieldContext: column?.fieldContext ?? (column?.type as FieldContext),
signal: column?.signal ?? undefined,
}),
),
};
}
export function convertTraceOperatorToV5(
traceOperator: Record<string, IBuilderTraceOperator>,
requestType: RequestType,
panelType?: PANEL_TYPES,
): QueryEnvelope[] {
return Object.entries(traceOperator).map(
([queryName, traceOperatorData]): QueryEnvelope => {
const baseSpec = createTraceOperatorBaseSpec(
traceOperatorData,
requestType,
panelType,
);
// Skip aggregation for raw request type
const aggregations =
requestType === 'raw'
? undefined
: createAggregation(traceOperatorData, panelType);
const spec: QueryEnvelope['spec'] = {
name: queryName,
...baseSpec,
expression: traceOperatorData.expression || '',
aggregations: aggregations as TraceAggregation[],
};
return {
type: 'builder_trace_operator' as QueryType,
spec,
};
},
);
}
/**
* Converts PromQL queries to V5 format
*/
@ -413,14 +520,28 @@ export const prepareQueryRangePayloadV5 = ({
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas } = query.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const filteredTraceOperator =
queryTraceOperator && queryTraceOperator.length > 0
? queryTraceOperator.filter((traceOperator) =>
Boolean(traceOperator.expression.trim()),
)
: [];
const currentTraceOperator = mapQueryDataToApi(
filteredTraceOperator,
'queryName',
tableParams,
);
// Combine legend maps
legendMap = {
...currentQueryData.newLegendMap,
...currentFormulas.newLegendMap,
...currentTraceOperator.newLegendMap,
};
// Convert builder queries
@ -453,8 +574,14 @@ export const prepareQueryRangePayloadV5 = ({
}),
);
// Combine both types
queries = [...builderQueries, ...formulaQueries];
const traceOperatorQueries = convertTraceOperatorToV5(
currentTraceOperator.data,
requestType,
graphType,
);
// Combine all query types
queries = [...builderQueries, ...formulaQueries, ...traceOperatorQueries];
break;
}
case EQueryType.PROM: {

View File

@ -125,6 +125,7 @@ export const getHostTracesQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,

View File

@ -51,6 +51,7 @@ export const getHostLogsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,

View File

@ -22,6 +22,10 @@
flex: 1;
position: relative;
.qb-trace-view-selector-container {
padding: 12px 8px 8px 8px;
}
}
.qb-content-section {
@ -179,7 +183,7 @@
flex-direction: column;
gap: 8px;
margin-left: 32px;
margin-left: 26px;
padding-bottom: 16px;
padding-left: 8px;
@ -195,8 +199,8 @@
}
.formula-container {
margin-left: 82px;
padding: 4px 0px;
padding: 8px;
margin-left: 74px;
.ant-col {
&::before {
@ -291,6 +295,13 @@
);
}
}
.qb-trace-operator-button-container {
&-text {
display: flex;
align-items: center;
gap: 8px;
}
}
}
}
@ -331,6 +342,12 @@
);
left: 15px;
}
&.has-trace-operator {
&::before {
height: 0px;
}
}
}
.formula-name {
@ -347,7 +364,7 @@
&::before {
content: '';
height: 65px;
height: 128px;
content: '';
position: absolute;
left: 0;
@ -387,6 +404,7 @@
}
.qb-search-filter-container {
flex: 1;
display: flex;
flex-direction: row;
align-items: flex-start;

View File

@ -5,11 +5,13 @@ import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { memo, useEffect, useMemo, useRef } from 'react';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2Provider } from './QueryBuilderV2Context';
import QueryFooter from './QueryV2/QueryFooter/QueryFooter';
import { QueryV2 } from './QueryV2/QueryV2';
import TraceOperator from './QueryV2/TraceOperator/TraceOperator';
export const QueryBuilderV2 = memo(function QueryBuilderV2({
config,
@ -18,6 +20,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryComponents,
isListViewPanel = false,
showOnlyWhereClause = false,
showTraceOperator = false,
version,
}: QueryBuilderProps): JSX.Element {
const {
@ -25,6 +28,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
addNewBuilderQuery,
addNewFormula,
handleSetConfig,
addTraceOperator,
panelType,
initialDataSource,
} = useQueryBuilder();
@ -54,6 +58,11 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
newPanelType,
]);
const isMultiQueryAllowed = useMemo(
() => !isListViewPanel || showTraceOperator,
[showTraceOperator, isListViewPanel],
);
const listViewLogFilterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
const config: QueryBuilderProps['filterConfigs'] = {
stepInterval: { isHidden: true, isDisabled: true },
@ -97,11 +106,60 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
listViewTracesFilterConfigs,
]);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const hasAtLeastOneTraceQuery = useMemo(
() =>
currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.TRACES,
),
[currentQuery.builder.queryData],
);
const hasTraceOperator = useMemo(
() => showTraceOperator && hasAtLeastOneTraceQuery && Boolean(traceOperator),
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
const shouldShowFooter = useMemo(
() =>
(!showOnlyWhereClause && !isListViewPanel) ||
(currentDataSource === DataSource.TRACES && showTraceOperator),
[isListViewPanel, showTraceOperator, showOnlyWhereClause, currentDataSource],
);
const showQueryList = useMemo(
() => (!showOnlyWhereClause && !isListViewPanel) || showTraceOperator,
[isListViewPanel, showOnlyWhereClause, showTraceOperator],
);
const showFormula = useMemo(() => {
if (currentDataSource === DataSource.TRACES) {
return !isListViewPanel;
}
return true;
}, [isListViewPanel, currentDataSource]);
const showAddTraceOperator = useMemo(
() => showTraceOperator && !traceOperator && hasAtLeastOneTraceQuery,
[showTraceOperator, traceOperator, hasAtLeastOneTraceQuery],
);
return (
<QueryBuilderV2Provider>
<div className="query-builder-v2">
<div className="qb-content-container">
{isListViewPanel && (
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={currentQuery.builder.queryData[0].queryName}
@ -109,15 +167,16 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
query={currentQuery.builder.queryData[0]}
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
isMultiQueryAllowed={isMultiQueryAllowed}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
version={version}
isAvailableToDisable={false}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
/>
)}
{!isListViewPanel &&
) : (
currentQuery.builder.queryData.map((query, index) => (
<QueryV2
ref={containerRef}
@ -127,13 +186,17 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
filterConfigs={queryFilterConfigs}
queryComponents={queryComponents}
version={version}
isMultiQueryAllowed={isMultiQueryAllowed}
isAvailableToDisable={false}
showTraceOperator={showTraceOperator}
hasTraceOperator={hasTraceOperator}
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isListViewPanel={isListViewPanel}
signalSource={config?.signalSource || ''}
/>
))}
))
)}
{!showOnlyWhereClause && currentQuery.builder.queryFormulas.length > 0 && (
<div className="qb-formulas-container">
@ -158,15 +221,25 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
)}
{!showOnlyWhereClause && !isListViewPanel && (
{shouldShowFooter && (
<QueryFooter
showAddFormula={showFormula}
addNewBuilderQuery={addNewBuilderQuery}
addNewFormula={addNewFormula}
addTraceOperator={addTraceOperator}
showAddTraceOperator={showAddTraceOperator}
/>
)}
{hasTraceOperator && (
<TraceOperator
isListViewPanel={isListViewPanel}
traceOperator={traceOperator as IBuilderTraceOperator}
/>
)}
</div>
{!showOnlyWhereClause && !isListViewPanel && (
{showQueryList && (
<div className="query-names-section">
{currentQuery.builder.queryData.map((query) => (
<div key={query.queryName} className="query-name">

View File

@ -1,7 +1,11 @@
.query-add-ons {
width: 100%;
}
.add-ons-list {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
.add-ons-tabs {
display: flex;

View File

@ -144,6 +144,7 @@ function QueryAddOns({
showReduceTo,
panelType,
index,
isForTraceOperator = false,
}: {
query: IBuilderQuery;
version: string;
@ -151,6 +152,7 @@ function QueryAddOns({
showReduceTo: boolean;
panelType: PANEL_TYPES | null;
index: number;
isForTraceOperator?: boolean;
}): JSX.Element {
const [addOns, setAddOns] = useState<AddOn[]>(ADD_ONS);
@ -160,6 +162,7 @@ function QueryAddOns({
index,
query,
entityVersion: '',
isForTraceOperator,
});
const { handleSetQueryData } = useQueryBuilder();

View File

@ -4,7 +4,10 @@ import { Tooltip } from 'antd';
import InputWithLabel from 'components/InputWithLabel/InputWithLabel';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useMemo } from 'react';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAggregationSelect from './QueryAggregationSelect';
@ -20,7 +23,7 @@ function QueryAggregationOptions({
panelType?: string;
onAggregationIntervalChange: (value: number) => void;
onChange?: (value: string) => void;
queryData: IBuilderQuery;
queryData: IBuilderQuery | IBuilderTraceOperator;
}): JSX.Element {
const showAggregationInterval = useMemo(() => {
// eslint-disable-next-line sonarjs/prefer-single-boolean-return

View File

@ -1,12 +1,20 @@
/* eslint-disable react/require-default-props */
import { Button, Tooltip, Typography } from 'antd';
import { Plus, Sigma } from 'lucide-react';
import { DraftingCompass, Plus, Sigma } from 'lucide-react';
import BetaTag from 'periscope/components/BetaTag/BetaTag';
export default function QueryFooter({
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
showAddFormula = true,
showAddTraceOperator = false,
}: {
addNewBuilderQuery: () => void;
addNewFormula: () => void;
addTraceOperator?: () => void;
showAddTraceOperator: boolean;
showAddFormula?: boolean;
}): JSX.Element {
return (
<div className="qb-footer">
@ -22,32 +30,65 @@ export default function QueryFooter({
</Tooltip>
</div>
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
{showAddFormula && (
<div className="qb-add-formula">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add New Formula
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-advanced-comparisons"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
Add Formula
</Button>
</Tooltip>
</div>
<Button
className="add-formula-button periscope-btn secondary"
icon={<Sigma size={16} />}
onClick={addNewFormula}
>
Add Formula
</Button>
</Tooltip>
</div>
)}
{showAddTraceOperator && (
<div className="qb-trace-operator-button-container">
<Tooltip
title={
<div style={{ textAlign: 'center' }}>
Add Trace Matching
<Typography.Link
href="https://signoz.io/docs/userguide/query-builder-v5/#multi-query-analysis-trace-operators"
target="_blank"
style={{ textDecoration: 'underline' }}
>
{' '}
<br />
Learn more
</Typography.Link>
</div>
}
>
<Button
className="add-trace-operator-button periscope-btn secondary"
icon={<DraftingCompass size={16} />}
onClick={(): void => addTraceOperator?.()}
>
<div className="qb-trace-operator-button-container-text">
Add Trace Matching
<BetaTag />
</div>
</Button>
</Tooltip>
</div>
)}
</div>
</div>
);

View File

@ -7,6 +7,7 @@
'Helvetica Neue', sans-serif;
.query-where-clause-editor-container {
position: relative;
display: flex;
flex-direction: row;

View File

@ -1,3 +1,4 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { Dropdown } from 'antd';
import cx from 'classnames';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
@ -26,9 +27,12 @@ export const QueryV2 = memo(function QueryV2({
query,
filterConfigs,
isListViewPanel = false,
showTraceOperator = false,
hasTraceOperator = false,
version,
showOnlyWhereClause = false,
signalSource = '',
isMultiQueryAllowed = false,
}: QueryProps & { ref: React.RefObject<HTMLDivElement> }): JSX.Element {
const { cloneQuery, panelType } = useQueryBuilder();
@ -75,6 +79,15 @@ export const QueryV2 = memo(function QueryV2({
dataSource,
]);
const showInlineQuerySearch = useMemo(() => {
if (!showTraceOperator) {
return false;
}
return (
dataSource === DataSource.TRACES && (hasTraceOperator || isListViewPanel)
);
}, [hasTraceOperator, isListViewPanel, showTraceOperator, dataSource]);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
@ -108,11 +121,12 @@ export const QueryV2 = memo(function QueryV2({
ref={ref}
>
<div className="qb-content-section">
{!showOnlyWhereClause && (
{(!showOnlyWhereClause || showTraceOperator) && (
<div className="qb-header-container">
<div className="query-actions-container">
<div className="query-actions-left-container">
<QBEntityOptions
hasTraceOperator={hasTraceOperator}
isMetricsDataSource={dataSource === DataSource.METRICS}
showFunctions={
(version && version === ENTITY_VERSION_V4) ||
@ -122,6 +136,7 @@ export const QueryV2 = memo(function QueryV2({
false
}
isCollapsed={isCollapsed}
showTraceOperator={showTraceOperator}
entityType="query"
entityData={query}
onToggleVisibility={handleToggleDisableQuery}
@ -139,7 +154,28 @@ export const QueryV2 = memo(function QueryV2({
/>
</div>
{!isListViewPanel && (
{!isCollapsed && showInlineQuerySearch && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
{isMultiQueryAllowed && (
<Dropdown
className="query-actions-dropdown"
menu={{
@ -181,28 +217,31 @@ export const QueryV2 = memo(function QueryV2({
</div>
)}
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
{!showInlineQuerySearch && (
<div className="qb-search-filter-container">
<div className="query-search-container">
<QuerySearch
key={`query-search-${query.queryName}-${query.dataSource}`}
onChange={handleSearchChange}
queryData={query}
dataSource={dataSource}
signalSource={signalSource}
/>
</div>
)}
</div>
{showSpanScopeSelector && (
<div className="traces-search-filter-container">
<div className="traces-search-filter-in">in</div>
<SpanScopeSelector query={query} />
</div>
)}
</div>
)}
</div>
{!showOnlyWhereClause &&
!isListViewPanel &&
!(hasTraceOperator && dataSource === DataSource.TRACES) &&
dataSource !== DataSource.METRICS && (
<QueryAggregation
dataSource={dataSource}
@ -225,16 +264,17 @@ export const QueryV2 = memo(function QueryV2({
/>
)}
{!showOnlyWhereClause && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
{!showOnlyWhereClause &&
!(hasTraceOperator && query.dataSource === DataSource.TRACES) && (
<QueryAddOns
index={index}
query={query}
version="v3"
isListViewPanel={isListViewPanel}
showReduceTo={showReduceTo}
panelType={panelType}
/>
)}
</div>
)}
</div>

View File

@ -0,0 +1,159 @@
.qb-trace-operator {
padding: 8px;
display: flex;
gap: 8px;
&.non-list-view {
padding-left: 40px;
position: relative;
&::before {
content: '';
position: absolute;
top: 24px;
left: 12px;
height: 88px;
width: 1px;
background: repeating-linear-gradient(
to bottom,
var(--bg-slate-400),
var(--bg-slate-400) 4px,
transparent 4px,
transparent 8px
);
}
}
&-arrow {
position: relative;
&::before {
content: '';
position: absolute;
top: 16px;
transform: translateY(-50%);
left: -26px;
height: 1px;
width: 20px;
background: repeating-linear-gradient(
to right,
var(--bg-slate-400),
var(--bg-slate-400) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
content: '';
position: absolute;
top: 16px;
left: -10px;
transform: translateY(-50%);
height: 4px;
width: 4px;
border-radius: 50%;
background-color: var(--bg-slate-400);
}
}
&-input {
width: 100%;
}
&-container {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
}
&-aggregation-container {
display: flex;
flex-direction: column;
gap: 8px;
}
&-add-ons-container {
width: 100%;
display: flex;
flex-direction: row;
gap: 16px;
}
&-label-with-input {
position: relative;
display: flex;
align-items: center;
flex-direction: row;
border-radius: 2px;
border: 1px solid var(--bg-slate-400);
background: var(--bg-ink-300);
.qb-trace-operator-editor-container {
flex: 1;
}
&.arrow-left {
&::before {
content: '';
position: absolute;
left: -16px;
top: 50%;
height: 1px;
width: 16px;
background-color: var(--bg-slate-400);
}
}
.label {
color: var(--bg-vanilla-400);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
padding: 0px 8px;
border-right: 1px solid var(--bg-slate-400);
}
}
}
.lightMode {
.qb-trace-operator {
&-arrow {
&::before {
background: repeating-linear-gradient(
to right,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
&::after {
background-color: var(--bg-vanilla-300);
}
}
&.non-list-view {
&::before {
background: repeating-linear-gradient(
to bottom,
var(--bg-vanilla-300),
var(--bg-vanilla-300) 4px,
transparent 4px,
transparent 8px
);
}
}
&-label-with-input {
border: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
.label {
color: var(--bg-ink-500) !important;
border-right: 1px solid var(--bg-vanilla-300) !important;
background: var(--bg-vanilla-100) !important;
}
}
}
}

View File

@ -0,0 +1,119 @@
/* eslint-disable react/require-default-props */
/* eslint-disable sonarjs/no-duplicate-string */
import './TraceOperator.styles.scss';
import { Button, Tooltip, Typography } from 'antd';
import cx from 'classnames';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Trash2 } from 'lucide-react';
import { useCallback } from 'react';
import {
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import QueryAddOns from '../QueryAddOns/QueryAddOns';
import QueryAggregation from '../QueryAggregation/QueryAggregation';
import TraceOperatorEditor from './TraceOperatorEditor';
export default function TraceOperator({
traceOperator,
isListViewPanel = false,
}: {
traceOperator: IBuilderTraceOperator;
isListViewPanel?: boolean;
}): JSX.Element {
const { panelType, removeTraceOperator } = useQueryBuilder();
const { handleChangeQueryData } = useQueryOperations({
index: 0,
query: traceOperator,
entityVersion: '',
isForTraceOperator: true,
});
const handleTraceOperatorChange = useCallback(
(traceOperatorExpression: string) => {
handleChangeQueryData('expression', traceOperatorExpression);
},
[handleChangeQueryData],
);
const handleChangeAggregateEvery = useCallback(
(value: IBuilderQuery['stepInterval']) => {
handleChangeQueryData('stepInterval', value);
},
[handleChangeQueryData],
);
const handleChangeAggregation = useCallback(
(value: string) => {
handleChangeQueryData('aggregations', [
{
expression: value,
},
]);
},
[handleChangeQueryData],
);
return (
<div className={cx('qb-trace-operator', !isListViewPanel && 'non-list-view')}>
<div className="qb-trace-operator-container">
<div
className={cx(
'qb-trace-operator-label-with-input',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<Typography.Text className="label">TRACE OPERATOR</Typography.Text>
<div className="qb-trace-operator-editor-container">
<TraceOperatorEditor
value={traceOperator?.expression || ''}
traceOperator={traceOperator}
onChange={handleTraceOperatorChange}
/>
</div>
</div>
{!isListViewPanel && (
<div className="qb-trace-operator-aggregation-container">
<div className={cx(!isListViewPanel && 'qb-trace-operator-arrow')}>
<QueryAggregation
dataSource={DataSource.TRACES}
key={`query-search-${traceOperator.queryName}`}
panelType={panelType || undefined}
onAggregationIntervalChange={handleChangeAggregateEvery}
onChange={handleChangeAggregation}
queryData={traceOperator}
/>
</div>
<div
className={cx(
'qb-trace-operator-add-ons-container',
!isListViewPanel && 'qb-trace-operator-arrow',
)}
>
<QueryAddOns
index={0}
query={traceOperator}
version="v3"
isForTraceOperator
isListViewPanel={false}
showReduceTo={false}
panelType={panelType}
/>
</div>
</div>
)}
</div>
<Tooltip title="Remove Trace Operator" placement="topLeft">
<Button className="periscope-btn ghost" onClick={removeTraceOperator}>
<Trash2 size={14} />
</Button>
</Tooltip>
</div>
);
}

View File

@ -0,0 +1,491 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable sonarjs/no-identical-functions */
import '../QuerySearch/QuerySearch.styles.scss';
import { CheckCircleFilled } from '@ant-design/icons';
import {
autocompletion,
closeCompletion,
CompletionContext,
completionKeymap,
CompletionResult,
startCompletion,
} from '@codemirror/autocomplete';
import { javascript } from '@codemirror/lang-javascript';
import { Color } from '@signozhq/design-tokens';
import { copilot } from '@uiw/codemirror-theme-copilot';
import { githubLight } from '@uiw/codemirror-theme-github';
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
import { Button, Popover } from 'antd';
import cx from 'classnames';
import {
TRACE_OPERATOR_OPERATORS,
TRACE_OPERATOR_OPERATORS_LABELS,
TRACE_OPERATOR_OPERATORS_WITH_PRIORITY,
} from 'constants/antlrQueryConstants';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useIsDarkMode } from 'hooks/useDarkMode';
import { TriangleAlert } from 'lucide-react';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { validateTraceOperatorQuery } from 'utils/queryValidationUtils';
import { getTraceOperatorContextAtCursor } from './utils/traceOperatorContextUtils';
import { getInvolvedQueriesInTraceOperator } from './utils/utils';
// Custom extension to stop events
const stopEventsExtension = EditorView.domEventHandlers({
keydown: (event) => {
// Stop all keyboard events from propagating to global shortcuts
event.stopPropagation();
event.stopImmediatePropagation();
return false; // Important for CM to know you handled it
},
input: (event) => {
event.stopPropagation();
return false;
},
focus: (event) => {
// Ensure focus events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
blur: (event) => {
// Ensure blur events don't interfere with global shortcuts
event.stopPropagation();
return false;
},
});
interface TraceOperatorEditorProps {
value: string;
traceOperator: IBuilderTraceOperator;
onChange: (value: string) => void;
placeholder?: string;
onRun?: (query: string) => void;
}
function TraceOperatorEditor({
value,
onChange,
traceOperator,
placeholder = 'Enter your trace operator query',
onRun,
}: TraceOperatorEditorProps): JSX.Element {
const isDarkMode = useIsDarkMode();
const [isFocused, setIsFocused] = useState(false);
const [cursorPos, setCursorPos] = useState({ line: 0, ch: 0 });
const editorRef = useRef<EditorView | null>(null);
const [validation, setValidation] = useState<IValidationResult>({
isValid: false,
message: '',
errors: [],
});
// Track if the query was changed externally (from props) vs internally (user input)
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery, handleRunQuery } = useQueryBuilder();
const queryOptions = useMemo(
() =>
currentQuery.builder.queryData
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
.map((query) => ({
label: query.queryName,
type: 'atom',
apply: query.queryName,
})),
[currentQuery.builder.queryData],
);
const toggleSuggestions = useCallback(
(timeout?: number) => {
const timeoutId = setTimeout(() => {
if (!editorRef.current) return;
if (isFocused) {
startCompletion(editorRef.current);
} else {
closeCompletion(editorRef.current);
}
}, timeout);
return (): void => clearTimeout(timeoutId);
},
[isFocused],
);
const handleQueryValidation = (newQuery: string): void => {
try {
const validationResponse = validateTraceOperatorQuery(newQuery);
setValidation(validationResponse);
} catch (error) {
setValidation({
isValid: false,
message: 'Failed to process trace operator',
errors: [error as IDetailedError],
});
}
};
// Detect external value changes and mark for validation
useEffect(() => {
const newValue = value || '';
if (newValue !== lastExternalValue) {
setIsExternalQueryChange(true);
setLastExternalValue(newValue);
}
}, [value, lastExternalValue]);
// Validate when the value changes externally (including on mount)
useEffect(() => {
if (isExternalQueryChange && value) {
handleQueryValidation(value);
setIsExternalQueryChange(false);
}
}, [isExternalQueryChange, value]);
// Enhanced autosuggestion function with context awareness
function autoSuggestions(context: CompletionContext): CompletionResult | null {
// This matches words before the cursor position
// eslint-disable-next-line no-useless-escape
const word = context.matchBefore(/[a-zA-Z0-9_.:/?&=#%\-\[\]]*/);
if (word?.from === word?.to && !context.explicit) return null;
// Get the trace operator context at the cursor position
const queryContext = getTraceOperatorContextAtCursor(value, cursorPos.ch);
// Define autocomplete options based on the context
let options: {
label: string;
type: string;
info?: string;
apply:
| string
| ((view: EditorView, completion: any, from: number, to: number) => void);
detail?: string;
boost?: number;
}[] = [];
// Helper function to add space after selection
const addSpaceAfterSelection = (
view: EditorView,
completion: any,
from: number,
to: number,
shouldAddSpace = true,
): void => {
view.dispatch({
changes: {
from,
to,
insert: shouldAddSpace ? `${completion.apply} ` : `${completion.apply}`,
},
selection: {
anchor:
from +
(shouldAddSpace ? completion.apply.length + 1 : completion.apply.length),
},
});
// Do not reopen here; onUpdate will handle reopening via toggleSuggestions
};
// Helper function to add space after selection to options
const addSpaceToOptions = (opts: typeof options): typeof options =>
opts.map((option) => {
const originalApply = option.apply || option.label;
return {
...option,
apply: (
view: EditorView,
completion: any,
from: number,
to: number,
): void => {
addSpaceAfterSelection(view, { apply: originalApply }, from, to);
},
};
});
if (queryContext.isInAtom) {
// Suggest atoms (identifiers) for trace operators
const involvedQueries = getInvolvedQueriesInTraceOperator([traceOperator]);
options = queryOptions.map((option) => ({
...option,
boost: !involvedQueries.includes(option.apply as string) ? 100 : -99,
}));
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection for atoms
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInOperator) {
// Suggest operators for trace operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for operators
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? cursorPos.ch,
options: optionsWithSpace,
};
}
if (queryContext.isInParenthesis) {
// Different suggestions based on the context within parenthesis
const curChar = value.charAt(cursorPos.ch - 1) || '';
if (curChar === '(') {
// Right after opening parenthesis, suggest atoms or nested expressions
options = [
{ label: '(', type: 'parenthesis', apply: '(' },
...queryOptions,
];
// Add space after selection for opening parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
if (curChar === ')') {
// After closing parenthesis, suggest operators
const operators = Object.values(TRACE_OPERATOR_OPERATORS);
options = operators.map((operator) => ({
label: TRACE_OPERATOR_OPERATORS_LABELS[operator]
? `${operator} (${TRACE_OPERATOR_OPERATORS_LABELS[operator]})`
: operator,
type: 'operator',
apply: operator,
boost: TRACE_OPERATOR_OPERATORS_WITH_PRIORITY[operator] * -10,
}));
// Add space after selection for closing parenthesis context
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
options: optionsWithSpace,
};
}
}
// Default: suggest atoms if no specific context
options = [
...queryOptions,
{
label: '(',
type: 'parenthesis',
apply: '(',
},
];
// Filter options based on what user is typing
const searchText = word?.text.toLowerCase().trim() ?? '';
options = options.filter((option) =>
option.label.toLowerCase().includes(searchText),
);
// Add space after selection
const optionsWithSpace = addSpaceToOptions(options);
return {
from: word?.from ?? 0,
to: word?.to ?? context.pos,
options: optionsWithSpace,
};
}
const handleUpdate = useCallback(
(viewUpdate: { view: EditorView }): void => {
if (!editorRef.current) {
editorRef.current = viewUpdate.view;
}
const selection = viewUpdate.view.state.selection.main;
const pos = selection.head;
const lineInfo = viewUpdate.view.state.doc.lineAt(pos);
const newPos = {
line: lineInfo.number,
ch: pos - lineInfo.from,
};
if (newPos.line !== cursorPos.line || newPos.ch !== cursorPos.ch) {
setCursorPos(newPos);
// Trigger suggestions on context update
toggleSuggestions(10);
}
},
[cursorPos, toggleSuggestions],
);
const handleChange = (newValue: string): void => {
// Mark as internal change to avoid triggering external validation
setIsExternalQueryChange(false);
setLastExternalValue(newValue);
onChange(newValue);
};
const handleBlur = (): void => {
handleQueryValidation(value);
setIsFocused(false);
};
// Effect to handle focus state and trigger suggestions on focus
useEffect(() => {
const clearTimeout = toggleSuggestions(10);
return (): void => clearTimeout();
}, [isFocused, toggleSuggestions]);
return (
<div className="code-mirror-where-clause">
<div className="query-where-clause-editor-container">
<CodeMirror
value={value}
theme={isDarkMode ? copilot : githubLight}
onChange={handleChange}
onUpdate={handleUpdate}
className={cx('query-where-clause-editor', {
isValid: validation.isValid === true,
hasErrors: validation.errors.length > 0,
})}
extensions={[
autocompletion({
override: [autoSuggestions],
defaultKeymap: true,
closeOnBlur: true,
activateOnTyping: true,
maxRenderedOptions: 50,
}),
javascript({ jsx: false, typescript: false }),
EditorView.lineWrapping,
stopEventsExtension,
Prec.highest(
keymap.of([
...completionKeymap,
{
key: 'Escape',
run: closeCompletion,
},
{
key: 'Enter',
preventDefault: true,
// Prevent default behavior of Enter to add new line
// and instead run a custom action
run: (): boolean => true,
},
{
key: 'Mod-Enter',
preventDefault: true,
run: (): boolean => {
if (onRun && typeof onRun === 'function') {
onRun(value);
} else {
handleRunQuery();
}
return true;
},
},
{
key: 'Shift-Enter',
preventDefault: true,
// Prevent default behavior of Shift-Enter to add new line
run: (): boolean => true,
},
]),
),
]}
placeholder={placeholder}
basicSetup={{
lineNumbers: false,
}}
onFocus={(): void => {
setIsFocused(true);
}}
onBlur={handleBlur}
/>
{value && validation.isValid === false && !isFocused && (
<div
className={cx('query-status-container', {
hasErrors: validation.errors.length > 0,
})}
>
<Popover
placement="bottomRight"
showArrow={false}
content={
<div className="query-status-content">
<div className="query-status-content-header">
<div className="query-validation">
<div className="query-validation-errors">
{validation.errors.map((error) => (
<div key={error.message} className="query-validation-error">
<div className="query-validation-error">
{error.line}:{error.column} - {error.message}
</div>
</div>
))}
</div>
</div>
</div>
</div>
}
overlayClassName="query-status-popover"
>
{validation.isValid ? (
<Button
type="text"
icon={<CheckCircleFilled />}
className="periscope-btn ghost"
/>
) : (
<Button
type="text"
icon={<TriangleAlert size={14} color={Color.BG_CHERRY_500} />}
className="periscope-btn ghost"
/>
)}
</Popover>
</div>
)}
</div>
</div>
);
}
TraceOperatorEditor.defaultProps = {
onRun: undefined,
placeholder: 'Enter your trace operator query',
};
export default TraceOperatorEditor;

View File

@ -0,0 +1,425 @@
/* eslint-disable sonarjs/no-duplicate-string */
/* eslint-disable sonarjs/cognitive-complexity */
import { Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import {
createTraceOperatorContext,
extractTraceExpressionPairs,
getTraceOperatorContextAtCursor,
} from '../utils/traceOperatorContextUtils';
describe('traceOperatorContextUtils', () => {
describe('createTraceOperatorContext', () => {
it('should create a context object with all required properties', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
true,
false,
false,
false,
'atom',
'operator',
[],
null,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
atomToken: 'atom',
operatorToken: 'operator',
expressionPairs: [],
currentPair: null,
});
});
it('should create a context object with default values', () => {
const mockToken = {
type: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
} as Token;
const context = createTraceOperatorContext(
mockToken,
false,
true,
false,
false,
);
expect(context).toEqual({
tokenType: TraceOperatorGrammarLexer.IDENTIFIER,
text: 'test',
start: 0,
stop: 3,
currentToken: 'test',
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: undefined,
operatorToken: undefined,
expressionPairs: [],
currentPair: undefined,
});
});
});
describe('extractTraceExpressionPairs', () => {
it('should extract simple expression pair', () => {
const query = 'A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].position.leftStart).toBe(0);
expect(result[0].position.leftEnd).toBe(0);
expect(result[0].operator).toBe('=>');
expect(result[0].position.operatorStart).toBe(2);
expect(result[0].position.operatorEnd).toBe(3);
expect(result[0].rightAtom).toBe('B');
expect(result[0].position.rightStart).toBe(5);
expect(result[0].position.rightEnd).toBe(5);
expect(result[0].isComplete).toBe(true);
});
it('should extract multiple expression pairs', () => {
const query = 'A => B && C => D';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
// First pair: A => B
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
// Second pair: C => D
expect(result[1].leftAtom).toBe('C');
expect(result[1].operator).toBe('=>');
expect(result[1].rightAtom).toBe('D');
});
it('should handle NOT operator', () => {
const query = 'NOT A => B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle parentheses', () => {
const query = '(A => B) && (C => D)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(2);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
});
it('should handle incomplete expressions', () => {
const query = 'A =>';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBeUndefined();
expect(result[0].isComplete).toBe(true);
});
it('should handle complex nested expressions', () => {
const query = 'A => B && (C => D || E => F)';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(3);
expect(result[0].leftAtom).toBe('A');
expect(result[0].rightAtom).toBe('B');
expect(result[1].leftAtom).toBe('C');
expect(result[1].rightAtom).toBe('D');
expect(result[2].leftAtom).toBe('E');
expect(result[2].rightAtom).toBe('F');
});
it('should handle whitespace variations', () => {
const query = 'A=>B';
const result = extractTraceExpressionPairs(query);
expect(result).toHaveLength(1);
expect(result[0].leftAtom).toBe('A');
expect(result[0].operator).toBe('=>');
expect(result[0].rightAtom).toBe('B');
});
it('should handle error cases gracefully', () => {
const query = 'invalid syntax @#$%';
const result = extractTraceExpressionPairs(query);
// Should return an array (even if empty or with partial results)
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThanOrEqual(0);
});
});
describe('getTraceOperatorContextAtCursor', () => {
beforeEach(() => {
// Reset console.error mock
jest.spyOn(console, 'error').mockImplementation(() => {});
});
afterEach(() => {
jest.restoreAllMocks();
});
it('should return default context for empty query', () => {
const result = getTraceOperatorContextAtCursor('', 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for null query', () => {
const result = getTraceOperatorContextAtCursor(null as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should return default context for undefined query', () => {
const result = getTraceOperatorContextAtCursor(undefined as any, 0);
expect(result).toEqual({
tokenType: -1,
text: '',
start: 0,
stop: 0,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
});
});
it('should identify atom context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should identify operator context', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 2); // cursor at '='
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(2);
expect(result.stop).toBe(2);
});
it('should identify parenthesis context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at '('
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle cursor at space', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at space
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
});
it('should handle cursor at end of query', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at end
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(5);
});
it('should handle complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 8); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(7);
expect(result.stop).toBe(8);
});
it('should identify operator position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 10); // cursor at 'C'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(10);
expect(result.stop).toBe(10);
});
it('should identify atom position in complex query', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 13); // cursor at '>'
expect(result.atomToken).toBe('C');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(12);
expect(result.stop).toBe(13);
});
it('should handle transition points', () => {
const query = 'A => B';
const result = getTraceOperatorContextAtCursor(query, 4); // cursor at 'B'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(4);
expect(result.stop).toBe(4);
});
it('should handle whitespace in complex queries', () => {
const query = 'A=>B && C=>D';
const result = getTraceOperatorContextAtCursor(query, 6); // cursor at '&'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBe('&&');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(true);
expect(result.isInParenthesis).toBe(false);
expect(result.start).toBe(5);
expect(result.stop).toBe(6);
});
it('should handle NOT operator context', () => {
const query = 'NOT A => B';
const result = getTraceOperatorContextAtCursor(query, 0); // cursor at 'N'
expect(result.atomToken).toBeUndefined();
expect(result.operatorToken).toBeUndefined();
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
});
it('should handle parentheses context', () => {
const query = '(A => B)';
const result = getTraceOperatorContextAtCursor(query, 1); // cursor at 'A'
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(false);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(true);
expect(result.start).toBe(0);
expect(result.stop).toBe(0);
});
it('should handle expression pairs context', () => {
const query = 'A => B && C => D';
const result = getTraceOperatorContextAtCursor(query, 5); // cursor at 'A' in "&&"
expect(result.atomToken).toBe('A');
expect(result.operatorToken).toBe('=>');
expect(result.isInAtom).toBe(true);
expect(result.isInOperator).toBe(false);
expect(result.isInParenthesis).toBe(false);
});
it('should handle various cursor positions', () => {
const query = 'A => B';
// Test cursor at each position
for (let i = 0; i < query.length; i++) {
const result = getTraceOperatorContextAtCursor(query, i);
expect(result).toBeDefined();
expect(typeof result.start).toBe('number');
expect(typeof result.stop).toBe('number');
}
});
});
});

View File

@ -0,0 +1,46 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { getInvolvedQueriesInTraceOperator } from '../utils/utils';
const makeTraceOperator = (expression: string): IBuilderTraceOperator =>
(({ expression } as unknown) as IBuilderTraceOperator);
describe('getInvolvedQueriesInTraceOperator', () => {
it('returns empty array for empty input', () => {
const result = getInvolvedQueriesInTraceOperator([]);
expect(result).toEqual([]);
});
it('extracts identifiers from expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => B'),
]);
expect(result).toEqual(['A', 'B']);
});
it('extracts identifiers from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator('A => (NOT B || C)'),
]);
expect(result).toEqual(['A', 'B', 'C']);
});
it('filters out querynames from complex expression', () => {
const result = getInvolvedQueriesInTraceOperator([
makeTraceOperator(
'(A1 && (NOT B2 || (C3 -> (D4 && E5)))) => ((F6 || G7) && (NOT (H8 -> I9)))',
),
]);
expect(result).toEqual([
'A1',
'B2',
'C3',
'D4',
'E5',
'F6',
'G7',
'H8',
'I9',
]);
});
});

View File

@ -0,0 +1,562 @@
/* eslint-disable sonarjs/cognitive-complexity */
/* eslint-disable no-continue */
import { CharStreams, CommonTokenStream, Token } from 'antlr4';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import { IToken } from 'types/antlrQueryTypes';
// Trace Operator Context Interface
export interface ITraceOperatorContext {
tokenType: number;
text: string;
start: number;
stop: number;
currentToken: string;
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
atomToken?: string;
operatorToken?: string;
expressionPairs: ITraceExpressionPair[];
currentPair?: ITraceExpressionPair | null;
}
// Trace Expression Pair Interface
export interface ITraceExpressionPair {
leftAtom: string;
operator: string;
rightAtom?: string;
rightExpression?: string;
position: {
leftStart: number;
leftEnd: number;
operatorStart: number;
operatorEnd: number;
rightStart?: number;
rightEnd?: number;
};
isComplete: boolean;
}
// Helper functions to determine token types
function isAtomToken(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.IDENTIFIER;
}
function isOperatorToken(tokenType: number): boolean {
return [
TraceOperatorGrammarLexer.T__2, // '=>'
TraceOperatorGrammarLexer.T__3, // '&&'
TraceOperatorGrammarLexer.T__4, // '||'
TraceOperatorGrammarLexer.T__5, // 'NOT'
TraceOperatorGrammarLexer.T__6, // '->'
].includes(tokenType);
}
function isParenthesisToken(tokenType: number): boolean {
return (
tokenType === TraceOperatorGrammarLexer.T__0 ||
tokenType === TraceOperatorGrammarLexer.T__1
);
}
function isOpeningParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__0;
}
function isClosingParenthesis(tokenType: number): boolean {
return tokenType === TraceOperatorGrammarLexer.T__1;
}
// Function to create a context object
export function createTraceOperatorContext(
token: Token,
isInAtom: boolean,
isInOperator: boolean,
isInParenthesis: boolean,
isInExpression: boolean,
atomToken?: string,
operatorToken?: string,
expressionPairs?: ITraceExpressionPair[],
currentPair?: ITraceExpressionPair | null,
): ITraceOperatorContext {
return {
tokenType: token.type,
text: token.text || '',
start: token.start,
stop: token.stop,
currentToken: token.text || '',
isInAtom,
isInOperator,
isInParenthesis,
isInExpression,
atomToken,
operatorToken,
expressionPairs: expressionPairs || [],
currentPair,
};
}
// Helper to determine token context
function determineTraceTokenContext(
token: IToken,
): {
isInAtom: boolean;
isInOperator: boolean;
isInParenthesis: boolean;
isInExpression: boolean;
} {
const tokenType = token.type;
return {
isInAtom: isAtomToken(tokenType),
isInOperator: isOperatorToken(tokenType),
isInParenthesis: isParenthesisToken(tokenType),
isInExpression: false, // Will be determined by broader context
};
}
/**
* Extracts all expression pairs from a trace operator query string
* This parses the query according to the TraceOperatorGrammar.g4 grammar
*
* @param query The trace operator query string to parse
* @returns An array of ITraceExpressionPair objects representing the expression pairs
*/
export function extractTraceExpressionPairs(
query: string,
): ITraceExpressionPair[] {
try {
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
const expressionPairs: ITraceExpressionPair[] = [];
let currentPair: Partial<ITraceExpressionPair> | null = null;
let i = 0;
while (i < allTokens.length) {
const token = allTokens[i];
i++;
// Skip EOF and whitespace tokens
if (token.type === TraceOperatorGrammarLexer.EOF || token.channel !== 0) {
continue;
}
// If token is an IDENTIFIER (atom), start or continue a pair
if (isAtomToken(token.type)) {
// If we don't have a current pair, start one
if (!currentPair) {
currentPair = {
leftAtom: token.text,
position: {
leftStart: token.start,
leftEnd: token.stop,
operatorStart: 0,
operatorEnd: 0,
},
};
}
// If we have a current pair but no operator yet, this is still the left atom
else if (!currentPair.operator && currentPair.position) {
currentPair.leftAtom = token.text;
currentPair.position.leftStart = token.start;
currentPair.position.leftEnd = token.stop;
}
// If we have an operator, this is the right atom
else if (
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
currentPair.rightAtom = token.text;
currentPair.position.rightStart = token.start;
currentPair.position.rightEnd = token.stop;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
}
}
// If token is an operator and we have a left atom
else if (
isOperatorToken(token.type) &&
currentPair &&
currentPair.leftAtom &&
currentPair.position
) {
currentPair.operator = token.text;
currentPair.position.operatorStart = token.start;
currentPair.position.operatorEnd = token.stop;
// If this is a NOT operator, it might be followed by another operator
if (token.type === TraceOperatorGrammarLexer.T__5 && i < allTokens.length) {
// Look ahead for the next operator
const nextToken = allTokens[i];
if (isOperatorToken(nextToken.type) && nextToken.channel === 0) {
currentPair.operator = `${token.text} ${nextToken.text}`;
currentPair.position.operatorEnd = nextToken.stop;
i++; // Skip the next token since we've consumed it
}
}
}
// If token is an opening parenthesis after an operator, this is a right expression
else if (
isOpeningParenthesis(token.type) &&
currentPair &&
currentPair.operator &&
!currentPair.rightAtom &&
currentPair.position
) {
// Find the matching closing parenthesis
let parenCount = 1;
let j = i;
let rightExpression = '';
const rightStart = token.start;
let rightEnd = token.stop;
while (j < allTokens.length && parenCount > 0) {
const parenToken = allTokens[j];
if (parenToken.channel === 0) {
if (isOpeningParenthesis(parenToken.type)) {
parenCount++;
} else if (isClosingParenthesis(parenToken.type)) {
parenCount--;
if (parenCount === 0) {
rightEnd = parenToken.stop;
break;
}
}
}
rightExpression += parenToken.text;
j++;
}
if (parenCount === 0) {
currentPair.rightExpression = rightExpression;
currentPair.position.rightStart = rightStart;
currentPair.position.rightEnd = rightEnd;
currentPair.isComplete = true;
// Add the completed pair to the result
expressionPairs.push(currentPair as ITraceExpressionPair);
currentPair = null;
// Skip to the end of the expression
i = j;
}
}
}
// Add any remaining incomplete pair
if (currentPair && currentPair.leftAtom && currentPair.position) {
expressionPairs.push({
...currentPair,
isComplete: !!(currentPair.leftAtom && currentPair.operator),
} as ITraceExpressionPair);
}
return expressionPairs;
} catch (error) {
console.error('Error in extractTraceExpressionPairs:', error);
return [];
}
}
/**
* Gets the current expression pair at the cursor position
*
* @param expressionPairs An array of ITraceExpressionPair objects
* @param query The full query string
* @param cursorIndex The position of the cursor in the query
* @returns The expression pair at the cursor position, or null if not found
*/
export function getCurrentTraceExpressionPair(
expressionPairs: ITraceExpressionPair[],
cursorIndex: number,
): ITraceExpressionPair | null {
try {
if (expressionPairs.length === 0) {
return null;
}
// Find the rightmost pair whose end position is before or at the cursor
let bestMatch: ITraceExpressionPair | null = null;
// eslint-disable-next-line no-restricted-syntax
for (const pair of expressionPairs) {
const { position } = pair;
const pairEnd =
position.rightEnd || position.operatorEnd || position.leftEnd;
const pairStart = position.leftStart;
// If this pair ends at or before the cursor, and it's further right than our previous best match
if (
pairStart <= cursorIndex &&
cursorIndex <= pairEnd + 1 &&
(!bestMatch ||
pairEnd >
(bestMatch.position.rightEnd ||
bestMatch.position.operatorEnd ||
bestMatch.position.leftEnd))
) {
bestMatch = pair;
}
}
return bestMatch;
} catch (error) {
console.error('Error in getCurrentTraceExpressionPair:', error);
return null;
}
}
/**
* Gets the current trace operator context at the cursor position
* This is useful for determining what kind of suggestions to show
*
* @param query The trace operator query string
* @param cursorIndex The position of the cursor in the query
* @returns The trace operator context at the cursor position
*/
export function getTraceOperatorContextAtCursor(
query: string,
cursorIndex: number,
): ITraceOperatorContext {
try {
// Guard against infinite recursion
const stackTrace = new Error().stack || '';
const callCount = (stackTrace.match(/getTraceOperatorContextAtCursor/g) || [])
.length;
if (callCount > 3) {
console.warn(
'Potential infinite recursion detected in getTraceOperatorContextAtCursor',
);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
// Create input stream and lexer
const input = query || '';
const chars = CharStreams.fromString(input);
const lexer = new TraceOperatorGrammarLexer(chars);
const tokenStream = new CommonTokenStream(lexer);
tokenStream.fill();
const allTokens = tokenStream.tokens as IToken[];
// Get expression pairs information
const expressionPairs = extractTraceExpressionPairs(query);
const currentPair = getCurrentTraceExpressionPair(
expressionPairs,
cursorIndex,
);
// Find the token at or just before the cursor
let lastTokenBeforeCursor: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.stop < cursorIndex || token.stop + 1 === cursorIndex) {
lastTokenBeforeCursor = token;
}
if (token.start > cursorIndex) {
break;
}
}
// Find exact token at cursor
let exactToken: IToken | null = null;
for (let i = 0; i < allTokens.length; i++) {
const token = allTokens[i];
if (token.type === TraceOperatorGrammarLexer.EOF) continue;
if (token.start <= cursorIndex && cursorIndex <= token.stop + 1) {
exactToken = token;
break;
}
}
// If we don't have any tokens, return default context
if (!lastTokenBeforeCursor && !exactToken) {
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true, // Default to atom context when input is empty
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair: null,
};
}
// Check if cursor is at a space after a token (transition point)
const isAtSpace = cursorIndex < query.length && query[cursorIndex] === ' ';
const isAfterSpace = cursorIndex > 0 && query[cursorIndex - 1] === ' ';
const isAfterToken = cursorIndex > 0 && query[cursorIndex - 1] !== ' ';
const isTransitionPoint =
(isAtSpace && isAfterToken) ||
(cursorIndex === query.length && isAfterToken);
// If we're at a transition point after a token, progress the context
if (
lastTokenBeforeCursor &&
(isAtSpace || isAfterSpace || isTransitionPoint)
) {
const lastTokenContext = determineTraceTokenContext(lastTokenBeforeCursor);
// Apply context progression: atom → operator → atom/expression → operator → atom
if (lastTokenContext.isInAtom) {
// After atom + space, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
atomToken: lastTokenBeforeCursor.text,
expressionPairs,
currentPair,
};
}
if (lastTokenContext.isInOperator) {
// After operator + space, move to atom/expression context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: true, // Expecting an atom or expression after operator
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
operatorToken: lastTokenBeforeCursor.text,
atomToken: currentPair?.leftAtom,
expressionPairs,
currentPair,
};
}
if (
lastTokenContext.isInParenthesis &&
isClosingParenthesis(lastTokenBeforeCursor.type)
) {
// After closing parenthesis, move to operator context
return {
tokenType: lastTokenBeforeCursor.type,
text: lastTokenBeforeCursor.text,
start: cursorIndex,
stop: cursorIndex,
currentToken: lastTokenBeforeCursor.text,
isInAtom: false,
isInOperator: true,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
}
}
// If cursor is at the end of a token, return the current token context
if (exactToken && cursorIndex === exactToken.stop + 1) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Regular token-based context detection
if (exactToken?.channel === 0) {
const tokenContext = determineTraceTokenContext(exactToken);
return {
tokenType: exactToken.type,
text: exactToken.text,
start: exactToken.start,
stop: exactToken.stop,
currentToken: exactToken.text,
...tokenContext,
atomToken: tokenContext.isInAtom ? exactToken.text : currentPair?.leftAtom,
operatorToken: tokenContext.isInOperator
? exactToken.text
: currentPair?.operator,
expressionPairs,
currentPair,
};
}
// Default fallback to atom context
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs,
currentPair,
};
} catch (error) {
console.error('Error in getTraceOperatorContextAtCursor:', error);
return {
tokenType: -1,
text: '',
start: cursorIndex,
stop: cursorIndex,
currentToken: '',
isInAtom: true,
isInOperator: false,
isInParenthesis: false,
isInExpression: false,
expressionPairs: [],
currentPair: null,
};
}
}

View File

@ -0,0 +1,22 @@
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
export const getInvolvedQueriesInTraceOperator = (
traceOperators: IBuilderTraceOperator[],
): string[] => {
if (
!traceOperators ||
traceOperators.length === 0 ||
traceOperators.length > 1
)
return [];
const currentTraceOperator = traceOperators[0];
// Match any word starting with letter or underscore
const tokens =
currentTraceOperator.expression.match(/\b[A-Za-z_][A-Za-z0-9_]*\b/g) || [];
// Filter out operator keywords
const operators = new Set(['NOT']);
return tokens.filter((t) => !operators.has(t));
};

View File

@ -17,6 +17,19 @@
font-weight: var(--font-weight-normal);
}
.view-title-container {
display: flex;
align-items: center;
gap: 6px;
justify-content: center;
.icon-container {
display: flex;
align-items: center;
justify-content: center;
}
}
.tab {
border: 1px solid var(--bg-slate-400);
&:hover {

View File

@ -6,6 +6,7 @@ import { RadioChangeEvent } from 'antd/es/radio';
interface Option {
value: string;
label: string;
icon?: React.ReactNode;
}
interface SignozRadioGroupProps {
@ -37,7 +38,10 @@ function SignozRadioGroup({
value={option.value}
className={value === option.value ? 'selected_view tab' : 'tab'}
>
{option.label}
<div className="view-title-container">
{option.icon && <div className="icon-container">{option.icon}</div>}
{option.label}
</div>
</Radio.Button>
))}
</Radio.Group>

View File

@ -17,6 +17,27 @@ export const OPERATORS = {
'<': '<',
};
export const TRACE_OPERATOR_OPERATORS = {
AND: '&&',
OR: '||',
NOT: 'NOT',
DIRECT_DESCENDENT: '=>',
INDIRECT_DESCENDENT: '->',
};
export const TRACE_OPERATOR_OPERATORS_WITH_PRIORITY = {
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 1,
[TRACE_OPERATOR_OPERATORS.AND]: 2,
[TRACE_OPERATOR_OPERATORS.OR]: 3,
[TRACE_OPERATOR_OPERATORS.NOT]: 4,
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 5,
};
export const TRACE_OPERATOR_OPERATORS_LABELS = {
[TRACE_OPERATOR_OPERATORS.DIRECT_DESCENDENT]: 'Direct Descendant',
[TRACE_OPERATOR_OPERATORS.INDIRECT_DESCENDENT]: 'Indirect Descendant',
};
export const QUERY_BUILDER_FUNCTIONS = {
HAS: 'has',
HASANY: 'hasAny',

View File

@ -12,6 +12,7 @@ import {
HavingForm,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -50,6 +51,8 @@ import {
export const MAX_FORMULAS = 20;
export const MAX_QUERIES = 26;
export const TRACE_OPERATOR_QUERY_NAME = 'Trace Operator';
export const idDivider = '--';
export const selectValueDivider = '__';
@ -263,6 +266,11 @@ export const initialFormulaBuilderFormValues: IBuilderFormula = {
legend: '',
};
export const initialQueryBuilderFormTraceOperatorValues: IBuilderTraceOperator = {
...initialQueryBuilderFormTracesValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
};
export const initialQueryPromQLData: IPromQLQuery = {
name: createNewBuilderItemName({ existNames: [], sourceNames: alphabet }),
query: '',
@ -280,6 +288,7 @@ export const initialClickHouseData: IClickHouseQuery = {
export const initialQueryBuilderData: QueryBuilderData = {
queryData: [initialQueryBuilderFormValues],
queryFormulas: [],
queryTraceOperator: [],
};
export const initialSingleQueryMap: Record<

View File

@ -1,3 +1,5 @@
import { TRACE_OPERATOR_QUERY_NAME } from './queryBuilder';
export const FORMULA_REGEXP = /F\d+/;
export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
@ -5,3 +7,5 @@ export const HAVING_FILTER_REGEXP = /^[-\d.,\s]+$/;
export const TYPE_ADDON_REGEXP = /_(.+)/;
export const SPLIT_FIRST_UNDERSCORE = /(?<!^)_/;
export const TRACE_OPERATOR_REGEXP = new RegExp(TRACE_OPERATOR_QUERY_NAME);

View File

@ -507,6 +507,7 @@ export const getDomainMetricsQueryPayload = (
legend: '',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -816,6 +817,7 @@ export const getEndPointsQueryPayload = (
legend: 'error percentage',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -965,6 +967,7 @@ export const getTopErrorsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1729,6 +1732,7 @@ export const getEndPointDetailsQueryPayload = (
legend: 'error percentage',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1928,6 +1932,7 @@ export const getEndPointDetailsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2016,6 +2021,7 @@ export const getEndPointDetailsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2287,6 +2293,7 @@ export const getEndPointDetailsQueryPayload = (
legend: 'error percentage',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2376,6 +2383,7 @@ export const getEndPointDetailsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2464,6 +2472,7 @@ export const getEndPointDetailsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2558,6 +2567,7 @@ export const getEndPointZeroStateQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -3135,6 +3145,7 @@ export const getStatusCodeBarChartWidgetData = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -54,6 +54,7 @@ function QuerySection({
queryVariant: 'static',
initialDataSource: ALERTS_DATA_SOURCE_MAP[alertType],
}}
showTraceOperator={alertType === AlertTypes.TRACES_BASED_ALERT}
showFunctions={
(alertType === AlertTypes.METRICS_BASED_ALERT &&
alertDef.version === ENTITY_VERSION_V4) ||

View File

@ -5,6 +5,7 @@ import { Button, FormInstance, Modal, SelectProps, Typography } from 'antd';
import saveAlertApi from 'api/alerts/save';
import testAlertApi from 'api/alerts/testAlert';
import logEvent from 'api/common/logEvent';
import { getInvolvedQueriesInTraceOperator } from 'components/QueryBuilderV2/QueryV2/TraceOperator/utils/utils';
import { ALERTS_DATA_SOURCE_MAP } from 'constants/alerts';
import { FeatureKeys } from 'constants/features';
import { QueryParams } from 'constants/query';
@ -149,10 +150,17 @@ function FormAlertRules({
]);
const queryOptions = useMemo(() => {
const involvedQueriesInTraceOperator = getInvolvedQueriesInTraceOperator(
currentQuery.builder.queryTraceOperator,
);
const queryConfig: Record<EQueryType, () => SelectProps['options']> = {
[EQueryType.QUERY_BUILDER]: () => [
...(getSelectedQueryOptions(currentQuery.builder.queryData) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryData)?.filter(
(option) =>
!involvedQueriesInTraceOperator.includes(option.value as string),
) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryFormulas) || []),
...(getSelectedQueryOptions(currentQuery.builder.queryTraceOperator) || []),
],
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>

View File

@ -5,6 +5,7 @@ import getStep from 'lib/getStep';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
} from 'types/api/queryBuilder/queryBuilderData';
@ -53,7 +54,11 @@ export const getUpdatedStepInterval = (evalWindow?: string): number => {
export const getSelectedQueryOptions = (
queries: Array<
IBuilderQuery | IBuilderFormula | IClickHouseQuery | IPromQLQuery
| IBuilderQuery
| IBuilderTraceOperator
| IBuilderFormula
| IClickHouseQuery
| IPromQLQuery
>,
): SelectProps['options'] =>
queries

View File

@ -90,6 +90,7 @@ const mockProps: WidgetGraphComponentProps = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -131,6 +131,7 @@ describe('GridCardLayout Utils', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
promql: [],
@ -171,6 +172,7 @@ describe('GridCardLayout Utils', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};
@ -195,6 +197,7 @@ describe('GridCardLayout Utils', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};
@ -240,6 +243,7 @@ describe('GridCardLayout Utils', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};
@ -268,6 +272,7 @@ describe('GridCardLayout Utils', () => {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};

View File

@ -162,6 +162,7 @@ export const widgetQueryWithLegend = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '48ad5a67-9a3c-49d4-a886-d7a34f8b875d',
queryType: 'builder',
@ -457,6 +458,7 @@ export const widgetQueryQBv5MultiAggregations = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: 'qb-v5-multi-aggregations-test',
queryType: 'builder',

View File

@ -301,6 +301,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -490,6 +491,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -575,6 +577,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -660,6 +663,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -797,6 +801,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1050,6 +1055,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1257,6 +1263,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1522,6 +1529,7 @@ export const getClusterMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -233,6 +233,7 @@ export const getDaemonSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -416,6 +417,7 @@ export const getDaemonSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -512,6 +514,7 @@ export const getDaemonSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -608,6 +611,7 @@ export const getDaemonSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -196,6 +196,7 @@ export const getDeploymentMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -346,6 +347,7 @@ export const getDeploymentMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -431,6 +433,7 @@ export const getDeploymentMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -516,6 +519,7 @@ export const getDeploymentMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -79,6 +79,7 @@ export const getEntityEventsOrLogsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: uuidv4(),
queryType: EQueryType.QUERY_BUILDER,
@ -226,6 +227,7 @@ export const getEntityTracesQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
id: '572f1d91-6ac0-46c0-b726-c21488b34434',
queryType: EQueryType.QUERY_BUILDER,

View File

@ -108,6 +108,7 @@ export const getJobMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -191,6 +192,7 @@ export const getJobMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -287,6 +289,7 @@ export const getJobMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -383,6 +386,7 @@ export const getJobMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -309,6 +309,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -576,6 +577,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -655,6 +657,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -734,6 +737,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -819,6 +823,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -904,6 +909,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1075,6 +1081,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1212,6 +1219,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1429,6 +1437,7 @@ export const getNamespaceMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1561,6 +1570,7 @@ export const getNamespaceMetricsQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -341,6 +341,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -647,6 +648,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -810,6 +812,7 @@ export const getNodeMetricsQueryPayload = (
queryName: 'F2',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -973,6 +976,7 @@ export const getNodeMetricsQueryPayload = (
queryName: 'F2',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1052,6 +1056,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1131,6 +1136,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1216,6 +1222,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1301,6 +1308,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1451,6 +1459,7 @@ export const getNodeMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1569,6 +1578,7 @@ export const getNodeMetricsQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -335,6 +335,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -668,6 +669,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -851,6 +853,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1184,6 +1187,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1324,6 +1328,7 @@ export const getPodMetricsQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1407,6 +1412,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1497,6 +1503,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1714,6 +1721,7 @@ export const getPodMetricsQueryPayload = (
queryName: 'F2',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -1918,6 +1926,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2135,6 +2144,7 @@ export const getPodMetricsQueryPayload = (
queryName: 'F2',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2231,6 +2241,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2327,6 +2338,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{
@ -2510,6 +2522,7 @@ export const getPodMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [
{

View File

@ -246,6 +246,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -365,6 +366,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -534,6 +536,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -653,6 +656,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -735,6 +739,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -817,6 +822,7 @@ export const getStatefulSetMetricsQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),

View File

@ -148,6 +148,7 @@ export const getVolumeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -239,6 +240,7 @@ export const getVolumeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -330,6 +332,7 @@ export const getVolumeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -421,6 +424,7 @@ export const getVolumeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),
@ -512,6 +516,7 @@ export const getVolumeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: v4(),

View File

@ -58,6 +58,7 @@ export const mockQuery: Query = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: 'test-query-id',

View File

@ -121,6 +121,7 @@ export const getPodQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '9b92756a-b445-45f8-90f4-d26f3ef28f8f',
@ -197,6 +198,7 @@ export const getPodQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'a22c1e03-4876-4b3e-9a96-a3c3a28f9c0f',
@ -337,6 +339,7 @@ export const getPodQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '7bb3a6f5-d1c6-4f2e-9cc9-7dcc46db398f',
@ -477,6 +480,7 @@ export const getPodQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '6d5ccd81-0ea1-4fb9-a66b-7f0fe2f15165',
@ -624,6 +628,7 @@ export const getPodQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '4d03a0ff-4fa5-4b19-b397-97f80ba9e0ac',
@ -772,6 +777,7 @@ export const getPodQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'ad491f19-0f83-4dd4-bb8f-bec295c18d1b',
@ -920,6 +926,7 @@ export const getPodQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '16908d4e-1565-4847-8d87-01ebb8fc494a',
@ -1001,6 +1008,7 @@ export const getPodQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '4b255d6d-4cde-474d-8866-f4418583c18b',
@ -1177,6 +1185,7 @@ export const getNodeQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '259295b5-774d-4b2e-8a4f-e5dd63e6c38d',
@ -1314,6 +1323,7 @@ export const getNodeQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '486af4da-2a1a-4b8f-992c-eba098d3a6f9',
@ -1409,6 +1419,7 @@ export const getNodeQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'b56143c0-7d2f-4425-97c5-65ad6fc87366',
@ -1557,6 +1568,7 @@ export const getNodeQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '57eeac15-615c-4a71-9c61-8e0c0c76b045',
@ -1718,6 +1730,7 @@ export const getHostQueryPayload = (
queryName: 'F1',
},
],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '315b15fa-ff0c-442f-89f8-2bf4fb1af2f2',
@ -1786,6 +1799,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '40218bfb-a9b7-4974-aead-5bf666e139bf',
@ -1928,6 +1942,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '8e6485ea-7018-43b0-ab27-b210f77b59ad',
@ -2009,6 +2024,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '47173220-44df-4ef6-87f4-31e333c180c7',
@ -2084,6 +2100,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '62eedbc6-c8ad-4d13-80a8-129396e1d1dc',
@ -2159,6 +2176,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '5ddb1b38-53bb-46f5-b4fe-fe832d6b9b24',
@ -2234,6 +2252,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'a849bcce-7684-4852-9134-530b45419b8f',
@ -2309,6 +2328,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'ab685a3d-fa4c-4663-8d94-c452e59038f3',
@ -2369,6 +2389,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '9bd40b51-0790-4cdd-9718-551b2ded5926',
@ -2450,6 +2471,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: '9c6d18ad-89ff-4e38-a15a-440e72ed6ca8',
@ -2524,6 +2546,7 @@ export const getHostQueryPayload = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'f4cfc2a5-78fc-42cc-8f4a-194c8c916132',

View File

@ -178,6 +178,10 @@ export const mockQueryBuilderContextValue = {
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
lastUsedQuery: 0,
handleSetTraceOperatorData: noop,
removeAllQueryBuilderEntities: noop,
removeTraceOperator: noop,
addTraceOperator: noop,
setLastUsedQuery: noop,
handleSetQueryData: noop,
handleSetFormulaData: noop,

View File

@ -71,6 +71,7 @@ export function getWidgetQuery(
builder: {
queryData: props.queryData,
queryFormulas: (props.queryFormulas as IBuilderFormula[]) || [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuid(),

View File

@ -64,6 +64,7 @@ export const getQueryBuilderQueries = ({
return newQueryData;
}),
queryTraceOperator: [],
});
export const getQueryBuilderQuerieswithFormula = ({
@ -106,4 +107,5 @@ export const getQueryBuilderQuerieswithFormula = ({
}),
dataSource,
})),
queryTraceOperator: [],
});

View File

@ -71,6 +71,7 @@ export const useGetRelatedMetricsGraphs = ({
builder: {
queryData: [metric.query],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuidv4(),

View File

@ -150,6 +150,7 @@ export function getMetricDetailsQuery(
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};
}

View File

@ -164,6 +164,7 @@ function QuerySection({
<QueryBuilderV2
panelType={selectedGraph}
filterConfigs={filterConfigs}
showTraceOperator={selectedGraph !== PANEL_TYPES.LIST}
version={selectedDashboard?.data?.version || 'v3'}
isListViewPanel={selectedGraph === PANEL_TYPES.LIST}
queryComponents={queryComponents}

View File

@ -53,6 +53,7 @@ const compositeQueryParam = {
legend: '',
},
],
queryTraceOperator: [],
},
promql: [
{

View File

@ -33,6 +33,7 @@ const buildSupersetQuery = (extras?: Record<string, unknown>): Query => ({
...(extras || {}),
},
],
queryTraceOperator: [],
},
});

View File

@ -528,6 +528,10 @@ export function handleQueryChange(
return tempQuery;
}),
queryTraceOperator:
newPanelType === PANEL_TYPES.LIST
? []
: supersetQuery.builder.queryTraceOperator,
},
};
}

View File

@ -77,6 +77,7 @@ export default function LogsConnectionStatus(): JSX.Element {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: '',

View File

@ -30,5 +30,14 @@ export type QueryBuilderProps = {
isListViewPanel?: boolean;
showFunctions?: boolean;
showOnlyWhereClause?: boolean;
showOnlyTraceOperator?: boolean;
showTraceViewSelector?: boolean;
showTraceOperator?: boolean;
version: string;
onChangeTraceView?: (view: TraceView) => void;
};
export enum TraceView {
SPANS = 'spans',
TRACES = 'traces',
}

View File

@ -39,6 +39,8 @@ interface QBEntityOptionsProps {
showCloneOption?: boolean;
isListViewPanel?: boolean;
index?: number;
showTraceOperator?: boolean;
hasTraceOperator?: boolean;
queryVariant?: 'dropdown' | 'static';
onChangeDataSource?: (value: DataSource) => void;
}
@ -61,6 +63,8 @@ export default function QBEntityOptions({
onCloneQuery,
index,
queryVariant,
hasTraceOperator = false,
showTraceOperator = false,
onChangeDataSource,
}: QBEntityOptionsProps): JSX.Element {
const handleCloneEntity = (): void => {
@ -97,7 +101,7 @@ export default function QBEntityOptions({
value="query-builder"
className="periscope-btn visibility-toggle"
onClick={onToggleVisibility}
disabled={isListViewPanel}
disabled={isListViewPanel && !showTraceOperator}
>
{entityData.disabled ? <EyeOff size={16} /> : <Eye size={16} />}
</Button>
@ -115,6 +119,10 @@ export default function QBEntityOptions({
className={cx(
'periscope-btn',
entityType === 'query' ? 'query-name' : 'formula-name',
query?.dataSource === DataSource.TRACES &&
(hasTraceOperator || (showTraceOperator && isListViewPanel))
? 'has-trace-operator'
: '',
isLogsExplorerPage && lastUsedQuery === index ? 'sync-btn' : '',
)}
>
@ -183,4 +191,6 @@ QBEntityOptions.defaultProps = {
showCloneOption: true,
queryVariant: 'static',
onChangeDataSource: noop,
hasTraceOperator: false,
showTraceOperator: false,
};

View File

@ -11,5 +11,8 @@ export type QueryProps = {
version: string;
showSpanScopeSelector?: boolean;
showOnlyWhereClause?: boolean;
showTraceOperator?: boolean;
hasTraceOperator?: boolean;
signalSource?: string;
isMultiQueryAllowed?: boolean;
} & Pick<QueryBuilderProps, 'filterConfigs' | 'queryComponents'>;

View File

@ -67,6 +67,7 @@ export const getTraceToLogsQuery = (
},
],
queryFormulas: [],
queryTraceOperator: [],
},
};

View File

@ -106,6 +106,19 @@ function ListView({
];
}
// add order by to trace operator
if (
query.builder.queryTraceOperator &&
query.builder.queryTraceOperator.length > 0
) {
query.builder.queryTraceOperator[0].orderBy = [
{
columnName: orderBy.split(':')[0],
order: orderBy.split(':')[1] as 'asc' | 'desc',
},
];
}
return query;
}, [stagedQuery, orderBy]);

View File

@ -37,11 +37,15 @@ function QuerySection(): JSX.Element {
};
}, [panelTypes, renderOrderBy]);
const isListViewPanel = useMemo(
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
[panelTypes],
);
return (
<QueryBuilderV2
isListViewPanel={
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
}
isListViewPanel={isListViewPanel}
showTraceOperator
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
queryComponents={queryComponents}
panelType={panelTypes}

View File

@ -54,9 +54,11 @@ export const useQueryOperations: UseQueryOperations = ({
formula,
isListViewPanel = false,
entityVersion,
isForTraceOperator = false,
}) => {
const {
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
removeQueryBuilderEntityByIndex,
panelType,
@ -400,9 +402,19 @@ export const useQueryOperations: UseQueryOperations = ({
: value,
};
handleSetQueryData(index, newQuery);
if (isForTraceOperator) {
handleSetTraceOperatorData(index, newQuery);
} else {
handleSetQueryData(index, newQuery);
}
},
[query, index, handleSetQueryData],
[
query,
index,
handleSetQueryData,
handleSetTraceOperatorData,
isForTraceOperator,
],
);
const handleChangeFormulaData: HandleChangeFormulaData = useCallback(

View File

@ -78,6 +78,7 @@ export const stepIntervalUnchanged = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
@ -242,6 +243,7 @@ export const replaceVariables = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],
@ -292,6 +294,7 @@ export const defaultOutput = {
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [{ disabled: false, legend: '', name: 'A', query: '' }],
id: 'test-id',
@ -469,6 +472,7 @@ export const outputWithFunctions = {
ShiftBy: 0,
},
],
queryTraceOperator: [],
},
promql: [{ name: 'A', query: '', legend: '', disabled: false }],
clickhouse_sql: [{ name: 'A', legend: '', disabled: false, query: '' }],

View File

@ -25,12 +25,17 @@ const buildBuilderQuery = (
query: Query,
panelType: PANEL_TYPES | null,
): ICompositeMetricQuery => {
const { queryData, queryFormulas } = query.builder;
const { queryData, queryFormulas, queryTraceOperator } = query.builder;
const currentQueryData = mapQueryDataToApi(queryData, 'queryName');
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
const currentTraceOperator = mapQueryDataToApi(
queryTraceOperator,
'queryName',
);
const builderQueries = {
...currentQueryData.data,
...currentFormulas.data,
...currentTraceOperator.data,
};
const compositeQuery = defaultCompositeQuery;

View File

@ -1,8 +1,10 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { initialQueryState } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -22,10 +24,13 @@ import { v4 as uuid } from 'uuid';
import { transformQueryBuilderDataModel } from '../transformQueryBuilderDataModel';
const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
const builderQueries: Record<string, IBuilderQuery | IBuilderFormula> = {};
const builderQueries: Record<
string,
IBuilderQuery | IBuilderFormula | IBuilderTraceOperator
> = {};
const builderQueryTypes: Record<
string,
'builder_query' | 'builder_formula'
'builder_query' | 'builder_formula' | 'builder_trace_operator'
> = {};
const promQueries: IPromQLQuery[] = [];
const clickhouseQueries: IClickHouseQuery[] = [];
@ -46,6 +51,11 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
);
builderQueryTypes[spec.name] = 'builder_formula';
}
} else if (q.type === 'builder_trace_operator') {
if (spec.name) {
builderQueries[spec.name] = (spec as unknown) as IBuilderTraceOperator;
builderQueryTypes[spec.name] = 'builder_trace_operator';
}
} else if (q.type === 'promql') {
const promSpec = spec as PromQuery;
promQueries.push({

View File

@ -2,29 +2,41 @@ import {
initialFormulaBuilderFormValues,
initialQueryBuilderFormValuesMap,
} from 'constants/queryBuilder';
import { FORMULA_REGEXP } from 'constants/regExp';
import { FORMULA_REGEXP, TRACE_OPERATOR_REGEXP } from 'constants/regExp';
import {
BuilderQueryDataResourse,
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { QueryBuilderData } from 'types/common/queryBuilder';
export const transformQueryBuilderDataModel = (
data: BuilderQueryDataResourse,
queryTypes?: Record<string, 'builder_query' | 'builder_formula'>,
queryTypes?: Record<
string,
'builder_query' | 'builder_formula' | 'builder_trace_operator'
>,
): QueryBuilderData => {
const queryData: QueryBuilderData['queryData'] = [];
const queryFormulas: QueryBuilderData['queryFormulas'] = [];
const queryTraceOperator: QueryBuilderData['queryTraceOperator'] = [];
Object.entries(data).forEach(([key, value]) => {
const isFormula = queryTypes
? queryTypes[key] === 'builder_formula'
: FORMULA_REGEXP.test(value.queryName);
const isTraceOperator = queryTypes
? queryTypes[key] === 'builder_trace_operator'
: TRACE_OPERATOR_REGEXP.test(value.queryName);
if (isFormula) {
const formula = value as IBuilderFormula;
queryFormulas.push({ ...initialFormulaBuilderFormValues, ...formula });
} else if (isTraceOperator) {
const traceOperator = value as IBuilderTraceOperator;
queryTraceOperator.push({ ...traceOperator });
} else {
const queryFromData = value as IBuilderQuery;
queryData.push({
@ -34,5 +46,5 @@ export const transformQueryBuilderDataModel = (
}
});
return { queryData, queryFormulas };
return { queryData, queryFormulas, queryTraceOperator };
};

View File

@ -206,6 +206,7 @@ describe('Logs Explorer Tests', () => {
initialQueryBuilderFormValues,
initialQueryBuilderFormValues,
],
queryTraceOperator: [],
},
},
setSupersetQuery: jest.fn(),
@ -215,6 +216,10 @@ describe('Logs Explorer Tests', () => {
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
lastUsedQuery: 0,
handleSetTraceOperatorData: noop,
removeAllQueryBuilderEntities: noop,
removeTraceOperator: noop,
addTraceOperator: noop,
setLastUsedQuery: noop,
handleSetQueryData: noop,
handleSetFormulaData: noop,

View File

@ -72,6 +72,7 @@ export function getWidgetQuery(
builder: {
queryData: props.queryData,
queryFormulas: (props.queryFormulas as IBuilderFormula[]) || [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuid(),

View File

@ -155,6 +155,7 @@ export function getWidgetQuery({
},
],
queryFormulas: [],
queryTraceOperator: [],
},
clickhouse_sql: [],
id: uuid(),

View File

@ -1,6 +1,7 @@
import './TracesExplorer.styles.scss';
import * as Sentry from '@sentry/react';
import { Callout } from '@signozhq/callout';
import { Card } from 'antd';
import logEvent from 'api/common/logEvent';
import cx from 'classnames';
@ -35,7 +36,10 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useSearchParams } from 'react-router-dom-v5-compat';
import { Warning } from 'types/api';
import { Dashboard } from 'types/api/dashboard/getAll';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import {
IBuilderTraceOperator,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { generateExportToDashboardLink } from 'utils/dashboard/generateExportToDashboardLink';
import {
@ -52,7 +56,6 @@ function TracesExplorer(): JSX.Element {
handleRunQuery,
stagedQuery,
handleSetConfig,
updateQueriesData,
} = useQueryBuilder();
const { options } = useOptionsMenu({
@ -103,32 +106,14 @@ function TracesExplorer(): JSX.Element {
handleSetConfig(PANEL_TYPES.LIST, DataSource.TRACES);
}
if (view === ExplorerViews.LIST) {
if (
selectedView !== ExplorerViews.LIST &&
currentQuery?.builder?.queryData?.[0]
) {
const filterToRetain = currentQuery.builder.queryData[0].filter;
const newDefaultQuery = updateAllQueriesOperators(
initialQueriesMap.traces,
PANEL_TYPES.LIST,
DataSource.TRACES,
);
const newListQuery = updateQueriesData(
newDefaultQuery,
'queryData',
(item, index) => {
if (index === 0) {
return { ...item, filter: filterToRetain };
}
return item;
},
);
setDefaultQuery(newListQuery);
}
setShouldReset(true);
if (
(selectedView === ExplorerViews.TRACE ||
selectedView === ExplorerViews.LIST) &&
stagedQuery?.builder?.queryTraceOperator &&
stagedQuery.builder.queryTraceOperator.length > 0
) {
// remove order by from trace operator
set(stagedQuery, 'builder.queryTraceOperator[0].orderBy', []);
}
setSelectedView(view);
@ -141,10 +126,8 @@ function TracesExplorer(): JSX.Element {
handleSetConfig,
handleExplorerTabChange,
selectedView,
currentQuery,
updateAllQueriesOperators,
updateQueriesData,
setSelectedView,
stagedQuery,
],
);
@ -211,19 +194,44 @@ function TracesExplorer(): JSX.Element {
useShareBuilderUrl({ defaultValue: defaultQuery, forceReset: shouldReset });
const isMultipleQueries = useMemo(() => {
const builder = currentQuery?.builder;
const queriesLen = builder?.queryData?.length ?? 0;
const formulasLen = builder?.queryFormulas?.length ?? 0;
return queriesLen > 1 || formulasLen > 0;
}, [currentQuery]);
const isGroupByExist = useMemo(() => {
const queryData = currentQuery?.builder?.queryData ?? [];
return queryData.some((q) => (q?.groupBy?.length ?? 0) > 0);
}, [currentQuery]);
const hasMultipleQueries = useMemo(
() => currentQuery?.builder?.queryData?.length > 1,
[currentQuery],
);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [currentQuery.builder.queryTraceOperator]);
const showTraceOperatorCallout = useMemo(
() =>
(selectedView === ExplorerViews.LIST ||
selectedView === ExplorerViews.TRACE) &&
hasMultipleQueries &&
!traceOperator,
[selectedView, hasMultipleQueries, traceOperator],
);
const traceOperatorCalloutDescription = useMemo(() => {
if (currentQuery.builder.queryData.length === 0) return '';
const firstQuery = currentQuery.builder.queryData[0];
return `Please use a Trace Operator to combine results of multiple span queries. Else you'd only see the results from query "${firstQuery.queryName}"`;
}, [currentQuery]);
useEffect(() => {
const shouldChangeView = isMultipleQueries || isGroupByExist;
const shouldChangeView = isGroupByExist;
if (
(selectedView === ExplorerViews.LIST ||
@ -233,12 +241,7 @@ function TracesExplorer(): JSX.Element {
// Switch to timeseries view automatically
handleChangeSelectedView(ExplorerViews.TIMESERIES);
}
}, [
selectedView,
isMultipleQueries,
isGroupByExist,
handleChangeSelectedView,
]);
}, [selectedView, isGroupByExist, handleChangeSelectedView]);
useEffect(() => {
if (shouldReset) {
@ -365,6 +368,15 @@ function TracesExplorer(): JSX.Element {
/>
</div>
{showTraceOperatorCallout && (
<Callout
type="info"
size="small"
showIcon
description={traceOperatorCalloutDescription}
/>
)}
{selectedView === ExplorerViews.LIST && (
<div className="trace-explorer-list-view">
<ListView

View File

@ -0,0 +1,33 @@
token literal names:
null
'NOT'
'('
')'
'=>'
'&&'
'||'
'->'
null
null
token symbolic names:
null
null
null
null
null
null
null
null
IDENTIFIER
WS
rule names:
query
expression
atom
operator
atn:
[4, 1, 9, 45, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 1, 0, 4, 0, 10, 8, 0, 11, 0, 12, 0, 11, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 1, 39, 8, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 0, 0, 4, 0, 2, 4, 6, 0, 1, 2, 0, 1, 1, 4, 7, 46, 0, 9, 1, 0, 0, 0, 2, 38, 1, 0, 0, 0, 4, 40, 1, 0, 0, 0, 6, 42, 1, 0, 0, 0, 8, 10, 3, 2, 1, 0, 9, 8, 1, 0, 0, 0, 10, 11, 1, 0, 0, 0, 11, 9, 1, 0, 0, 0, 11, 12, 1, 0, 0, 0, 12, 13, 1, 0, 0, 0, 13, 14, 5, 0, 0, 1, 14, 1, 1, 0, 0, 0, 15, 16, 5, 1, 0, 0, 16, 39, 3, 2, 1, 0, 17, 18, 5, 2, 0, 0, 18, 19, 3, 2, 1, 0, 19, 20, 5, 3, 0, 0, 20, 21, 3, 6, 3, 0, 21, 22, 3, 2, 1, 0, 22, 39, 1, 0, 0, 0, 23, 24, 5, 2, 0, 0, 24, 25, 3, 2, 1, 0, 25, 26, 5, 3, 0, 0, 26, 39, 1, 0, 0, 0, 27, 28, 3, 4, 2, 0, 28, 29, 3, 6, 3, 0, 29, 30, 3, 2, 1, 0, 30, 39, 1, 0, 0, 0, 31, 32, 3, 4, 2, 0, 32, 33, 3, 6, 3, 0, 33, 34, 5, 2, 0, 0, 34, 35, 3, 2, 1, 0, 35, 36, 5, 3, 0, 0, 36, 39, 1, 0, 0, 0, 37, 39, 3, 4, 2, 0, 38, 15, 1, 0, 0, 0, 38, 17, 1, 0, 0, 0, 38, 23, 1, 0, 0, 0, 38, 27, 1, 0, 0, 0, 38, 31, 1, 0, 0, 0, 38, 37, 1, 0, 0, 0, 39, 3, 1, 0, 0, 0, 40, 41, 5, 8, 0, 0, 41, 5, 1, 0, 0, 0, 42, 43, 7, 0, 0, 0, 43, 7, 1, 0, 0, 0, 2, 11, 38]

View File

@ -0,0 +1,16 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
T__6=7
IDENTIFIER=8
WS=9
'NOT'=1
'('=2
')'=3
'=>'=4
'&&'=5
'||'=6
'->'=7

View File

@ -0,0 +1,44 @@
token literal names:
null
'NOT'
'('
')'
'=>'
'&&'
'||'
'->'
null
null
token symbolic names:
null
null
null
null
null
null
null
null
IDENTIFIER
WS
rule names:
T__0
T__1
T__2
T__3
T__4
T__5
T__6
IDENTIFIER
WS
channel names:
DEFAULT_TOKEN_CHANNEL
HIDDEN
mode names:
DEFAULT_MODE
atn:
[4, 0, 9, 57, 6, -1, 2, 0, 7, 0, 2, 1, 7, 1, 2, 2, 7, 2, 2, 3, 7, 3, 2, 4, 7, 4, 2, 5, 7, 5, 2, 6, 7, 6, 2, 7, 7, 7, 2, 8, 7, 8, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 2, 1, 2, 1, 3, 1, 3, 1, 3, 1, 4, 1, 4, 1, 4, 1, 5, 1, 5, 1, 5, 1, 6, 1, 6, 1, 6, 1, 7, 4, 7, 41, 8, 7, 11, 7, 12, 7, 42, 1, 7, 5, 7, 46, 8, 7, 10, 7, 12, 7, 49, 9, 7, 1, 8, 4, 8, 52, 8, 8, 11, 8, 12, 8, 53, 1, 8, 1, 8, 0, 0, 9, 1, 1, 3, 2, 5, 3, 7, 4, 9, 5, 11, 6, 13, 7, 15, 8, 17, 9, 1, 0, 3, 2, 0, 65, 90, 97, 122, 1, 0, 48, 57, 3, 0, 9, 10, 13, 13, 32, 32, 59, 0, 1, 1, 0, 0, 0, 0, 3, 1, 0, 0, 0, 0, 5, 1, 0, 0, 0, 0, 7, 1, 0, 0, 0, 0, 9, 1, 0, 0, 0, 0, 11, 1, 0, 0, 0, 0, 13, 1, 0, 0, 0, 0, 15, 1, 0, 0, 0, 0, 17, 1, 0, 0, 0, 1, 19, 1, 0, 0, 0, 3, 23, 1, 0, 0, 0, 5, 25, 1, 0, 0, 0, 7, 27, 1, 0, 0, 0, 9, 30, 1, 0, 0, 0, 11, 33, 1, 0, 0, 0, 13, 36, 1, 0, 0, 0, 15, 40, 1, 0, 0, 0, 17, 51, 1, 0, 0, 0, 19, 20, 5, 78, 0, 0, 20, 21, 5, 79, 0, 0, 21, 22, 5, 84, 0, 0, 22, 2, 1, 0, 0, 0, 23, 24, 5, 40, 0, 0, 24, 4, 1, 0, 0, 0, 25, 26, 5, 41, 0, 0, 26, 6, 1, 0, 0, 0, 27, 28, 5, 61, 0, 0, 28, 29, 5, 62, 0, 0, 29, 8, 1, 0, 0, 0, 30, 31, 5, 38, 0, 0, 31, 32, 5, 38, 0, 0, 32, 10, 1, 0, 0, 0, 33, 34, 5, 124, 0, 0, 34, 35, 5, 124, 0, 0, 35, 12, 1, 0, 0, 0, 36, 37, 5, 45, 0, 0, 37, 38, 5, 62, 0, 0, 38, 14, 1, 0, 0, 0, 39, 41, 7, 0, 0, 0, 40, 39, 1, 0, 0, 0, 41, 42, 1, 0, 0, 0, 42, 40, 1, 0, 0, 0, 42, 43, 1, 0, 0, 0, 43, 47, 1, 0, 0, 0, 44, 46, 7, 1, 0, 0, 45, 44, 1, 0, 0, 0, 46, 49, 1, 0, 0, 0, 47, 45, 1, 0, 0, 0, 47, 48, 1, 0, 0, 0, 48, 16, 1, 0, 0, 0, 49, 47, 1, 0, 0, 0, 50, 52, 7, 2, 0, 0, 51, 50, 1, 0, 0, 0, 52, 53, 1, 0, 0, 0, 53, 51, 1, 0, 0, 0, 53, 54, 1, 0, 0, 0, 54, 55, 1, 0, 0, 0, 55, 56, 6, 8, 0, 0, 56, 18, 1, 0, 0, 0, 4, 0, 42, 47, 53, 1, 6, 0, 0]

View File

@ -0,0 +1,16 @@
T__0=1
T__1=2
T__2=3
T__3=4
T__4=5
T__5=6
T__6=7
IDENTIFIER=8
WS=9
'NOT'=1
'('=2
')'=3
'=>'=4
'&&'=5
'||'=6
'->'=7

View File

@ -0,0 +1,92 @@
// Generated from ./TraceOperatorGrammar.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
ATNDeserializer,
CharStream,
DecisionState, DFA,
Lexer,
LexerATNSimulator,
RuleContext,
PredictionContextCache,
Token
} from "antlr4";
export default class TraceOperatorGrammarLexer extends Lexer {
public static readonly T__0 = 1;
public static readonly T__1 = 2;
public static readonly T__2 = 3;
public static readonly T__3 = 4;
public static readonly T__4 = 5;
public static readonly T__5 = 6;
public static readonly T__6 = 7;
public static readonly IDENTIFIER = 8;
public static readonly WS = 9;
public static readonly EOF = Token.EOF;
public static readonly channelNames: string[] = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];
public static readonly literalNames: (string | null)[] = [ null, "'NOT'",
"'('", "')'",
"'=>'", "'&&'",
"'||'", "'->'" ];
public static readonly symbolicNames: (string | null)[] = [ null, null,
null, null,
null, null,
null, null,
"IDENTIFIER",
"WS" ];
public static readonly modeNames: string[] = [ "DEFAULT_MODE", ];
public static readonly ruleNames: string[] = [
"T__0", "T__1", "T__2", "T__3", "T__4", "T__5", "T__6", "IDENTIFIER",
"WS",
];
constructor(input: CharStream) {
super(input);
this._interp = new LexerATNSimulator(this, TraceOperatorGrammarLexer._ATN, TraceOperatorGrammarLexer.DecisionsToDFA, new PredictionContextCache());
}
public get grammarFileName(): string { return "TraceOperatorGrammar.g4"; }
public get literalNames(): (string | null)[] { return TraceOperatorGrammarLexer.literalNames; }
public get symbolicNames(): (string | null)[] { return TraceOperatorGrammarLexer.symbolicNames; }
public get ruleNames(): string[] { return TraceOperatorGrammarLexer.ruleNames; }
public get serializedATN(): number[] { return TraceOperatorGrammarLexer._serializedATN; }
public get channelNames(): string[] { return TraceOperatorGrammarLexer.channelNames; }
public get modeNames(): string[] { return TraceOperatorGrammarLexer.modeNames; }
public static readonly _serializedATN: number[] = [4,0,9,57,6,-1,2,0,7,
0,2,1,7,1,2,2,7,2,2,3,7,3,2,4,7,4,2,5,7,5,2,6,7,6,2,7,7,7,2,8,7,8,1,0,1,
0,1,0,1,0,1,1,1,1,1,2,1,2,1,3,1,3,1,3,1,4,1,4,1,4,1,5,1,5,1,5,1,6,1,6,1,
6,1,7,4,7,41,8,7,11,7,12,7,42,1,7,5,7,46,8,7,10,7,12,7,49,9,7,1,8,4,8,52,
8,8,11,8,12,8,53,1,8,1,8,0,0,9,1,1,3,2,5,3,7,4,9,5,11,6,13,7,15,8,17,9,
1,0,3,2,0,65,90,97,122,1,0,48,57,3,0,9,10,13,13,32,32,59,0,1,1,0,0,0,0,
3,1,0,0,0,0,5,1,0,0,0,0,7,1,0,0,0,0,9,1,0,0,0,0,11,1,0,0,0,0,13,1,0,0,0,
0,15,1,0,0,0,0,17,1,0,0,0,1,19,1,0,0,0,3,23,1,0,0,0,5,25,1,0,0,0,7,27,1,
0,0,0,9,30,1,0,0,0,11,33,1,0,0,0,13,36,1,0,0,0,15,40,1,0,0,0,17,51,1,0,
0,0,19,20,5,78,0,0,20,21,5,79,0,0,21,22,5,84,0,0,22,2,1,0,0,0,23,24,5,40,
0,0,24,4,1,0,0,0,25,26,5,41,0,0,26,6,1,0,0,0,27,28,5,61,0,0,28,29,5,62,
0,0,29,8,1,0,0,0,30,31,5,38,0,0,31,32,5,38,0,0,32,10,1,0,0,0,33,34,5,124,
0,0,34,35,5,124,0,0,35,12,1,0,0,0,36,37,5,45,0,0,37,38,5,62,0,0,38,14,1,
0,0,0,39,41,7,0,0,0,40,39,1,0,0,0,41,42,1,0,0,0,42,40,1,0,0,0,42,43,1,0,
0,0,43,47,1,0,0,0,44,46,7,1,0,0,45,44,1,0,0,0,46,49,1,0,0,0,47,45,1,0,0,
0,47,48,1,0,0,0,48,16,1,0,0,0,49,47,1,0,0,0,50,52,7,2,0,0,51,50,1,0,0,0,
52,53,1,0,0,0,53,51,1,0,0,0,53,54,1,0,0,0,54,55,1,0,0,0,55,56,6,8,0,0,56,
18,1,0,0,0,4,0,42,47,53,1,6,0,0];
private static __ATN: ATN;
public static get _ATN(): ATN {
if (!TraceOperatorGrammarLexer.__ATN) {
TraceOperatorGrammarLexer.__ATN = new ATNDeserializer().deserialize(TraceOperatorGrammarLexer._serializedATN);
}
return TraceOperatorGrammarLexer.__ATN;
}
static DecisionsToDFA = TraceOperatorGrammarLexer._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) );
}

View File

@ -0,0 +1,58 @@
// Generated from ./TraceOperatorGrammar.g4 by ANTLR 4.13.1
import {ParseTreeListener} from "antlr4";
import { QueryContext } from "./TraceOperatorGrammarParser";
import { ExpressionContext } from "./TraceOperatorGrammarParser";
import { AtomContext } from "./TraceOperatorGrammarParser";
import { OperatorContext } from "./TraceOperatorGrammarParser";
/**
* This interface defines a complete listener for a parse tree produced by
* `TraceOperatorGrammarParser`.
*/
export default class TraceOperatorGrammarListener extends ParseTreeListener {
/**
* Enter a parse tree produced by `TraceOperatorGrammarParser.query`.
* @param ctx the parse tree
*/
enterQuery?: (ctx: QueryContext) => void;
/**
* Exit a parse tree produced by `TraceOperatorGrammarParser.query`.
* @param ctx the parse tree
*/
exitQuery?: (ctx: QueryContext) => void;
/**
* Enter a parse tree produced by `TraceOperatorGrammarParser.expression`.
* @param ctx the parse tree
*/
enterExpression?: (ctx: ExpressionContext) => void;
/**
* Exit a parse tree produced by `TraceOperatorGrammarParser.expression`.
* @param ctx the parse tree
*/
exitExpression?: (ctx: ExpressionContext) => void;
/**
* Enter a parse tree produced by `TraceOperatorGrammarParser.atom`.
* @param ctx the parse tree
*/
enterAtom?: (ctx: AtomContext) => void;
/**
* Exit a parse tree produced by `TraceOperatorGrammarParser.atom`.
* @param ctx the parse tree
*/
exitAtom?: (ctx: AtomContext) => void;
/**
* Enter a parse tree produced by `TraceOperatorGrammarParser.operator`.
* @param ctx the parse tree
*/
enterOperator?: (ctx: OperatorContext) => void;
/**
* Exit a parse tree produced by `TraceOperatorGrammarParser.operator`.
* @param ctx the parse tree
*/
exitOperator?: (ctx: OperatorContext) => void;
}

View File

@ -0,0 +1,423 @@
// Generated from ./TraceOperatorGrammar.g4 by ANTLR 4.13.1
// noinspection ES6UnusedImports,JSUnusedGlobalSymbols,JSUnusedLocalSymbols
import {
ATN,
ATNDeserializer, DecisionState, DFA, FailedPredicateException,
RecognitionException, NoViableAltException, BailErrorStrategy,
Parser, ParserATNSimulator,
RuleContext, ParserRuleContext, PredictionMode, PredictionContextCache,
TerminalNode, RuleNode,
Token, TokenStream,
Interval, IntervalSet
} from 'antlr4';
import TraceOperatorGrammarListener from "./TraceOperatorGrammarListener.js";
import TraceOperatorGrammarVisitor from "./TraceOperatorGrammarVisitor.js";
// for running tests with parameters, TODO: discuss strategy for typed parameters in CI
// eslint-disable-next-line no-unused-vars
type int = number;
export default class TraceOperatorGrammarParser extends Parser {
public static readonly T__0 = 1;
public static readonly T__1 = 2;
public static readonly T__2 = 3;
public static readonly T__3 = 4;
public static readonly T__4 = 5;
public static readonly T__5 = 6;
public static readonly T__6 = 7;
public static readonly IDENTIFIER = 8;
public static readonly WS = 9;
public static readonly EOF = Token.EOF;
public static readonly RULE_query = 0;
public static readonly RULE_expression = 1;
public static readonly RULE_atom = 2;
public static readonly RULE_operator = 3;
public static readonly literalNames: (string | null)[] = [ null, "'NOT'",
"'('", "')'",
"'=>'", "'&&'",
"'||'", "'->'" ];
public static readonly symbolicNames: (string | null)[] = [ null, null,
null, null,
null, null,
null, null,
"IDENTIFIER",
"WS" ];
// tslint:disable:no-trailing-whitespace
public static readonly ruleNames: string[] = [
"query", "expression", "atom", "operator",
];
public get grammarFileName(): string { return "TraceOperatorGrammar.g4"; }
public get literalNames(): (string | null)[] { return TraceOperatorGrammarParser.literalNames; }
public get symbolicNames(): (string | null)[] { return TraceOperatorGrammarParser.symbolicNames; }
public get ruleNames(): string[] { return TraceOperatorGrammarParser.ruleNames; }
public get serializedATN(): number[] { return TraceOperatorGrammarParser._serializedATN; }
protected createFailedPredicateException(predicate?: string, message?: string): FailedPredicateException {
return new FailedPredicateException(this, predicate, message);
}
constructor(input: TokenStream) {
super(input);
this._interp = new ParserATNSimulator(this, TraceOperatorGrammarParser._ATN, TraceOperatorGrammarParser.DecisionsToDFA, new PredictionContextCache());
}
// @RuleVersion(0)
public query(): QueryContext {
let localctx: QueryContext = new QueryContext(this, this._ctx, this.state);
this.enterRule(localctx, 0, TraceOperatorGrammarParser.RULE_query);
let _la: number;
try {
this.enterOuterAlt(localctx, 1);
{
this.state = 9;
this._errHandler.sync(this);
_la = this._input.LA(1);
do {
{
{
this.state = 8;
this.expression();
}
}
this.state = 11;
this._errHandler.sync(this);
_la = this._input.LA(1);
} while ((((_la) & ~0x1F) === 0 && ((1 << _la) & 262) !== 0));
this.state = 13;
this.match(TraceOperatorGrammarParser.EOF);
}
}
catch (re) {
if (re instanceof RecognitionException) {
localctx.exception = re;
this._errHandler.reportError(this, re);
this._errHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localctx;
}
// @RuleVersion(0)
public expression(): ExpressionContext {
let localctx: ExpressionContext = new ExpressionContext(this, this._ctx, this.state);
this.enterRule(localctx, 2, TraceOperatorGrammarParser.RULE_expression);
try {
this.state = 38;
this._errHandler.sync(this);
switch ( this._interp.adaptivePredict(this._input, 1, this._ctx) ) {
case 1:
this.enterOuterAlt(localctx, 1);
{
this.state = 15;
this.match(TraceOperatorGrammarParser.T__0);
this.state = 16;
this.expression();
}
break;
case 2:
this.enterOuterAlt(localctx, 2);
{
this.state = 17;
this.match(TraceOperatorGrammarParser.T__1);
this.state = 18;
this.expression();
this.state = 19;
this.match(TraceOperatorGrammarParser.T__2);
this.state = 20;
this.operator();
this.state = 21;
this.expression();
}
break;
case 3:
this.enterOuterAlt(localctx, 3);
{
this.state = 23;
this.match(TraceOperatorGrammarParser.T__1);
this.state = 24;
this.expression();
this.state = 25;
this.match(TraceOperatorGrammarParser.T__2);
}
break;
case 4:
this.enterOuterAlt(localctx, 4);
{
this.state = 27;
localctx._left = this.atom();
this.state = 28;
this.operator();
this.state = 29;
localctx._right = this.expression();
}
break;
case 5:
this.enterOuterAlt(localctx, 5);
{
this.state = 31;
localctx._left = this.atom();
this.state = 32;
this.operator();
this.state = 33;
this.match(TraceOperatorGrammarParser.T__1);
this.state = 34;
localctx._expr = this.expression();
this.state = 35;
this.match(TraceOperatorGrammarParser.T__2);
}
break;
case 6:
this.enterOuterAlt(localctx, 6);
{
this.state = 37;
this.atom();
}
break;
}
}
catch (re) {
if (re instanceof RecognitionException) {
localctx.exception = re;
this._errHandler.reportError(this, re);
this._errHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localctx;
}
// @RuleVersion(0)
public atom(): AtomContext {
let localctx: AtomContext = new AtomContext(this, this._ctx, this.state);
this.enterRule(localctx, 4, TraceOperatorGrammarParser.RULE_atom);
try {
this.enterOuterAlt(localctx, 1);
{
this.state = 40;
this.match(TraceOperatorGrammarParser.IDENTIFIER);
}
}
catch (re) {
if (re instanceof RecognitionException) {
localctx.exception = re;
this._errHandler.reportError(this, re);
this._errHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localctx;
}
// @RuleVersion(0)
public operator(): OperatorContext {
let localctx: OperatorContext = new OperatorContext(this, this._ctx, this.state);
this.enterRule(localctx, 6, TraceOperatorGrammarParser.RULE_operator);
let _la: number;
try {
this.enterOuterAlt(localctx, 1);
{
this.state = 42;
_la = this._input.LA(1);
if(!((((_la) & ~0x1F) === 0 && ((1 << _la) & 242) !== 0))) {
this._errHandler.recoverInline(this);
}
else {
this._errHandler.reportMatch(this);
this.consume();
}
}
}
catch (re) {
if (re instanceof RecognitionException) {
localctx.exception = re;
this._errHandler.reportError(this, re);
this._errHandler.recover(this, re);
} else {
throw re;
}
}
finally {
this.exitRule();
}
return localctx;
}
public static readonly _serializedATN: number[] = [4,1,9,45,2,0,7,0,2,1,
7,1,2,2,7,2,2,3,7,3,1,0,4,0,10,8,0,11,0,12,0,11,1,0,1,0,1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,
1,1,3,1,39,8,1,1,2,1,2,1,3,1,3,1,3,0,0,4,0,2,4,6,0,1,2,0,1,1,4,7,46,0,9,
1,0,0,0,2,38,1,0,0,0,4,40,1,0,0,0,6,42,1,0,0,0,8,10,3,2,1,0,9,8,1,0,0,0,
10,11,1,0,0,0,11,9,1,0,0,0,11,12,1,0,0,0,12,13,1,0,0,0,13,14,5,0,0,1,14,
1,1,0,0,0,15,16,5,1,0,0,16,39,3,2,1,0,17,18,5,2,0,0,18,19,3,2,1,0,19,20,
5,3,0,0,20,21,3,6,3,0,21,22,3,2,1,0,22,39,1,0,0,0,23,24,5,2,0,0,24,25,3,
2,1,0,25,26,5,3,0,0,26,39,1,0,0,0,27,28,3,4,2,0,28,29,3,6,3,0,29,30,3,2,
1,0,30,39,1,0,0,0,31,32,3,4,2,0,32,33,3,6,3,0,33,34,5,2,0,0,34,35,3,2,1,
0,35,36,5,3,0,0,36,39,1,0,0,0,37,39,3,4,2,0,38,15,1,0,0,0,38,17,1,0,0,0,
38,23,1,0,0,0,38,27,1,0,0,0,38,31,1,0,0,0,38,37,1,0,0,0,39,3,1,0,0,0,40,
41,5,8,0,0,41,5,1,0,0,0,42,43,7,0,0,0,43,7,1,0,0,0,2,11,38];
private static __ATN: ATN;
public static get _ATN(): ATN {
if (!TraceOperatorGrammarParser.__ATN) {
TraceOperatorGrammarParser.__ATN = new ATNDeserializer().deserialize(TraceOperatorGrammarParser._serializedATN);
}
return TraceOperatorGrammarParser.__ATN;
}
static DecisionsToDFA = TraceOperatorGrammarParser._ATN.decisionToState.map( (ds: DecisionState, index: number) => new DFA(ds, index) );
}
export class QueryContext extends ParserRuleContext {
constructor(parser?: TraceOperatorGrammarParser, parent?: ParserRuleContext, invokingState?: number) {
super(parent, invokingState);
this.parser = parser;
}
public EOF(): TerminalNode {
return this.getToken(TraceOperatorGrammarParser.EOF, 0);
}
public expression_list(): ExpressionContext[] {
return this.getTypedRuleContexts(ExpressionContext) as ExpressionContext[];
}
public expression(i: number): ExpressionContext {
return this.getTypedRuleContext(ExpressionContext, i) as ExpressionContext;
}
public get ruleIndex(): number {
return TraceOperatorGrammarParser.RULE_query;
}
public enterRule(listener: TraceOperatorGrammarListener): void {
if(listener.enterQuery) {
listener.enterQuery(this);
}
}
public exitRule(listener: TraceOperatorGrammarListener): void {
if(listener.exitQuery) {
listener.exitQuery(this);
}
}
// @Override
public accept<Result>(visitor: TraceOperatorGrammarVisitor<Result>): Result {
if (visitor.visitQuery) {
return visitor.visitQuery(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class ExpressionContext extends ParserRuleContext {
public _left!: AtomContext;
public _right!: ExpressionContext;
public _expr!: ExpressionContext;
constructor(parser?: TraceOperatorGrammarParser, parent?: ParserRuleContext, invokingState?: number) {
super(parent, invokingState);
this.parser = parser;
}
public expression_list(): ExpressionContext[] {
return this.getTypedRuleContexts(ExpressionContext) as ExpressionContext[];
}
public expression(i: number): ExpressionContext {
return this.getTypedRuleContext(ExpressionContext, i) as ExpressionContext;
}
public operator(): OperatorContext {
return this.getTypedRuleContext(OperatorContext, 0) as OperatorContext;
}
public atom(): AtomContext {
return this.getTypedRuleContext(AtomContext, 0) as AtomContext;
}
public get ruleIndex(): number {
return TraceOperatorGrammarParser.RULE_expression;
}
public enterRule(listener: TraceOperatorGrammarListener): void {
if(listener.enterExpression) {
listener.enterExpression(this);
}
}
public exitRule(listener: TraceOperatorGrammarListener): void {
if(listener.exitExpression) {
listener.exitExpression(this);
}
}
// @Override
public accept<Result>(visitor: TraceOperatorGrammarVisitor<Result>): Result {
if (visitor.visitExpression) {
return visitor.visitExpression(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class AtomContext extends ParserRuleContext {
constructor(parser?: TraceOperatorGrammarParser, parent?: ParserRuleContext, invokingState?: number) {
super(parent, invokingState);
this.parser = parser;
}
public IDENTIFIER(): TerminalNode {
return this.getToken(TraceOperatorGrammarParser.IDENTIFIER, 0);
}
public get ruleIndex(): number {
return TraceOperatorGrammarParser.RULE_atom;
}
public enterRule(listener: TraceOperatorGrammarListener): void {
if(listener.enterAtom) {
listener.enterAtom(this);
}
}
public exitRule(listener: TraceOperatorGrammarListener): void {
if(listener.exitAtom) {
listener.exitAtom(this);
}
}
// @Override
public accept<Result>(visitor: TraceOperatorGrammarVisitor<Result>): Result {
if (visitor.visitAtom) {
return visitor.visitAtom(this);
} else {
return visitor.visitChildren(this);
}
}
}
export class OperatorContext extends ParserRuleContext {
constructor(parser?: TraceOperatorGrammarParser, parent?: ParserRuleContext, invokingState?: number) {
super(parent, invokingState);
this.parser = parser;
}
public get ruleIndex(): number {
return TraceOperatorGrammarParser.RULE_operator;
}
public enterRule(listener: TraceOperatorGrammarListener): void {
if(listener.enterOperator) {
listener.enterOperator(this);
}
}
public exitRule(listener: TraceOperatorGrammarListener): void {
if(listener.exitOperator) {
listener.exitOperator(this);
}
}
// @Override
public accept<Result>(visitor: TraceOperatorGrammarVisitor<Result>): Result {
if (visitor.visitOperator) {
return visitor.visitOperator(this);
} else {
return visitor.visitChildren(this);
}
}
}

View File

@ -0,0 +1,45 @@
// Generated from ./TraceOperatorGrammar.g4 by ANTLR 4.13.1
import {ParseTreeVisitor} from 'antlr4';
import { QueryContext } from "./TraceOperatorGrammarParser";
import { ExpressionContext } from "./TraceOperatorGrammarParser";
import { AtomContext } from "./TraceOperatorGrammarParser";
import { OperatorContext } from "./TraceOperatorGrammarParser";
/**
* This interface defines a complete generic visitor for a parse tree produced
* by `TraceOperatorGrammarParser`.
*
* @param <Result> The return type of the visit operation. Use `void` for
* operations with no return type.
*/
export default class TraceOperatorGrammarVisitor<Result> extends ParseTreeVisitor<Result> {
/**
* Visit a parse tree produced by `TraceOperatorGrammarParser.query`.
* @param ctx the parse tree
* @return the visitor result
*/
visitQuery?: (ctx: QueryContext) => Result;
/**
* Visit a parse tree produced by `TraceOperatorGrammarParser.expression`.
* @param ctx the parse tree
* @return the visitor result
*/
visitExpression?: (ctx: ExpressionContext) => Result;
/**
* Visit a parse tree produced by `TraceOperatorGrammarParser.atom`.
* @param ctx the parse tree
* @return the visitor result
*/
visitAtom?: (ctx: AtomContext) => Result;
/**
* Visit a parse tree produced by `TraceOperatorGrammarParser.operator`.
* @param ctx the parse tree
* @return the visitor result
*/
visitOperator?: (ctx: OperatorContext) => Result;
}

View File

@ -7,6 +7,7 @@ import {
initialClickHouseData,
initialFormulaBuilderFormValues,
initialQueriesMap,
initialQueryBuilderFormTraceOperatorValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
initialQueryState,
@ -14,6 +15,7 @@ import {
MAX_FORMULAS,
MAX_QUERIES,
PANEL_TYPES,
TRACE_OPERATOR_QUERY_NAME,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
@ -45,6 +47,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -72,14 +75,18 @@ export const QueryBuilderContext = createContext<QueryBuilderContextType>({
panelType: PANEL_TYPES.TIME_SERIES,
isEnabledQuery: false,
handleSetQueryData: () => {},
handleSetTraceOperatorData: () => {},
handleSetFormulaData: () => {},
handleSetQueryItemData: () => {},
handleSetConfig: () => {},
removeQueryBuilderEntityByIndex: () => {},
removeAllQueryBuilderEntities: () => {},
removeQueryTypeItemByIndex: () => {},
addNewBuilderQuery: () => {},
cloneQuery: () => {},
addNewFormula: () => {},
addTraceOperator: () => {},
removeTraceOperator: () => {},
addNewQueryItem: () => {},
redirectWithQueryBuilderData: () => {},
handleRunQuery: () => {},
@ -166,6 +173,10 @@ export function QueryBuilderProvider({
...initialFormulaBuilderFormValues,
...item,
})),
queryTraceOperator: query.builder.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})),
};
const setupedQueryData = builder.queryData.map((item) => {
@ -378,8 +389,11 @@ export function QueryBuilderProvider({
const removeQueryBuilderEntityByIndex = useCallback(
(type: keyof QueryBuilderData, index: number) => {
setCurrentQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@ -393,8 +407,11 @@ export function QueryBuilderProvider({
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const currentArray: (IBuilderQuery | IBuilderFormula)[] =
prevState.builder[type];
const currentArray: (
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
@ -410,6 +427,20 @@ export function QueryBuilderProvider({
[],
);
const removeAllQueryBuilderEntities = useCallback(
(type: keyof QueryBuilderData) => {
setCurrentQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
setSupersetQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
},
[setCurrentQuery, setSupersetQuery],
);
const removeQueryTypeItemByIndex = useCallback(
(type: EQueryType.PROM | EQueryType.CLICKHOUSE, index: number) => {
setCurrentQuery((prevState) => {
@ -632,6 +663,68 @@ export function QueryBuilderProvider({
});
}, [createNewBuilderFormula]);
const addTraceOperator = useCallback((expression = '') => {
const trimmed = (expression || '').trim();
setCurrentQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
}, []);
const removeTraceOperator = useCallback(() => {
setCurrentQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: [],
},
}));
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: [],
},
}));
}, []);
const updateQueryBuilderData: <T>(
arr: T[],
index: number,
@ -738,6 +831,44 @@ export function QueryBuilderProvider({
},
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
);
const handleSetTraceOperatorData = useCallback(
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
setCurrentQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
});
},
[updateQueryBuilderData],
);
const handleSetFormulaData = useCallback(
(index: number, formulaData: IBuilderFormula): void => {
setCurrentQuery((prevState) => {
@ -1020,14 +1151,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,
@ -1048,14 +1183,18 @@ export function QueryBuilderProvider({
panelType,
isEnabledQuery,
handleSetQueryData,
handleSetTraceOperatorData,
handleSetFormulaData,
handleSetQueryItemData,
handleSetConfig,
removeQueryBuilderEntityByIndex,
removeQueryTypeItemByIndex,
removeAllQueryBuilderEntities,
cloneQuery,
addNewBuilderQuery,
addNewFormula,
addTraceOperator,
removeTraceOperator,
addNewQueryItem,
redirectWithQueryBuilderData,
handleRunQuery,

View File

@ -29,6 +29,8 @@ export interface IBuilderFormula {
orderBy?: OrderByPayload[];
}
export type IBuilderTraceOperator = IBuilderQuery;
export interface TagFilterItem {
id: string;
key?: BaseAutocompleteData;
@ -118,12 +120,13 @@ export type BuilderClickHouseResource = Record<string, IClickHouseQuery>;
export type BuilderPromQLResource = Record<string, IPromQLQuery>;
export type BuilderQueryDataResourse = Record<
string,
IBuilderQuery | IBuilderFormula
IBuilderQuery | IBuilderFormula | IBuilderTraceOperator
>;
export type MapData =
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
| IClickHouseQuery
| IPromQLQuery;

View File

@ -14,6 +14,7 @@ export type RequestType =
export type QueryType =
| 'builder_query'
| 'builder_trace_operator'
| 'builder_formula'
| 'builder_sub_query'
| 'builder_join'
@ -220,6 +221,7 @@ export interface BaseBuilderQuery {
secondaryAggregations?: SecondaryAggregation[];
functions?: QueryFunction[];
legend?: string;
expression?: string; // for trace operator
}
export interface TraceBuilderQuery extends BaseBuilderQuery {

View File

@ -4,6 +4,7 @@ import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteRe
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import {
BaseBuilderQuery,
@ -18,6 +19,7 @@ import { SelectOption } from './select';
type UseQueryOperationsParams = Pick<QueryProps, 'index' | 'query'> &
Pick<QueryBuilderProps, 'filterConfigs'> & {
isForTraceOperator?: boolean;
formula?: IBuilderFormula;
isListViewPanel?: boolean;
entityVersion: string;
@ -32,6 +34,14 @@ export type HandleChangeQueryData<T = IBuilderQuery> = <
value: Value,
) => void;
export type HandleChangeTraceOperatorData<T = IBuilderTraceOperator> = <
Key extends keyof T,
Value extends T[Key]
>(
key: Key,
value: Value,
) => void;
// Legacy version for backward compatibility
export type HandleChangeQueryDataLegacy = HandleChangeQueryData<IBuilderQuery>;

View File

@ -6,6 +6,7 @@ import { Dispatch, SetStateAction } from 'react';
import {
IBuilderFormula,
IBuilderQuery,
IBuilderTraceOperator,
IClickHouseQuery,
IPromQLQuery,
Query,
@ -222,6 +223,7 @@ export type ReduceOperators = 'last' | 'sum' | 'avg' | 'max' | 'min';
export type QueryBuilderData = {
queryData: IBuilderQuery[];
queryFormulas: IBuilderFormula[];
queryTraceOperator: IBuilderTraceOperator[];
};
export type QueryBuilderContextType = {
@ -235,6 +237,10 @@ export type QueryBuilderContextType = {
panelType: PANEL_TYPES | null;
isEnabledQuery: boolean;
handleSetQueryData: (index: number, queryData: IBuilderQuery) => void;
handleSetTraceOperatorData: (
index: number,
traceOperatorData: IBuilderTraceOperator,
) => void;
handleSetFormulaData: (index: number, formulaData: IBuilderFormula) => void;
handleSetQueryItemData: (
index: number,
@ -249,12 +255,15 @@ export type QueryBuilderContextType = {
type: keyof QueryBuilderData,
index: number,
) => void;
removeAllQueryBuilderEntities: (type: keyof QueryBuilderData) => void;
removeQueryTypeItemByIndex: (
type: EQueryType.PROM | EQueryType.CLICKHOUSE,
index: number,
) => void;
addNewBuilderQuery: () => void;
addNewFormula: () => void;
removeTraceOperator: () => void;
addTraceOperator: (expression?: string) => void;
cloneQuery: (type: string, query: IBuilderQuery) => void;
addNewQueryItem: (type: EQueryType.PROM | EQueryType.CLICKHOUSE) => void;
redirectWithQueryBuilderData: (

View File

@ -2,10 +2,15 @@ import {
convertBuilderQueriesToV5,
convertClickHouseQueriesToV5,
convertPromQueriesToV5,
convertTraceOperatorToV5,
mapPanelTypeToRequestType,
} from 'api/v5/queryRange/prepareQueryRangePayloadV5';
import { TRACE_OPERATOR_QUERY_NAME } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { BuilderQueryDataResourse } from 'types/api/queryBuilder/queryBuilderData';
import {
BuilderQueryDataResourse,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { OrderBy, QueryEnvelope } from 'types/api/v5/queryRange';
function convertFormulasToV5(
@ -46,9 +51,12 @@ export function compositeQueryToQueryEnvelope(
const regularQueries: BuilderQueryDataResourse = {};
const formulaQueries: BuilderQueryDataResourse = {};
const traceOperatorQueries: BuilderQueryDataResourse = {};
Object.entries(builderQueries || {}).forEach(([queryName, queryData]) => {
if ('dataSource' in queryData) {
if (queryData.queryName === TRACE_OPERATOR_QUERY_NAME) {
traceOperatorQueries[queryName] = queryData;
} else if ('dataSource' in queryData) {
regularQueries[queryName] = queryData;
} else {
formulaQueries[queryName] = queryData;
@ -64,6 +72,12 @@ export function compositeQueryToQueryEnvelope(
);
const formulaQueriesV5 = convertFormulasToV5(formulaQueries);
const traceOperatorQueriesV5 = convertTraceOperatorToV5(
traceOperatorQueries as Record<string, IBuilderTraceOperator>,
requestType,
panelType,
);
const promQueriesV5 = convertPromQueriesToV5(promQueries || {});
const chQueriesV5 = convertClickHouseQueriesToV5(chQueries || {});
@ -72,7 +86,11 @@ export function compositeQueryToQueryEnvelope(
switch (queryType) {
case 'builder':
queries = [...builderQueriesV5, ...formulaQueriesV5];
queries = [
...builderQueriesV5,
...formulaQueriesV5,
...traceOperatorQueriesV5,
];
break;
case 'promql':
queries = [...promQueriesV5];
@ -85,6 +103,7 @@ export function compositeQueryToQueryEnvelope(
queries = [
...builderQueriesV5,
...formulaQueriesV5,
...traceOperatorQueriesV5,
...promQueriesV5,
...chQueriesV5,
];

View File

@ -3,6 +3,8 @@
import { CharStreams, CommonTokenStream } from 'antlr4';
import FilterQueryLexer from 'parser/FilterQueryLexer';
import FilterQueryParser from 'parser/FilterQueryParser';
import TraceOperatorGrammarLexer from 'parser/TraceOperatorParser/TraceOperatorGrammarLexer';
import TraceOperatorGrammarParser from 'parser/TraceOperatorParser/TraceOperatorGrammarParser';
import { IDetailedError, IValidationResult } from 'types/antlrQueryTypes';
// Custom error listener to capture ANTLR errors
@ -169,3 +171,66 @@ export const validateQuery = (query: string): IValidationResult => {
};
}
};
export const validateTraceOperatorQuery = (
query: string,
): IValidationResult => {
// Empty query is considered valid
if (!query.trim()) {
return {
isValid: true,
message: 'Trace operator query is empty',
errors: [],
};
}
try {
const errorListener = new QueryErrorListener();
const inputStream = CharStreams.fromString(query);
// Setup lexer
const lexer = new TraceOperatorGrammarLexer(inputStream);
lexer.removeErrorListeners(); // Remove default error listeners
lexer.addErrorListener(errorListener);
// Setup parser
const tokenStream = new CommonTokenStream(lexer);
const parser = new TraceOperatorGrammarParser(tokenStream);
parser.removeErrorListeners(); // Remove default error listeners
parser.addErrorListener(errorListener);
// Try parsing
parser.query();
// Check if any errors were captured
if (errorListener.hasErrors()) {
return {
isValid: false,
message: 'Trace operator syntax error',
errors: errorListener.getErrors(),
};
}
return {
isValid: true,
message: 'Trace operator is valid!',
errors: [],
};
} catch (error) {
const errorMessage =
error instanceof Error ? error.message : 'Invalid trace operator syntax';
const detailedError: IDetailedError = {
message: errorMessage,
line: 0,
column: 0,
offendingSymbol: '',
expectedTokens: [],
};
return {
isValid: false,
message: 'Invalid trace operator syntax',
errors: [detailedError],
};
}
};

View File

@ -31,7 +31,7 @@
],
"types": ["node", "jest"]
},
"exclude": ["node_modules", "src/parser/*.ts"],
"exclude": ["node_modules", "src/parser/*.ts", "src/parser/TraceOperatorParser/*.ts"],
"include": [
"./src",
"./src/**/*.ts",

View File

@ -4276,6 +4276,20 @@
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/callout@0.0.2":
version "0.0.2"
resolved "https://registry.yarnpkg.com/@signozhq/callout/-/callout-0.0.2.tgz#131ca15f89a8ee6729fecc4d322f11359c02e5cf"
integrity sha512-tmguHm+/JVRKjMElJOFyG7LJcdqCW1hHnFfp8ZkjQ+Gi7MfFt/r2foLZG2DNdOcfxSvhf2zhzr7D+epgvmbQ1A==
dependencies:
"@radix-ui/react-icons" "^1.3.0"
"@radix-ui/react-slot" "^1.1.0"
class-variance-authority "^0.7.0"
clsx "^2.1.1"
lucide-react "^0.445.0"
lucide-solid "^0.510.0"
tailwind-merge "^2.5.2"
tailwindcss-animate "^1.0.7"
"@signozhq/design-tokens@1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@signozhq/design-tokens/-/design-tokens-1.1.4.tgz#5d5de5bd9d19b6a3631383db015cc4b70c3f7661"
@ -12370,6 +12384,11 @@ lucide-react@^0.445.0:
resolved "https://registry.yarnpkg.com/lucide-react/-/lucide-react-0.445.0.tgz#35c42341e98fbf0475b2a6cf74fd25ef7cbfcd62"
integrity sha512-YrLf3aAHvmd4dZ8ot+mMdNFrFpJD7YRwQ2pUcBhgqbmxtrMP4xDzIorcj+8y+6kpuXBF4JB0NOCTUWIYetJjgA==
lucide-solid@^0.510.0:
version "0.510.0"
resolved "https://registry.yarnpkg.com/lucide-solid/-/lucide-solid-0.510.0.tgz#f5b17397ef1df3017f62f96f4d00e080abfb492f"
integrity sha512-G6rKYxURfSLG/zeOCN/BEl2dq2ezujFKPbcHjl7RLJ4bBQwWk4ZF2Swga/8anWglSVZyqYz7HMrrpb8/+vOcXw==
lz-string@^1.4.4:
version "1.5.0"
resolved "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz"

View File

@ -0,0 +1,39 @@
grammar TraceOperatorGrammar;
// Entry point of the grammar (the root of the parse tree)
query : expression+ EOF;
// Expression rules
expression
: 'NOT' expression // NOT prefix expression
| '(' expression ')' operator expression // Parenthesized operator expression
| '(' expression ')' // Parenthesized expression
| left=atom operator right=expression // Binary operator with expression on right
| left=atom operator '(' expr=expression ')' // Expression with parentheses inside
| atom // Simple expression (atom)
;
// Atom definition: atoms are identifiers (letters and optional numbers)
atom
: IDENTIFIER // General atom (combination of letters and numbers)
;
// Operator definition
operator
: '=>' // Implication
| '&&' // AND
| '||' // OR
| 'NOT' // NOT
| '->' // Implication
;
// Lexer rules
// IDENTIFIER can be a sequence of letters followed by optional numbers
IDENTIFIER
: [a-zA-Z]+[0-9]* // Letters followed by optional numbers (e.g., A1, B123, C99)
;
// Whitespace (to be skipped)
WS
: [ \t\r\n]+ -> skip; // Skip whitespace