Compare commits

..

6 Commits

Author SHA1 Message Date
Naman Verma
62d477e920 Merge branch 'main' into nv/exec-stats 2026-11-09 12:20:27 +05:30
Naman Verma
fe45b9dcae Merge branch 'main' into nv/exec-stats 2026-11-06 00:51:46 +05:30
Naman Verma
63ee60a40c fix(querier): stop cache refreshes from inflating exec stats 2026-11-06 00:51:03 +05:30
Naman Verma
29a393e47e Merge branch 'main' into nv/exec-stats 2026-11-05 10:51:11 +05:30
Naman Verma
c58f76f998 Merge branch 'main' into nv/exec-stats 2026-11-03 11:34:10 +05:30
Naman Verma
e39ad9e221 test: check how exec stats fail 2026-11-02 22:24:22 +05:30
121 changed files with 1605 additions and 2186 deletions

View File

@@ -903,40 +903,6 @@ describe('prepareQueryRangePayloadV5', () => {
const logSpec = builderQuery.spec as LogBuilderQuery;
expect(logSpec.filter).toStrictEqual({ expression: '' });
});
it('sends the AI slot as builder_ai_query envelopes, ignoring builder', () => {
const props: GetQueryResultsProps = {
query: {
queryType: EQueryType.AI_QUERY_BUILDER,
id: 'q-ai',
unit: undefined,
promql: [],
clickhouse_sql: [],
builder: {
queryData: [baseBuilderQuery({ queryName: 'M' })],
queryFormulas: [],
queryTraceOperator: [],
},
builder_ai_query: {
queryData: [
baseBuilderQuery({ queryName: 'A', dataSource: DataSource.TRACES }),
],
queryFormulas: [],
queryTraceOperator: [],
},
},
graphType: PANEL_TYPES.TIME_SERIES,
selectedTime: 'GLOBAL_TIME',
start,
end,
};
const { queries } =
prepareQueryRangePayloadV5(props).queryPayload.compositeQuery;
expect(
queries.map((q) => [q.type, (q.spec as { name: string }).name]),
).toStrictEqual([['builder_ai_query', 'A']]);
});
});
describe('mapPanelTypeToRequestType', () => {

View File

@@ -3,7 +3,6 @@
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
import { toWireQuery } from 'lib/query/builderSlot';
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
import { isEmpty } from 'lodash-es';
@@ -560,15 +559,9 @@ export const prepareQueryRangePayloadV5 = (
const requestType = mapPanelTypeToRequestType(graphType);
let queries: QueryEnvelope[] = [];
const wireQuery = toWireQuery(query);
switch (wireQuery.queryType) {
switch (query.queryType) {
case EQueryType.QUERY_BUILDER: {
const {
queryData: data,
queryFormulas,
queryTraceOperator,
} = wireQuery.builder;
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
@@ -630,13 +623,13 @@ export const prepareQueryRangePayloadV5 = (
break;
}
case EQueryType.PROM: {
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
const promQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertPromQueriesToV5(promQueries.queries);
legendMap = promQueries.legends;
break;
}
case EQueryType.CLICKHOUSE: {
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
const chQueries = reduceQueriesToObject(query[query.queryType]);
queries = convertClickHouseQueriesToV5(chQueries.queries);
legendMap = chQueries.legends;
break;

View File

@@ -1,5 +1,4 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
@@ -50,7 +49,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
initialDataSource,
handleRunQuery,
} = useQueryBuilder();
const activeBuilder = getActiveBuilder(currentQuery);
const containerRef = useRef(null);
@@ -131,28 +129,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
const renderedQueries = useMemo(
() =>
isMultiQueryAllowed
? activeBuilder.queryData
: activeBuilder.queryData.slice(0, 1),
[isMultiQueryAllowed, activeBuilder.queryData],
? currentQuery.builder.queryData
: currentQuery.builder.queryData.slice(0, 1),
[isMultiQueryAllowed, currentQuery.builder.queryData],
);
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
if (
activeBuilder.queryTraceOperator &&
activeBuilder.queryTraceOperator.length > 0
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0
) {
return activeBuilder.queryTraceOperator[0];
return currentQuery.builder.queryTraceOperator[0];
}
return undefined;
}, [activeBuilder.queryTraceOperator]);
}, [currentQuery.builder.queryTraceOperator]);
const hasAtLeastOneTraceQuery = useMemo(
() =>
activeBuilder.queryData.some(
currentQuery.builder.queryData.some(
(query) => query.dataSource === DataSource.TRACES,
),
[activeBuilder.queryData],
[currentQuery.builder.queryData],
);
const hasTraceOperator = useMemo(
@@ -227,9 +225,9 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
{!isMultiQueryAllowed ? (
<QueryV2
ref={containerRef}
key={activeBuilder.queryData[0].queryName}
key={currentQuery.builder.queryData[0].queryName}
index={0}
query={activeBuilder.queryData[0]}
query={currentQuery.builder.queryData[0]}
fieldsConfig={fieldsConfig}
allowedDataSources={queryDataSources}
isMultiQueryAllowed={isMultiQueryAllowed}
@@ -240,7 +238,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
queryVariant={config?.queryVariant || 'dropdown'}
showOnlyWhereClause={showOnlyWhereClause}
isRawQuery={isRawQuery}
signalSource={activeBuilder.queryData[0].source as 'meter' | ''}
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={1}
@@ -266,26 +264,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
signalSource={query.source as 'meter' | ''}
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
signalSourceChangeEnabled={signalSourceChangeEnabled}
queriesCount={activeBuilder.queryData.length}
queriesCount={currentQuery.builder.queryData.length}
savePreviousQuery={savePreviousQuery}
/>
))
)}
{!showOnlyWhereClause && activeBuilder.queryFormulas?.length > 0 && (
<div className="qb-formulas-container">
{activeBuilder.queryFormulas.map((formula, index) => {
const query =
activeBuilder.queryData[index] || activeBuilder.queryData[0];
{!showOnlyWhereClause &&
currentQuery.builder.queryFormulas?.length > 0 && (
<div className="qb-formulas-container">
{currentQuery.builder.queryFormulas.map((formula, index) => {
const query =
currentQuery.builder.queryData[index] ||
currentQuery.builder.queryData[0];
return (
<div key={formula.queryName} className="qb-formula">
<Formula query={query} formula={formula} index={index} isQBV2 />
</div>
);
})}
</div>
)}
return (
<div key={formula.queryName} className="qb-formula">
<Formula query={query} formula={formula} index={index} isQBV2 />
</div>
);
})}
</div>
)}
{shouldShowFooter && (
<QueryFooter
@@ -319,7 +319,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
</div>
))}
{activeBuilder.queryFormulas?.map((formula) => (
{currentQuery.builder.queryFormulas?.map((formula) => (
<div key={formula.queryName} className="formula-name">
{formula.queryName}
</div>

View File

@@ -1,5 +1,4 @@
import { useMemo } from 'react';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { Button, Tooltip } from 'antd';
import { Typography } from '@signozhq/ui/typography';
import WarningPopover from 'components/WarningPopover/WarningPopover';
@@ -16,25 +15,28 @@ function TraceOperatorSection({
addTraceOperator?: () => void;
}): JSX.Element {
const { currentQuery, panelType } = useQueryBuilder();
const activeBuilder = getActiveBuilder(currentQuery);
const showTraceOperatorWarning = useMemo(() => {
const isRawQueryPanel =
panelType === PANEL_TYPES.LIST || panelType === PANEL_TYPES.TRACE;
const hasMultipleQueries = activeBuilder.queryData.length > 1;
const hasMultipleQueries = currentQuery.builder.queryData.length > 1;
const hasTraceOperator =
activeBuilder.queryTraceOperator &&
activeBuilder.queryTraceOperator.length > 0;
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0;
return isRawQueryPanel && hasMultipleQueries && !hasTraceOperator;
}, [activeBuilder.queryData, activeBuilder.queryTraceOperator, panelType]);
}, [
currentQuery?.builder?.queryData,
currentQuery?.builder?.queryTraceOperator,
panelType,
]);
const traceOperatorWarning = useMemo(() => {
if (activeBuilder.queryData.length === 0) {
if (currentQuery.builder.queryData.length === 0) {
return '';
}
const firstQuery = activeBuilder.queryData[0];
const firstQuery = currentQuery.builder.queryData[0];
return `Currently, you are only seeing results from query ${firstQuery.queryName}. Add a trace operator to combine results of multiple queries.`;
}, [activeBuilder.queryData]);
}, [currentQuery]);
return (
<div className="qb-trace-operator-button-container">
<Tooltip

View File

@@ -26,7 +26,6 @@ import {
queryOperatorSuggestions,
} from 'constants/antlrQueryConstants';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
import { useIsDarkMode } from 'hooks/useDarkMode';
import useDebounce from 'hooks/useDebounce';
import { debounce, isNull } from 'lodash-es';
@@ -133,7 +132,6 @@ function QuerySearch({
metricNamespace,
valueSuggestionsOverride,
}: QuerySearchProps): JSX.Element {
const builderQueryType = useBuilderQueryType(queryData);
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
@@ -332,7 +330,7 @@ function QuerySearch({
source: signalSource as TelemetrytypesSourceDTO,
metricNamespace,
},
builderQueryType,
queryData.builderQueryType,
);
if (response.data.keys) {
@@ -370,7 +368,7 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
builderQueryType,
queryData.builderQueryType,
],
);
@@ -512,7 +510,7 @@ function QuerySearch({
source: signalSource as TelemetrytypesSourceDTO,
metricName: debouncedMetricName ?? undefined,
},
builderQueryType,
queryData.builderQueryType,
).then((response) => {
const responseData = response.data;
const responseDataValues = responseData.values;
@@ -615,7 +613,7 @@ function QuerySearch({
signalSource,
toggleSuggestions,
valueSuggestionsOverride,
builderQueryType,
queryData.builderQueryType,
],
);

View File

@@ -12,7 +12,6 @@ import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
import { QueryProps } from 'container/QueryBuilder/type';
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
import { Copy, Ellipsis, Trash } from '@signozhq/icons';
@@ -63,8 +62,7 @@ export const QueryV2 = forwardRef(function QueryV2(
const { cloneQuery, panelType } = useQueryBuilder();
const hasQueryFunctions = query?.functions?.length > 0;
const { dataSource } = query;
const builderQueryType = useBuilderQueryType(query);
const { dataSource, builderQueryType } = query;
const [isCollapsed, setIsCollapsed] = useState(false);

View File

@@ -2,7 +2,6 @@
/* eslint-disable sonarjs/no-identical-functions */
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { getActiveBuilder } from 'lib/query/builderSlot';
import {
autocompletion,
closeCompletion,
@@ -88,18 +87,17 @@ function TraceOperatorEditor({
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
const [lastExternalValue, setLastExternalValue] = useState<string>('');
const { currentQuery } = useQueryBuilder();
const activeBuilder = getActiveBuilder(currentQuery);
const queryOptions = useMemo(
() =>
activeBuilder.queryData
currentQuery.builder.queryData
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
.map((query) => ({
label: query.queryName,
type: 'atom',
apply: query.queryName,
})),
[activeBuilder.queryData],
[currentQuery.builder.queryData],
);
const toggleSuggestions = useCallback(

View File

@@ -1,71 +0,0 @@
import {
initialQueriesMap,
initialQueryAIWithType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { render, screen } from 'tests/test-utils';
import type {
IBuilderQuery,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { QueryBuilderV2 } from '../QueryBuilderV2';
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('../QueryV2/QueryV2', () => ({
QueryV2: ({ query }: { query: IBuilderQuery }): JSX.Element => (
<div data-testid="query-v2">{query.queryName}</div>
),
}));
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
function renderWith(currentQuery: Query): void {
mockedUseQueryBuilder.mockReturnValue({
currentQuery,
panelType: PANEL_TYPES.TIME_SERIES,
initialDataSource: null,
handleSetConfig: jest.fn(),
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
addTraceOperator: jest.fn(),
handleRunQuery: jest.fn(),
} as unknown as ReturnType<typeof useQueryBuilder>);
render(<QueryBuilderV2 panelType={PANEL_TYPES.TIME_SERIES} version="v3" />);
}
const builderWithQuery = (
builder: Query['builder'],
queryName: string,
): Query['builder'] => ({
...builder,
queryData: [{ ...builder.queryData[0], queryName }],
});
const query: Query = {
...initialQueriesMap.metrics,
builder: builderWithQuery(initialQueriesMap.metrics.builder, 'MAIN'),
builder_ai_query: builderWithQuery(
getActiveBuilder(initialQueryAIWithType),
'AI',
),
};
describe('QueryBuilderV2 builder slot', () => {
it('renders the AI slot when AI is the query type', () => {
renderWith({ ...query, queryType: EQueryType.AI_QUERY_BUILDER });
expect(screen.getByTestId('query-v2')).toHaveTextContent('AI');
});
it('renders builder when Query Builder is the query type', () => {
renderWith(query);
expect(screen.getByTestId('query-v2')).toHaveTextContent('MAIN');
});
});

View File

@@ -1,8 +1,8 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { render } from 'tests/test-utils';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
@@ -19,7 +19,7 @@ const aiQuery: Query = {
...initialQueriesMap.traces.builder,
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
...query,
builderQueryType: EQueryType.AI_QUERY_BUILDER,
builderQueryType: QueryMode.AI_QUERY_BUILDER,
})),
},
};

View File

@@ -1,16 +1,13 @@
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
adjustQueryForV5,
convertAggregationToExpression,
convertExpressionToFilters,
convertFiltersToExpression,
@@ -1687,31 +1684,3 @@ describe('formatValueForExpression', () => {
});
});
});
describe('adjustQueryForV5', () => {
it('adjusts the AI builder slot for AI query builder queries', () => {
const emptyBuilder = {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
};
const query: Query = {
id: 'q',
queryType: EQueryType.AI_QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
builder: emptyBuilder,
builder_ai_query: {
...emptyBuilder,
queryData: [initialQueryBuilderFormValuesMap.logs],
},
};
const result = adjustQueryForV5(query);
expect(result.builder).toBe(emptyBuilder);
expect(result.builder_ai_query?.queryData[0]).not.toHaveProperty(
'aggregateOperator',
);
});
});

View File

@@ -1,5 +1,4 @@
import { useEffect, useRef } from 'react';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
@@ -21,8 +20,7 @@ export function useSyncQueryBuilderFields(
fields: SyncedQueryBuilderField[] = NO_FIELDS,
): void {
const { currentQuery, handleSetQueryData } = useQueryBuilder();
const activeBuilder = getActiveBuilder(currentQuery);
const queries = activeBuilder.queryData;
const queries = currentQuery.builder.queryData;
const previousRef = useRef<Map<string, IBuilderQuery>>();
const enabled = fields.length > 0;

View File

@@ -9,11 +9,6 @@ import {
QUERY_BUILDER_FUNCTIONS,
} from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
import {
getActiveBuilder,
getBuilderSlotKey,
isBuilderQueryType,
} from 'lib/query/builderSlot';
import FilterQueryLexer from 'parser/FilterQueryLexer';
import FilterQueryParser, {
AndExpressionContext,
@@ -38,6 +33,7 @@ import {
MetricAggregation,
TraceAggregation,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import { isQuoted, unquote } from 'utils/stringUtils';
@@ -1034,9 +1030,8 @@ export function getQueryLabelWithAggregation(
}
export const adjustQueryForV5 = (currentQuery: Query): Query => {
if (isBuilderQueryType(currentQuery.queryType)) {
const activeBuilder = getActiveBuilder(currentQuery);
const newQueryData = activeBuilder.queryData.map((query) => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const newQueryData = currentQuery.builder.queryData.map((query) => {
const aggregations = query.aggregations?.map((aggregation) => {
if (query.dataSource === DataSource.METRICS) {
const metricAggregation = aggregation as MetricAggregation;
@@ -1077,8 +1072,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
return {
...currentQuery,
[getBuilderSlotKey(currentQuery.queryType)]: {
...activeBuilder,
builder: {
...currentQuery.builder,
queryData: newQueryData,
},
};

View File

@@ -4,8 +4,6 @@ function QueryTypeTag({ queryType }: IQueryTypeTagProps): JSX.Element {
switch (queryType) {
case EQueryType.QUERY_BUILDER:
return <span>Query Builder</span>;
case EQueryType.AI_QUERY_BUILDER:
return <span>AI Query Builder</span>;
case EQueryType.CLICKHOUSE:
return <span>ClickHouse Query</span>;

View File

@@ -3,7 +3,6 @@ import {
convertFiltersToExpressionWithExistingQuery,
} from 'components/QueryBuilderV2/utils';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { EQueryType } from 'types/common/dashboard';
import {
Query,
TagFilter,
@@ -557,49 +556,3 @@ describe('clearFilterFromQuery', () => {
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
});
});
describe('quick filters on an AI query', () => {
const builderQuery = buildQuery([], '').builder;
const aiQuery = {
queryType: EQueryType.AI_QUERY_BUILDER,
builder: builderQuery,
builder_ai_query: buildQuery(
[{ key: KEY, op: 'in', value: ['a'] }],
`${KEY} IN ['a']`,
).builder,
} as unknown as Query;
it('a checkbox toggle writes into the AI slot and leaves builder alone', () => {
const result = applyCheckboxToggle({
currentQuery: aiQuery,
activeQueryIndex: 0,
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
attributeValues: ['a', 'b', 'c'],
value: 'b',
checked: true,
isOnlyOrAllClicked: false,
});
expect(result.builder).toBe(builderQuery);
expect(
result.builder_ai_query?.queryData[0].filters?.items.map(
(item) => item.value,
),
).toStrictEqual([['a', 'b']]);
});
it('clearing a filter clears it from the AI slot', () => {
const result = clearFilterFromQuery({
currentQuery: aiQuery,
filterKey: KEY,
activeQueryIndex: 0,
});
expect(result.builder).toBe(builderQuery);
expect(result.builder_ai_query?.queryData[0].filters?.items).toStrictEqual(
[],
);
expect(result.builder_ai_query?.queryData[0].filter?.expression).toBe('');
});
});

View File

@@ -10,11 +10,6 @@ import {
import { OPERATORS } from 'constants/antlrQueryConstants';
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
import { cloneDeep, isArray } from 'lodash-es';
import {
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { v4 as uuid } from 'uuid';
@@ -162,9 +157,8 @@ export function applyCheckboxToggle({
previousState?: CheckedState;
sectionType?: SectionType;
}): Query {
const slot = getBuilderSlotKey(currentQuery.queryType);
const builder = getBuilderAt(currentQuery, slot);
const activeItems = builder.queryData?.[activeQueryIndex]?.filters?.items;
const activeItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
@@ -179,7 +173,7 @@ export function applyCheckboxToggle({
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
const query = cloneDeep(builder.queryData?.[activeQueryIndex]);
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
// if only or all are clicked we do not need to worry about anything just override whatever we have
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
@@ -436,15 +430,18 @@ export function applyCheckboxToggle({
query.filters = synced.filters;
}
return withBuilderAt(currentQuery, slot, {
...builder,
queryData: [
...builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
});
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
},
};
}

View File

@@ -1,7 +1,6 @@
import { useMemo, useState } from 'react';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { DEFAULT_VISIBLE_ITEMS_COUNT } from './constants';
import { isKeyMatch } from './utils';
@@ -38,13 +37,12 @@ function useCheckboxDisclosure({
DEFAULT_VISIBLE_ITEMS_COUNT,
);
const { queryData } = getActiveBuilder(currentQuery);
const isSomeFilterPresentForCurrentAttribute = useMemo(
() =>
!!queryData?.[activeQueryIndex]?.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
),
[queryData, activeQueryIndex, filter.attributeKey.key],
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
);
const isOpen = useMemo(() => {

View File

@@ -4,7 +4,6 @@ import {
QuickFiltersSource,
} from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { isEqual, isFunction } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -78,7 +77,7 @@ function useCheckboxFilterActions({
dispatch(updatedQuery);
if (onQuickFilterChange) {
const queryData = getActiveBuilder(updatedQuery).queryData[activeQueryIndex];
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
const expression = queryData?.filter?.expression || '';
const filterItemKeys = (queryData?.filters?.items || [])
.map((item) => item.key?.key)

View File

@@ -1,7 +1,6 @@
import { useMemo } from 'react';
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { deriveCheckboxState } from './checkboxFilterQuery';
import { isKeyMatch } from './utils';
@@ -32,28 +31,33 @@ function useCheckboxFilterState({
activeQueryIndex,
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
const { currentQuery } = useQueryBuilder();
const activeQueryData = currentQuery
? getActiveBuilder(currentQuery).queryData
: undefined;
// derive the state of each filter key here and keep it in sync with current query
const currentFilterState = useMemo(
() =>
deriveCheckboxState({
attributeValues,
filterItems: activeQueryData?.[activeQueryIndex]?.filters?.items,
filterItems:
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
filterKey: filter.attributeKey.key,
}),
[attributeValues, activeQueryData, filter.attributeKey, activeQueryIndex],
[
attributeValues,
currentQuery?.builder.queryData,
filter.attributeKey,
activeQueryIndex,
],
);
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
const isFilterDisabled = useMemo(
() =>
(activeQueryData?.[activeQueryIndex]?.filters?.items?.filter((item) =>
(currentQuery?.builder?.queryData?.[
activeQueryIndex
]?.filters?.items?.filter((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
)?.length || 0) > 1,
[activeQueryData, activeQueryIndex, filter.attributeKey],
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
);
// whether the current filter has multiple values to its name in the key op value section

View File

@@ -11,7 +11,6 @@ import {
} from 'components/QuickFilters/types';
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import useDebouncedFn from 'hooks/useDebouncedFunction';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -129,15 +128,18 @@ export default function CheckboxFilterV2(
onToggleOpen();
};
const activeQueryData = currentQuery
? getActiveBuilder(currentQuery).queryData
: undefined;
const currentFilterOp = useMemo(() => {
const filterSync = activeQueryData?.[activeQueryIndex]?.filters?.items.find(
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
);
return filterSync?.op;
}, [activeQueryData, activeQueryIndex, filter.attributeKey.key]);
}, [
currentQuery?.builder.queryData,
activeQueryIndex,
filter.attributeKey.key,
]);
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
@@ -19,9 +18,6 @@ export function useExistingQuery({
activeQueryIndex,
}: UseExistingQueryParams): UseExistingQueryResult {
const { stagedQuery } = useQueryBuilder();
const stagedQueryData = stagedQuery
? getActiveBuilder(stagedQuery).queryData
: undefined;
const existingQuery = useMemo(() => {
if (useFieldApis.existingQuery === null) {
@@ -32,7 +28,7 @@ export function useExistingQuery({
return useFieldApis.existingQuery;
}
const queryData = stagedQueryData?.[activeQueryIndex];
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
// Prefer V5 filter.expression
if (queryData?.filter?.expression) {
@@ -45,17 +41,21 @@ export function useExistingQuery({
}
return undefined;
}, [useFieldApis.existingQuery, stagedQueryData, activeQueryIndex]);
}, [
useFieldApis.existingQuery,
stagedQuery?.builder.queryData,
activeQueryIndex,
]);
// Check if ANY filters exist in query (V3 items or V5 expression)
// This is separate from existingQuery because existingQuery can be explicitly
// disabled (null) while filters still exist in the query for UI purposes
const hasExistingQuery = useMemo(() => {
const queryData = stagedQueryData?.[activeQueryIndex];
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
const hasV5Expression = !!queryData?.filter?.expression;
return hasV3Items || hasV5Expression || !!existingQuery;
}, [stagedQueryData, activeQueryIndex, existingQuery]);
}, [stagedQuery?.builder.queryData, activeQueryIndex, existingQuery]);
return { existingQuery, hasExistingQuery };
}

View File

@@ -9,12 +9,6 @@ import {
import { getMs } from 'utils/timeUtils';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
getActiveBuilder,
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
import {
AllTraceFilterKeys,
@@ -63,8 +57,7 @@ function Duration({
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {
const filters =
compositeQuery &&
getActiveBuilder(compositeQuery).queryData?.[activeQueryIndex]?.filters;
compositeQuery?.builder.queryData?.[activeQueryIndex]?.filters;
if (!filters) {
return {} as FilterType;
}
@@ -190,51 +183,49 @@ function Duration({
};
const removeFilterItemIds = (query: Query): Query => {
const slot = getBuilderSlotKey(query.queryType);
const builder = getBuilderAt(query, slot);
return withBuilderAt(cloneDeep(query), slot, {
...builder,
queryData: builder.queryData.map((data) => ({
...data,
filters: {
...data.filters,
items:
data.filters?.items?.map((item) => ({
...item,
id: '',
})) || [],
op: data.filters?.op || 'AND',
},
})),
});
const clonedQuery = cloneDeep(query);
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
...data,
filters: {
...data.filters,
items:
data.filters?.items?.map((item) => ({
...item,
id: '',
})) || [],
op: data.filters?.op || 'AND',
},
}));
return clonedQuery;
};
const handleRun = useCallback(
(props?: HandleRunProps): void => {
const slot = getBuilderSlotKey(currentQuery.queryType);
const builder = getBuilderAt(currentQuery, slot);
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
...builder,
queryData: builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
return {
...item,
filters: {
...item.filters,
items: props?.resetAll
? []
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
.map((item) =>
item.key?.key === props?.clearByType ? undefined : item,
)
.filter((i) => i) as TagFilterItem[]),
op: item.filters?.op || 'AND',
},
};
}),
});
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
return {
...item,
filters: {
...item.filters,
items: props?.resetAll
? []
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
.map((item) =>
item.key?.key === props?.clearByType ? undefined : item,
)
.filter((i) => i) as TagFilterItem[]),
op: item.filters?.op || 'AND',
},
};
}),
},
};
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);

View File

@@ -1,9 +1,4 @@
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
import {
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { getKeySpellings, isKeyMatch } from '../Checkbox/utils';
@@ -25,33 +20,34 @@ export function clearFilterFromQuery({
activeQueryIndex: number;
operators?: string[];
}): Query {
const slot = getBuilderSlotKey(currentQuery.queryType);
const builder = getBuilderAt(currentQuery, slot);
return withBuilderAt(currentQuery, slot, {
...builder,
queryData: builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
return {
...item,
filter: {
expression: removeKeysFromExpression(
item.filter?.expression ?? '',
getKeySpellings(filterKey),
false,
operators,
),
},
filters: {
...item.filters,
items:
item.filters?.items?.filter(
(fil) => !isKeyMatch(fil.key?.key, filterKey),
) || [],
op: item.filters?.op || 'AND',
},
};
}),
});
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
return {
...item,
filter: {
expression: removeKeysFromExpression(
item.filter?.expression ?? '',
getKeySpellings(filterKey),
false,
operators,
),
},
filters: {
...item.filters,
items:
item.filters?.items?.filter(
(fil) => !isKeyMatch(fil.key?.key, filterKey),
) || [],
op: item.filters?.op || 'AND',
},
};
}),
},
};
}

View File

@@ -32,12 +32,6 @@ import {
QuickFilterReadPermission,
} from 'lib/authz/hooks/useAuthZ/permissions/quick-filter.permissions';
import { isFunction } from 'lodash-es';
import {
getActiveBuilder,
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
@@ -93,15 +87,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
panelType,
} = useQueryBuilder();
const [open, setOpen] = useState(false);
const activeBuilder = currentQuery
? getActiveBuilder(currentQuery)
: undefined;
// Sync lastUsedQuery when queries change (e.g., after deletion)
const validQueryIndex = useMemo(
() =>
Math.min(lastUsedQuery || 0, (activeBuilder?.queryData?.length || 1) - 1),
[lastUsedQuery, activeBuilder?.queryData?.length],
Math.min(
lastUsedQuery || 0,
(currentQuery?.builder?.queryData?.length || 1) - 1,
),
[lastUsedQuery, currentQuery?.builder?.queryData?.length],
);
// Determine if we're in ListView mode
@@ -109,15 +103,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
// Generate query options based on available queries
const queryOptions = useMemo(() => {
if (!activeBuilder?.queryData) {
if (!currentQuery?.builder?.queryData) {
return [];
}
return activeBuilder.queryData.map((query, index) => ({
return currentQuery.builder.queryData.map((query, index) => ({
label: query.queryName || String.fromCharCode(65 + index),
value: index,
}));
}, [activeBuilder?.queryData]);
}, [currentQuery?.builder?.queryData]);
// Show dropdown in ListView only for TRACES_EXPLORER source
const shouldShowDropdownInListView =
@@ -133,35 +127,36 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
// clear all the filters for the query which is in sync with filters
const handleReset = (): void => {
const activeQuery = activeBuilder?.queryData?.[activeQueryIndex];
const activeQuery = currentQuery?.builder.queryData?.[activeQueryIndex];
if (!activeQuery) {
return;
}
const slot = getBuilderSlotKey(currentQuery.queryType);
const builder = getBuilderAt(currentQuery, slot);
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
...builder,
queryData: builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
return {
...item,
filter: {
...item.filter,
expression: '',
},
filters: {
...item.filters,
items: [],
op: item.filters?.op || 'AND',
},
};
}),
});
return {
...item,
filter: {
...item.filter,
expression: '',
},
filters: {
...item.filters,
items: [],
op: item.filters?.op || 'AND',
},
};
}),
},
};
if (onFilterChange && isFunction(onFilterChange)) {
onFilterChange(preparedQuery);
@@ -171,12 +166,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
};
const lastQueryName =
showQueryName && activeBuilder?.queryData?.[lastUsedQuery || 0]?.queryName;
showQueryName &&
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
// In ListView, always show the 0th query's name; otherwise use the active query's name
const displayedQueryName =
isListView || isAIObservabilityRowView
? showQueryName && activeBuilder?.queryData?.[0]?.queryName
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
: lastQueryName;
const handleQueryChange = (value: number): void => {

View File

@@ -311,10 +311,14 @@ export const initialQueryMeterWithType: Query = {
export const initialQueryAIWithType: Query = {
...initialQueryWithType,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: {
builder: {
...initialQueryWithType.builder,
queryData: [initialQueryBuilderFormValuesMap.traces],
queryData: [
{
...initialQueryBuilderFormValuesMap.traces,
builderQueryType: 'builder_ai_query',
},
],
},
};

View File

@@ -13,11 +13,6 @@ import {
Query,
TagFilterItem,
} from 'types/api/queryBuilder/queryBuilderData';
import {
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { v4 as uuidv4 } from 'uuid';
/** Param shape emitted by the AI for every filter-touching action. */
@@ -61,11 +56,12 @@ export function replaceFirstQueryData(
query: Query,
updated: IBuilderQuery,
): Query {
const slot = getBuilderSlotKey(query.queryType);
const builder = getBuilderAt(query, slot);
const queryData = [...builder.queryData];
const queryData = [...query.builder.queryData];
queryData[0] = updated;
return withBuilderAt(query, slot, { ...builder, queryData });
return {
...query,
builder: { ...query.builder, queryData },
};
}
/** Page-state deps required by the runQuery / addFilter actions. */

View File

@@ -40,7 +40,6 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>
getSelectedQueryOptions(currentQuery.clickhouse_sql),
[EQueryType.AI_QUERY_BUILDER]: () => [],
};
return queryConfig[currentQuery.queryType]?.() || [];

View File

@@ -218,7 +218,6 @@ function FormAlertRules({
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
[EQueryType.CLICKHOUSE]: () =>
getSelectedQueryOptions(currentQuery.clickhouse_sql),
[EQueryType.AI_QUERY_BUILDER]: () => [],
};
return queryConfig[currentQuery.queryType]?.() || [];

View File

@@ -25,7 +25,6 @@ import {
useHandleExplorerTabChange,
} from 'hooks/useHandleExplorerTabChange';
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { isEmpty } from 'lodash-es';
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
import { ExplorerViews } from 'pages/LogsExplorer/utils';
@@ -187,9 +186,7 @@ function Explorer(): JSX.Element {
const isFilterApplied = useMemo(() => {
// if any of the non-disabled queries has filters applied, return true
const result = (
stagedQuery ? getActiveBuilder(stagedQuery) : undefined
)?.queryData?.filter(
const result = stagedQuery?.builder?.queryData?.filter(
(item) => !isEmpty(item.filters?.items) && !item.disabled,
);
return !!result?.length;

View File

@@ -15,7 +15,6 @@ import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { AppState } from 'store/reducers';
import { Warning } from 'types/api';
import APIError from 'types/api/error';
@@ -36,7 +35,7 @@ function TimeSeriesViewContainer({
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
getActiveBuilder(currentQuery).queryData.forEach(
currentQuery.builder.queryData.forEach(
({ aggregateAttribute, aggregateOperator }) => {
const isExistDurationNanoAttribute =
aggregateAttribute?.key === 'durationNano' ||

View File

@@ -18,7 +18,6 @@ import {
FilterDeps,
replaceFirstQueryData,
} from 'container/AIAssistant/pageActions/builderQueryHelpers';
import { getActiveBuilder } from 'lib/query/builderSlot';
import {
ActionResult,
PageAction,
@@ -94,7 +93,7 @@ export function tracesRunQueryAction(
},
autoApply: true,
execute: async ({ filters }): Promise<ActionResult> => {
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
const baseQuery = deps.currentQuery.builder.queryData[0];
if (!baseQuery) {
throw new Error('No active query found in Traces Explorer.');
}
@@ -118,7 +117,7 @@ export function tracesRunQueryAction(
},
getContext: (): Record<string, unknown> => ({
filters:
getActiveBuilder(deps.currentQuery).queryData[0]?.filters?.items?.map(
deps.currentQuery.builder.queryData[0]?.filters?.items?.map(
(f: TagFilterItem) => ({
key: f.key?.key,
op: f.op,
@@ -159,7 +158,7 @@ export function tracesAddFilterAction(
},
autoApply: true,
execute: async ({ key, op, value }): Promise<ActionResult> => {
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
const baseQuery = deps.currentQuery.builder.queryData[0];
if (!baseQuery) {
throw new Error('No active query found in Traces Explorer.');
}

View File

@@ -1,5 +1,4 @@
import { initialQueriesMap } from 'constants/queryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { cloneDeep } from 'lodash-es';
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
import { parseListOrderBy } from 'utils/explorerUtils';
@@ -16,9 +15,8 @@ export const getListViewQuery = (
? [parseListOrderBy(orderBy)]
: [];
const { queryData: builderQueries } = getActiveBuilder(query);
for (let i = 0; i < builderQueries.length; i++) {
const queryData = builderQueries[i];
for (let i = 0; i < query.builder.queryData.length; i++) {
const queryData = query.builder.queryData[i];
queryData.groupBy = [];
queryData.having = {
expression: '',

View File

@@ -29,7 +29,6 @@ import { getMetricNameForQuery } from '../utils';
import OptionRenderer from '../OptionRenderer/OptionRenderer';
import { GroupByFilterProps } from './GroupByFilter.interfaces';
import { toAttributeKey } from './utils';
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
export const GroupByFilter = memo(function GroupByFilter({
query,
@@ -46,7 +45,6 @@ export const GroupByFilter = memo(function GroupByFilter({
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
const builderQueryType = useBuilderQueryType(query);
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
{
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
@@ -54,7 +52,7 @@ export const GroupByFilter = memo(function GroupByFilter({
metricName: getMetricNameForQuery(query),
searchText: debouncedValue,
},
builderQueryType,
query.builderQueryType,
{ enabled: !disabled && isFocused, keepPreviousData: true },
);

View File

@@ -14,7 +14,6 @@ import { selectStyle } from '../QueryBuilderSearchV2/config';
import { getMetricNameForQuery } from '../utils';
import { OrderByFilterProps } from './OrderByFilter.interfaces';
import { useOrderByFilter } from './useOrderByFilter';
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
export function OrderByFilter({
query,
@@ -35,7 +34,6 @@ export function OrderByFilter({
handleSearchKeys,
} = useOrderByFilter({ query, onChange, entityVersion });
const builderQueryType = useBuilderQueryType(query);
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
{
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
@@ -43,7 +41,7 @@ export function OrderByFilter({
metricName: getMetricNameForQuery(query),
searchText: debouncedSearchText,
},
builderQueryType,
query.builderQueryType,
{
enabled: !!query.aggregateAttribute?.key || isRawQuery,
keepPreviousData: true,

View File

@@ -9,7 +9,6 @@ import {
useState,
} from 'react';
import { type RefSelectProps, Select, Spin } from 'antd';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { Pill } from '@signozhq/ui/pill';
import cx from 'classnames';
import {
@@ -857,10 +856,14 @@ function QueryBuilderSearchV2(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const isLastQuery = useMemo(() => {
const { queryData } = getActiveBuilder(currentQuery);
return isEqual(queryData[queryData.length - 1], query);
}, [currentQuery, query]);
const isLastQuery = useMemo(
() =>
isEqual(
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
query,
),
[currentQuery, query],
);
useEffect(() => {
if (isLastQuery) {

View File

@@ -2,7 +2,6 @@ import { useMemo } from 'react';
import { UseQueryResult } from 'react-query';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { isBuilderQueryType } from 'lib/query/builderSlot';
import { ContextLinksData } from 'types/api/widgets/widget';
import { MetricQueryRangeSuccessResponse } from 'types/api/metrics/getQueryRange';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -42,7 +41,7 @@ export function useGraphContextMenu({
} {
const drilldownQuery = useGetCompositeQueryParam() || query;
const isQueryTypeBuilder = isBuilderQueryType(drilldownQuery?.queryType);
const isQueryTypeBuilder = drilldownQuery?.queryType === 'builder';
const { aggregateDrilldownConfig } = useAggregateDrilldown({
query: drilldownQuery,

View File

@@ -3,7 +3,6 @@ import { useAppParams } from 'lib/router/useAppParams';
import cx from 'classnames';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { isBuilderQueryType } from 'lib/query/builderSlot';
import {
createTableColumnsFromQuery,
RowData,
@@ -34,7 +33,7 @@ export function QueryTable({
...props
}: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
const isQueryTypeBuilder = query.queryType === 'builder';
const { servicename: encodedServiceName } = useAppParams<'servicename'>();
const servicename = decodeURIComponent(encodedServiceName || '');

View File

@@ -1,7 +1,6 @@
import WidgetHeader from 'container/WidgetCard/Header';
import { mockAppLocation } from 'tests/router-mocks';
import { fireEvent, render } from 'tests/test-utils';
import { EQueryType } from 'types/common/dashboard';
import { QueryTable } from '../QueryTable';
import { QueryTableProps, WidgetHeaderProps } from './mocks';
@@ -30,21 +29,6 @@ describe('QueryTable -', () => {
const tableRows = container.querySelectorAll('tr.ant-table-row');
expect(tableRows).toHaveLength(3);
});
it('should enable drilldown cells for AI query builder queries', () => {
const { container } = render(
<QueryTable
{...QueryTableProps}
query={{
...QueryTableProps.query,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: QueryTableProps.query.builder,
}}
enableDrillDown
/>,
);
expect(container.querySelector('.clickable-cell')).toBeInTheDocument();
});
});
const setSearchTerm = jest.fn();

View File

@@ -14,12 +14,12 @@ import { useNotifications } from 'hooks/useNotifications';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import useUrlQuery from 'hooks/useUrlQuery';
import createQueryParams from 'lib/createQueryParams';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import {
getCustomTimeRangeWindowSweepInMS,
getStartAndEndTimesInMilliseconds,
} from 'pages/MessagingQueues/MessagingQueuesUtils';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { useGraphClickToShowButton } from 'container/WidgetCard/hooks/useGraphClickToShowButton';
@@ -132,13 +132,13 @@ function WidgetGraphComponent({
const [searchTerm, setSearchTerm] = useState<string>('');
const widgetQueryData = widget?.query
? getActiveBuilder(widget.query)?.queryData
: undefined;
const graphClick = useGraphClickToShowButton({
graphRef: currentGraphRef?.current ? currentGraphRef : graphRef,
isButtonEnabled: (Array.isArray(widgetQueryData) ? widgetQueryData : []).some(
isButtonEnabled: (widget?.query?.builder?.queryData &&
Array.isArray(widget.query.builder.queryData)
? widget.query.builder.queryData
: []
).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),
@@ -219,7 +219,7 @@ function WidgetGraphComponent({
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
enableDrillDown={
enableDrillDown && isBuilderQueryType(widget?.query?.queryType)
enableDrillDown && widget?.query?.queryType === EQueryType.QUERY_BUILDER
}
/>
</Modal>

View File

@@ -1,5 +1,4 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
createColumnsAndDataSource,
@@ -48,18 +47,6 @@ describe('Table Panel utils', () => {
expect(getQueryLegend(query, 'B')).toBeUndefined();
});
it('getQueryLegend reads the AI builder slot', () => {
const builderQuery = widgetQueryWithLegend as Query;
const query: Query = {
...builderQuery,
queryType: EQueryType.AI_QUERY_BUILDER,
builder: { queryData: [], queryFormulas: [], queryTraceOperator: [] },
builder_ai_query: builderQuery.builder,
};
expect(getQueryLegend(query, 'A')).toBe('p99');
});
it('sorter function for table sorting', () => {
let rowA: {
A: string | number;

View File

@@ -4,7 +4,6 @@ import { ThresholdProps } from 'types/api/widgets/threshold';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { RowData } from 'lib/query/createTableColumnsFromQuery';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { isEmpty, isNaN } from 'lodash-es';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -120,15 +119,14 @@ export function getQueryLegend(
let legend: string | undefined;
switch (currentQuery.queryType) {
case EQueryType.QUERY_BUILDER:
case EQueryType.AI_QUERY_BUILDER:
// check if the value is present in the queries
legend = getActiveBuilder(currentQuery)?.queryData?.find(
legend = currentQuery?.builder?.queryData?.find(
(query) => query.queryName === queryName,
)?.legend;
if (!legend) {
// check if the value is present in the formula
legend = getActiveBuilder(currentQuery)?.queryFormulas?.find(
legend = currentQuery?.builder?.queryFormulas?.find(
(query) => query.queryName === queryName,
)?.legend;
}
@@ -201,7 +199,7 @@ export function createColumnsAndDataSource(
: undefined;
const isNewAggregation =
getActiveBuilder(currentQuery).queryData?.find(
currentQuery?.builder?.queryData?.find(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;

View File

@@ -6,7 +6,6 @@ import ValueGraph from 'components/ValueGraph';
import { generateGridTitle } from 'utils/generateGridTitle';
import useGraphContextMenu from 'container/QueryTable/Drilldown/useGraphContextMenu';
import ContextMenu, { useCoordinates } from 'periscope/components/ContextMenu';
import { isBuilderQueryType } from 'lib/query/builderSlot';
import { EQueryType } from 'types/common/dashboard';
import {
@@ -73,7 +72,8 @@ function GridValueComponent({
);
}
const isQueryTypeBuilder = isBuilderQueryType(widget?.query?.queryType);
const isQueryTypeBuilder =
widget?.query?.queryType === EQueryType.QUERY_BUILDER;
return (
<>

View File

@@ -58,27 +58,6 @@ describe('getLegend', () => {
expect(legendsData).toBe('alias_sum');
});
it('should read the AI builder slot for AI query builder queries', () => {
const payloadQuery = getMockQuery({
...mockQuery,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: {
...mockQuery.builder,
queryData: [
{
...mockQuery.builder.queryData[0],
queryName: mockQueryData.queryName,
dataSource: DataSource.LOGS,
aggregations: [{ expression: "sum(bytes) as 'alias_sum'" }],
},
],
},
});
const legendsData = getLegend(mockQueryData, payloadQuery, MOCK_LABEL_NAME);
expect(legendsData).toBe('alias_sum');
});
it('should return legend when single builder query with no alias but legend set (builder)', () => {
const payloadQuery = getMockQuery({
...mockQuery,

View File

@@ -1,38 +0,0 @@
import { renderHook } from '@testing-library/react';
import { EQueryType } from 'types/common/dashboard';
import { useBuilderQueryType } from '../useBuilderQueryType';
import { useQueryBuilder } from '../useQueryBuilder';
jest.mock('../useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
}));
const withQueryType = (queryType: EQueryType): void => {
(useQueryBuilder as jest.Mock).mockReturnValue({
currentQuery: { queryType },
});
};
describe('useBuilderQueryType', () => {
it('is AI for every query while AI is the query type', () => {
withQueryType(EQueryType.AI_QUERY_BUILDER);
expect(renderHook(() => useBuilderQueryType({})).result.current).toBe(
'builder_ai_query',
);
});
it("follows the query's own tag otherwise", () => {
withQueryType(EQueryType.QUERY_BUILDER);
expect(
renderHook(() =>
useBuilderQueryType({ builderQueryType: 'builder_ai_query' }),
).result.current,
).toBe('builder_ai_query');
expect(
renderHook(() => useBuilderQueryType({})).result.current,
).toBeUndefined();
});
});

View File

@@ -2,7 +2,6 @@ import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { initialQueriesMap, initialQueryState } from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { AllTheProviders, renderHook } from 'tests/test-utils';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -106,25 +105,18 @@ describe('useGetCompositeQueryParam', () => {
},
});
const { QUERY_BUILDER, AI_QUERY_BUILDER } = EQueryType;
[
[ROUTES.LOGS_EXPLORER, QUERY_BUILDER, DataSource.LOGS, ''],
[ROUTES.AI_OBSERVABILITY_EXPLORER, AI_QUERY_BUILDER, DataSource.TRACES, ''],
[ROUTES.METRICS_EXPLORER_EXPLORER, QUERY_BUILDER, DataSource.METRICS, ''],
[
ROUTES.METER_EXPLORER,
QUERY_BUILDER,
DataSource.METRICS,
TelemetrytypesSourceDTO.meter,
],
].forEach(([route, queryType, dataSource, source]) => {
[ROUTES.LOGS_EXPLORER, DataSource.LOGS, ''],
[ROUTES.AI_OBSERVABILITY_EXPLORER, DataSource.TRACES, ''],
[ROUTES.METRICS_EXPLORER_EXPLORER, DataSource.METRICS, ''],
[ROUTES.METER_EXPLORER, DataSource.METRICS, TelemetrytypesSourceDTO.meter],
].forEach(([route, dataSource, source]) => {
const query = renderOn(clickhouseWithOperator, route);
const builder = query && getActiveBuilder(query);
expect(query?.queryType).toBe(queryType);
expect(builder?.queryData[0].dataSource).toBe(dataSource);
expect(builder?.queryData[0].source ?? '').toBe(source);
expect(builder?.queryTraceOperator).toStrictEqual([]);
expect(query?.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(query?.builder.queryData[0].dataSource).toBe(dataSource);
expect(query?.builder.queryData[0].source ?? '').toBe(source);
expect(query?.builder.queryTraceOperator).toStrictEqual([]);
});
});

View File

@@ -1,14 +1,16 @@
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import {
Querybuildertypesv5QueryTypeDTO,
TelemetrytypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { EQueryType } from 'types/common/dashboard';
import { BuilderQueryType } from 'types/api/v5/queryRange';
import { BuilderQuerySource, DataSource } from 'types/common/queryBuilder';
export type ExplorerQueryRules = {
dataSource: DataSource;
/** `meter` on the meter explorer, which shares the metrics signal; '' elsewhere. */
source: BuilderQuerySource;
/** AI_QUERY_BUILDER on the AI explorer, whose queries live in the AI slot. */
queryType: EQueryType.QUERY_BUILDER | EQueryType.AI_QUERY_BUILDER;
builderQueryType: BuilderQueryType;
allowsTraceOperator: boolean;
};
@@ -16,31 +18,31 @@ const EXPLORER_QUERY_RULES: Partial<Record<string, ExplorerQueryRules>> = {
[ROUTES.LOGS_EXPLORER]: {
dataSource: DataSource.LOGS,
source: '',
queryType: EQueryType.QUERY_BUILDER,
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
allowsTraceOperator: false,
},
[ROUTES.TRACES_EXPLORER]: {
dataSource: DataSource.TRACES,
source: '',
queryType: EQueryType.QUERY_BUILDER,
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
allowsTraceOperator: true,
},
[ROUTES.AI_OBSERVABILITY_EXPLORER]: {
dataSource: DataSource.TRACES,
source: '',
queryType: EQueryType.AI_QUERY_BUILDER,
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
allowsTraceOperator: false,
},
[ROUTES.METRICS_EXPLORER_EXPLORER]: {
dataSource: DataSource.METRICS,
source: '',
queryType: EQueryType.QUERY_BUILDER,
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
allowsTraceOperator: false,
},
[ROUTES.METER_EXPLORER]: {
dataSource: DataSource.METRICS,
source: TelemetrytypesSourceDTO.meter,
queryType: EQueryType.QUERY_BUILDER,
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
allowsTraceOperator: false,
},
};

View File

@@ -8,8 +8,11 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { getActiveBuilder } from 'lib/query/builderSlot';
import {
IBuilderQuery,
Query,
QueryMode,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -41,9 +44,8 @@ function firstBuilderQuery(
queryData: unknown[],
pathname: string,
): IBuilderQuery {
return getActiveBuilder(
normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname).query,
).queryData[0];
return normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname)
.query.builder.queryData[0];
}
function without(builderQuery: object, key: keyof IBuilderQuery): object {
@@ -60,11 +62,9 @@ describe('normalizeCompositeQuery', () => {
queryType: EQueryType.CLICKHOUSE,
extra: 'kept',
};
const aiQuery: Query = {
...initialQueriesMap.traces,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: initialQueriesMap.traces.builder,
};
const aiQuery = withBuilderQueries(initialQueriesMap.traces, [
{ ...tracesQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER },
]);
[NOT_AN_EXPLORER, ROUTES.LOGS_EXPLORER].forEach((pathname) =>
expect(
@@ -86,33 +86,6 @@ describe('normalizeCompositeQuery', () => {
).toBe(false);
});
it('moves the AI queries of an older link into the AI slot', () => {
const aiQueryData = [
{ ...tracesQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER },
];
const { query, changed } = normalizeCompositeQuery(
withBuilderQueries(initialQueriesMap.traces, aiQueryData),
ROUTES.AI_OBSERVABILITY_EXPLORER,
);
expect(changed).toBe(true);
expect(query.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(query.builder_ai_query?.queryData).toStrictEqual([tracesQuery]);
expect(query.builder).toStrictEqual(initialQueryState.builder);
});
it('drops the AI slot outside the AI explorer', () => {
const { query } = normalizeCompositeQuery(
{
...initialQueriesMap.traces,
builder_ai_query: initialQueriesMap.traces.builder,
},
ROUTES.TRACES_EXPLORER,
);
expect(query.builder_ai_query).toBeUndefined();
});
it('adds missing blocks and a stable id without changing its inputs', () => {
const { builder: _, ...builderless } = {
...initialQueriesMap.logs,
@@ -236,7 +209,7 @@ describe('normalizeCompositeQuery', () => {
expect(
firstBuilderQuery(
logs,
[{ ...logsQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER }],
[{ ...logsQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER }],
ROUTES.LOGS_EXPLORER,
).builderQueryType,
).toBeUndefined();
@@ -247,7 +220,7 @@ describe('normalizeCompositeQuery', () => {
{
...tracesQuery,
queryName: 'D',
builderQueryType: EQueryType.AI_QUERY_BUILDER,
builderQueryType: QueryMode.AI_QUERY_BUILDER,
...llmFilter,
},
],
@@ -270,7 +243,10 @@ describe('normalizeCompositeQuery', () => {
).toBe(TelemetrytypesSourceDTO.meter);
expect(
firstBuilderQuery(traces, [{ ...tracesQuery, ...llmFilter }], ai),
).toStrictEqual({ ...tracesQuery, ...llmFilter });
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
...llmFilter,
});
expect(
firstBuilderQuery(
traces,
@@ -284,13 +260,17 @@ describe('normalizeCompositeQuery', () => {
],
ai,
),
).toStrictEqual({ ...tracesQuery, queryName: 'B', ...llmFilter });
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
queryName: 'B',
filter: { expression: '' },
});
expect(
firstBuilderQuery(traces, [{ ...logsQuery, queryName: 'C' }], ai),
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
dataSource: DataSource.TRACES,
queryName: 'C',
filter: { expression: '' },
});
});

View File

@@ -7,10 +7,8 @@ import {
Querybuildertypesv5QueryTypeDTO,
TelemetrytypesSourceDTO,
} from 'api/generated/services/sigNoz.schemas';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import type { QueryBuilderData } from 'types/common/queryBuilder';
import { v5 as uuidv5 } from 'uuid';
import {
@@ -80,14 +78,6 @@ function addMissingBlocks(query: Query, urlQuery: object): void {
if (!query.builder.queryTraceOperator) {
query.builder.queryTraceOperator = [];
}
if (isPlainObject(query.builder_ai_query)) {
const aiBuilder = query.builder_ai_query as QueryBuilderData;
aiBuilder.queryFormulas = aiBuilder.queryFormulas ?? [];
aiBuilder.queryTraceOperator = aiBuilder.queryTraceOperator ?? [];
} else {
delete query.builder_ai_query;
}
}
/**
@@ -103,21 +93,8 @@ function isListOfObjects(value: unknown): boolean {
}
function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
// Explorers only have a query builder: no PromQL or ClickHouse editor.
query.queryType = rules.queryType;
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
// Older links carry the AI queries inside `builder`.
if (!query.builder_ai_query) {
query.builder_ai_query = query.builder;
query.builder = cloneDeep(initialQueryState.builder);
}
} else {
delete query.builder_ai_query;
}
const builder = getActiveBuilder(query);
if (!isListOfObjects(builder.queryData) || builder.queryData.length === 0) {
builder.queryData = cloneDeep(initialQueryState.builder.queryData);
}
// Explorers only have the query builder: no PromQL or ClickHouse editor.
query.queryType = EQueryType.QUERY_BUILDER;
if (query.promql.some((promql) => promql.query !== '')) {
query.promql = cloneDeep(initialQueryState.promql);
@@ -128,10 +105,10 @@ function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
}
if (!rules.allowsTraceOperator) {
builder.queryTraceOperator = [];
query.builder.queryTraceOperator = [];
}
builder.queryData = builder.queryData.map((builderQuery) =>
query.builder.queryData = query.builder.queryData.map((builderQuery) =>
fitBuilderQueryToExplorer(builderQuery, rules),
);
}
@@ -171,9 +148,12 @@ function fillMissingSignal(
builderQuery.source = rules.source;
}
// The AI slot marks its queries as AI; a tag from an older link is redundant.
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
delete builderQuery.builderQueryType;
// A missing `builderQueryType` already means builder_query, so only AI needs it written.
if (
builderQuery.builderQueryType === undefined &&
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
) {
builderQuery.builderQueryType = rules.builderQueryType;
}
}
@@ -189,11 +169,15 @@ function belongsToExplorer(
return false;
}
// Only the AI explorer runs AI queries.
return (
builderQuery.builderQueryType !==
Querybuildertypesv5QueryTypeDTO.builder_ai_query
);
const builderQueryType =
builderQuery.builderQueryType ??
Querybuildertypesv5QueryTypeDTO.builder_query;
if (builderQueryType !== rules.builderQueryType) {
return false;
}
return true;
}
function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
@@ -202,5 +186,11 @@ function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
);
builderQuery.source = rules.source;
if (
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
) {
builderQuery.builderQueryType = rules.builderQueryType;
}
return builderQuery;
}

View File

@@ -1,18 +0,0 @@
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
import { BuilderQueryType } from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
/**
* The query's builder type, for picking the field suggestion endpoints. Alert editors
* keep AI queries in `builder`, so a query's own tag still counts.
*/
export function useBuilderQueryType(
query: Pick<IBuilderQuery, 'builderQueryType'>,
): BuilderQueryType | undefined {
const { currentQuery } = useQueryBuilder();
return currentQuery?.queryType === EQueryType.AI_QUERY_BUILDER
? 'builder_ai_query'
: query.builderQueryType;
}

View File

@@ -7,7 +7,6 @@ import {
saveAsPreviousQuery,
} from 'components/QueryBuilderV2/QueryV2/previousQuery.utils';
import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { LEGEND } from 'constants/global';
import {
ATTRIBUTE_TYPES,
@@ -489,15 +488,13 @@ export const useQueryOperations: UseQueryOperations = ({
],
);
const activeQueryCount = getActiveBuilder(currentQuery).queryData.length;
const handleDeleteQuery = useCallback(() => {
if (activeQueryCount > 1) {
if (currentQuery.builder.queryData.length > 1) {
removeQueryBuilderEntityByIndex('queryData', index);
}
setLastUsedQuery(0);
}, [
activeQueryCount,
currentQuery.builder.queryData.length,
setLastUsedQuery,
removeQueryBuilderEntityByIndex,
index,

View File

@@ -67,23 +67,6 @@ describe('useGetQueryLabels', () => {
});
});
describe('AI_QUERY_BUILDER type', () => {
it('returns formula labels from builder_ai_query', () => {
const query = buildQuery({
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: {
queryData: [],
queryFormulas: [{ queryName: 'F1' } as unknown as IBuilderFormula],
queryTraceOperator: [],
},
});
const { result } = renderHook(() => useGetQueryLabels(query));
expect(result.current).toStrictEqual([{ label: 'F1', value: 'F1' }]);
});
});
describe('CLICKHOUSE type', () => {
it('returns empty array when clickhouse_sql is undefined', () => {
const query = buildQuery({

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { getQueryLabelWithAggregation } from 'components/QueryBuilderV2/utils';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
@@ -8,12 +7,11 @@ export const useGetQueryLabels = (
currentQuery: Query,
): { label: string; value: string }[] =>
useMemo(() => {
if (currentQuery && isBuilderQueryType(currentQuery.queryType)) {
const activeBuilder = getActiveBuilder(currentQuery);
if (currentQuery?.queryType === EQueryType.QUERY_BUILDER) {
const queryLabels = getQueryLabelWithAggregation(
activeBuilder?.queryData || [],
currentQuery?.builder?.queryData || [],
);
const formulaLabels = (activeBuilder?.queryFormulas ?? []).map(
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
(formula) => ({
label: formula.queryName,
value: formula.queryName,

View File

@@ -27,7 +27,7 @@ import {
QueryRangeResponseV5,
} from 'types/api/v5/queryRange';
import { QueryData } from 'types/api/widgets/getQuery';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { prepareQueryRangePayload } from './prepareQueryRangePayload';
@@ -36,11 +36,11 @@ import { prepareQueryRangePayload } from './prepareQueryRangePayload';
* Validates if metric name is available for METRICS data source
*/
function validateMetricNameForMetricsDataSource(query: Query): boolean {
if (!isBuilderQueryType(query.queryType)) {
if (query.queryType !== 'builder') {
return true; // Non-builder queries don't need this validation
}
const { queryData } = getActiveBuilder(query);
const { queryData } = query.builder;
// Check if any METRICS data source queries exist
const metricsQueries = queryData.filter(
@@ -124,15 +124,14 @@ export const getLegend = (
labelName: string,
): string => {
// For non-query builder queries, return the label name directly
if (!isBuilderQueryType(payloadQuery.queryType)) {
if (payloadQuery.queryType !== EQueryType.QUERY_BUILDER) {
return labelName;
}
const activeBuilder = getActiveBuilder(payloadQuery);
// Combine queryData and queryTraceOperator
const allQueries = [
...(activeBuilder?.queryData || []),
...(activeBuilder?.queryTraceOperator || []),
...(payloadQuery?.builder?.queryData || []),
...(payloadQuery?.builder?.queryTraceOperator || []),
];
const aggregationPerQuery = allQueries.reduce((acc, query) => {

View File

@@ -161,35 +161,6 @@ describe('extractQueryTextStrings', () => {
expect(texts).toStrictEqual(['env = $deployment_environment']);
});
it('extracts from the AI slot, not builder, for an AI query', () => {
const query: Query = {
...baseQuery,
queryType: EQueryType.AI_QUERY_BUILDER,
builder: {
queryData: [
{
filter: { expression: 'env = $ignored' },
} as unknown as IBuilderQuery,
],
queryFormulas: [],
queryTraceOperator: [],
},
builder_ai_query: {
queryData: [
{
filter: { expression: 'service.name = $service' },
} as unknown as IBuilderQuery,
],
queryFormulas: [],
queryTraceOperator: [],
},
};
expect(extractQueryTextStrings(query)).toStrictEqual([
'service.name = $service',
]);
});
it('skips non-string filter values', () => {
const query: Query = {
...baseQuery,

View File

@@ -1,8 +1,6 @@
import { escapeRegExp, isArray } from 'lodash-es';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import type { QueryBuilderData } from 'types/common/queryBuilder';
/**
* Builds a RegExp that matches any recognized variable reference syntax:
@@ -52,11 +50,9 @@ export function containsAnyVariableReference(text: string): boolean {
return !!text && ANY_VARIABLE_REFERENCE.test(text);
}
function extractQueryBuilderTexts(
builder: QueryBuilderData | undefined,
): string[] {
function extractQueryBuilderTexts(query: Query): string[] {
const texts: string[] = [];
const queryDataList = builder?.queryData;
const queryDataList = query.builder?.queryData;
if (isArray(queryDataList)) {
queryDataList.forEach((queryData) => {
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
@@ -106,13 +102,13 @@ function extractClickhouseSQLTexts(query: Query): string[] {
/**
* Extracts all text strings from a widget Query that could contain variable
* references. Covers:
* - QUERY_BUILDER / AI_QUERY_BUILDER: filter items' string values + filter.expression
* - QUERY_BUILDER: filter items' string values + filter.expression
* - PROM: each promql[].query
* - CLICKHOUSE: each clickhouse_sql[].query
*/
export function extractQueryTextStrings(query: Query): string[] {
if (isBuilderQueryType(query.queryType)) {
return extractQueryBuilderTexts(getActiveBuilder(query));
if (query.queryType === EQueryType.QUERY_BUILDER) {
return extractQueryBuilderTexts(query);
}
if (query.queryType === EQueryType.PROM) {

View File

@@ -1,42 +0,0 @@
import {
initialQueryBuilderFormValuesMap,
PANEL_TYPES,
} from 'constants/queryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { mapCompositeQueryFromQuery } from '../mapCompositeQueryFromQuery';
const emptyBuilder = {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
};
describe('mapCompositeQueryFromQuery', () => {
it('sends an AI query as builder_ai_query envelopes under the builder query type', () => {
const query: Query = {
id: 'q',
queryType: EQueryType.AI_QUERY_BUILDER,
promql: [],
clickhouse_sql: [],
builder: {
...emptyBuilder,
queryData: [
{ ...initialQueryBuilderFormValuesMap.metrics, queryName: 'M' },
],
},
builder_ai_query: {
...emptyBuilder,
queryData: [{ ...initialQueryBuilderFormValuesMap.traces, queryName: 'A' }],
},
};
const composite = mapCompositeQueryFromQuery(query, PANEL_TYPES.TIME_SERIES);
expect(composite.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(
composite.queries?.map((q) => [q.type, (q.spec as { name: string }).name]),
).toStrictEqual([['builder_ai_query', 'A']]);
});
});

View File

@@ -1,6 +1,5 @@
import { PANEL_TYPES } from 'constants/queryBuilder';
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
import { toWireQuery } from 'lib/query/builderSlot';
import {
BuilderClickHouseResource,
BuilderPromQLResource,
@@ -92,13 +91,8 @@ const buildPromQuery = (
return compositeQuery;
};
const queryTypeMethodMapping: Record<
EQueryType,
(query: Query, panelType: PANEL_TYPES | null) => ICompositeMetricQuery
> = {
const queryTypeMethodMapping = {
[EQueryType.QUERY_BUILDER]: buildBuilderQuery,
[EQueryType.AI_QUERY_BUILDER]: (query, panelType) =>
buildBuilderQuery(toWireQuery(query), panelType),
[EQueryType.CLICKHOUSE]: buildClickHouseQuery,
[EQueryType.PROM]: buildPromQuery,
};

View File

@@ -15,7 +15,6 @@ import {
PromQuery,
QueryBuilderFormula,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import {
convertBuilderQueryToIBuilderQuery,
convertQueryBuilderFormulaToIBuilderFormula,
@@ -38,7 +37,6 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
> = {};
const promQueries: IPromQLQuery[] = [];
const clickhouseQueries: IClickHouseQuery[] = [];
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
compositeQuery.queries?.forEach((q) => {
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
@@ -51,13 +49,10 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
}
} else if (q.type === 'builder_ai_query') {
if (spec.name) {
const builderQuery = convertBuilderQueryToIBuilderQuery(
spec as BuilderQuery,
);
// Only AI queries kept in `builder` (alerts) need the tag; the AI slot marks its own.
builderQueries[spec.name] = isAIQuery
? builderQuery
: { ...builderQuery, builderQueryType: 'builder_ai_query' };
builderQueries[spec.name] = {
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
builderQueryType: 'builder_ai_query',
};
builderQueryTypes[spec.name] = 'builder_ai_query';
}
} else if (q.type === 'builder_formula') {
@@ -92,14 +87,8 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
});
}
});
const builder = transformQueryBuilderDataModel(
builderQueries,
builderQueryTypes,
);
return {
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
...(isAIQuery && { builder_ai_query: builder }),
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
promql: promQueries,
clickhouse_sql: clickhouseQueries,
queryType: compositeQuery.queryType,

View File

@@ -1,93 +0,0 @@
import {
initialQueriesMap,
initialQueryAIWithType,
} from 'constants/queryBuilder';
import {
getActiveBuilder,
getBuilderSlotKey,
isBuilderQueryType,
toWireQuery,
} from 'lib/query/builderSlot';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
const baseQuery = initialQueriesMap[DataSource.METRICS];
const aiBuilder = getActiveBuilder(initialQueryAIWithType);
describe('isBuilderQueryType', () => {
it.each([
[EQueryType.QUERY_BUILDER, true],
[EQueryType.AI_QUERY_BUILDER, true],
[EQueryType.CLICKHOUSE, false],
[EQueryType.PROM, false],
])('%s → %s', (queryType, expected) => {
expect(isBuilderQueryType(queryType)).toBe(expected);
});
});
describe('getBuilderSlotKey', () => {
it.each([
[EQueryType.QUERY_BUILDER, 'builder'],
[EQueryType.CLICKHOUSE, 'builder'],
[EQueryType.PROM, 'builder'],
[EQueryType.AI_QUERY_BUILDER, 'builder_ai_query'],
])('maps %s to %s', (queryType, slot) => {
expect(getBuilderSlotKey(queryType)).toBe(slot);
});
});
describe('getActiveBuilder', () => {
it('reads builder for the Query Builder type', () => {
const query: Query = { ...baseQuery, builder_ai_query: aiBuilder };
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
});
it('reads the AI slot for the AI type', () => {
const query: Query = {
...baseQuery,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: aiBuilder,
};
expect(getActiveBuilder(query)).toBe(aiBuilder);
});
it('reads an unused AI slot as holding no queries', () => {
const query: Query = { ...baseQuery, queryType: EQueryType.AI_QUERY_BUILDER };
expect(getActiveBuilder(query)).toStrictEqual({
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
});
});
it('keeps builder for ClickHouse and PromQL', () => {
const query: Query = { ...baseQuery, queryType: EQueryType.PROM };
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
});
});
describe('toWireQuery', () => {
it('passes non-AI queries through untouched', () => {
expect(toWireQuery(baseQuery)).toBe(baseQuery);
});
it('sends AI as a builder query holding the AI slot, every query tagged', () => {
const untagged = {
...aiBuilder.queryData[0],
builderQueryType: undefined,
};
const query: Query = {
...baseQuery,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: { ...aiBuilder, queryData: [untagged] },
};
const wire = toWireQuery(query);
expect(wire.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(wire.builder.queryData).toStrictEqual([
{ ...untagged, builderQueryType: 'builder_ai_query' },
]);
});
});

View File

@@ -1,73 +0,0 @@
import type {
Query,
QueryState,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import type { QueryBuilderData } from 'types/common/queryBuilder';
export type BuilderSlotKey = 'builder' | 'builder_ai_query';
const EMPTY_BUILDER: QueryBuilderData = {
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
};
/** Query Builder and AI Query Builder; both author builder queries, each in its own slot. */
export function isBuilderQueryType(queryType: EQueryType | undefined): boolean {
return (
queryType === EQueryType.QUERY_BUILDER ||
queryType === EQueryType.AI_QUERY_BUILDER
);
}
/** The `Query` slot builder edits land in; non-builder types keep `builder`, as they always have. */
export function getBuilderSlotKey(queryType: EQueryType): BuilderSlotKey {
return queryType === EQueryType.AI_QUERY_BUILDER
? 'builder_ai_query'
: 'builder';
}
/**
* The query as the backend takes it. AI goes out as a `builder` query whose queries carry
* `builderQueryType: builder_ai_query`, the tag set from the slot so a stale one can't leak.
*/
export function toWireQuery(query: Query): Query {
if (query.queryType !== EQueryType.AI_QUERY_BUILDER) {
return query;
}
const aiBuilder = getActiveBuilder(query);
return {
...query,
queryType: EQueryType.QUERY_BUILDER,
builder: {
...aiBuilder,
queryData: aiBuilder.queryData.map((queryData) => ({
...queryData,
builderQueryType: 'builder_ai_query',
})),
},
};
}
/** The builder at `slot`; an unused AI slot holds none. */
export function getBuilderAt(
state: QueryState,
slot: BuilderSlotKey,
): QueryBuilderData {
return state[slot] ?? EMPTY_BUILDER;
}
/** `state` with the builder at `slot` replaced. */
export function withBuilderAt<T extends QueryState>(
state: T,
slot: BuilderSlotKey,
builder: QueryBuilderData,
): T {
return { ...state, [slot]: builder };
}
/** The builder queries `queryType` authors; an unused AI slot holds none. */
export function getActiveBuilder(query: Query): QueryBuilderData {
return getBuilderAt(query, getBuilderSlotKey(query.queryType));
}

View File

@@ -12,7 +12,6 @@ import {
import { FORMULA_REGEXP } from 'constants/regExp';
import { QUERY_TABLE_CONFIG } from 'container/QueryTable/config';
import { QueryTableProps } from 'container/QueryTable/QueryTable.intefaces';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
import { get, isEqual, isNaN, isObject } from 'lodash-es';
import {
IBuilderFormula,
@@ -94,8 +93,8 @@ const getQueryByName = <T extends keyof QueryBuilderData>(
queryArray.find((q) => q.name === currentQueryName) || defaultQueryValue
);
}
if (isBuilderQueryType(query.queryType)) {
const queryArray = (getActiveBuilder(query)[type] || []) as (
if (query.queryType === EQueryType.QUERY_BUILDER) {
const queryArray = (query.builder[type] || []) as (
| IBuilderQuery
| IBuilderFormula
)[];
@@ -174,7 +173,7 @@ const addOperatorFormulaColumns = (
return;
}
if (isBuilderQueryType(queryType)) {
if (queryType === EQueryType.QUERY_BUILDER) {
const currentQueryData = query as IBuilderQuery;
let operatorLabel = `${currentQueryData.aggregateOperator}`;
if (currentQueryData.aggregateAttribute?.key) {

View File

@@ -0,0 +1,51 @@
import {
BuilderVariant,
type Query,
QueryMode,
} from 'types/api/queryBuilder/queryBuilderData';
/** Which variant the `builder` slot holds, whatever query type is active. */
export function getBuilderVariant(builder: Query['builder']): BuilderVariant {
return builder.queryData[0]?.builderQueryType === QueryMode.AI_QUERY_BUILDER
? BuilderVariant.AI
: BuilderVariant.MAIN;
}
/** The variant a tab authors; `undefined` for tabs that own their own slot. */
export function toBuilderVariant(mode: QueryMode): BuilderVariant | undefined {
if (mode === QueryMode.AI_QUERY_BUILDER) {
return BuilderVariant.AI;
}
return mode === QueryMode.QUERY_BUILDER ? BuilderVariant.MAIN : undefined;
}
/** The tab a query belongs to: its query type, or AI when a builder query carries the tag. */
export function getQueryMode(query: Query): QueryMode {
if (query.queryType !== QueryMode.QUERY_BUILDER) {
return query.queryType;
}
return getBuilderVariant(query.builder) === BuilderVariant.AI
? QueryMode.AI_QUERY_BUILDER
: QueryMode.QUERY_BUILDER;
}
/** Query Builder and AI both author `Query.builder`; the other modes own their own slot. */
export function isBuilderMode(mode: QueryMode): boolean {
return toBuilderVariant(mode) !== undefined;
}
/** Drops the AI tag, leaving the same query readable as a plain builder query. */
export function withoutAIQueryTag(query: Query): Query {
if (getBuilderVariant(query.builder) !== BuilderVariant.AI) {
return query;
}
return {
...query,
builder: {
...query.builder,
queryData: query.builder.queryData.map(
({ builderQueryType: _tag, ...queryData }) => queryData,
),
},
};
}

View File

@@ -1,9 +1,10 @@
import { EQueryType } from 'types/common/dashboard';
import { Flame } from '@signozhq/icons';
import type { DashboardtypesPanelDTO } from 'api/generated/services/sigNoz.schemas';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getQueryMode } from 'lib/query/queryMode';
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import { useCreateAlertFromPanel } from 'pages/DashboardPage/DashboardContainer/PanelsAndSectionsLayout/Panel/hooks/useCreateAlertFromPanel';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import ConfigActionRow from './ConfigActionRow';
import styles from './ConfigActions.module.scss';
@@ -32,7 +33,7 @@ function ConfigActions({
//TODO: AI query check should be removed after we support alerts on ai query @tewarig
if (
!actions.createAlert ||
currentQuery.queryType === EQueryType.AI_QUERY_BUILDER
getQueryMode(currentQuery) === QueryMode.AI_QUERY_BUILDER
) {
return null;
}

View File

@@ -81,7 +81,7 @@ describe('PanelTypeSwitcher', () => {
return {
...OPTIONS.find((option) => option.kind === kind),
mode: kind === 'signoz/TextPanel' ? 'static' : 'query',
supportedQueryTypesKindMap: Object.fromEntries(
supportedQueryModesKindMap: Object.fromEntries(
queryTypes.map((queryType) => [
queryType,
queryType === EQueryType.QUERY_BUILDER

View File

@@ -25,25 +25,6 @@ describe('getPanelTypeDisabledReason', () => {
).toBeUndefined();
});
it('offers the AI kinds to an AI query and explains the rest', () => {
expect(
getPanelTypeDisabledReason({
kind: 'signoz/TimeSeriesPanel',
queryType: EQueryType.AI_QUERY_BUILDER,
signal: TelemetrytypesSignalDTO.traces,
label: 'Time Series',
}),
).toBeUndefined();
expect(
getPanelTypeDisabledReason({
kind: 'signoz/HeatmapPanel',
queryType: EQueryType.AI_QUERY_BUILDER,
signal: TelemetrytypesSignalDTO.traces,
label: 'Heatmap',
}),
).toBe("Heatmap isn't available for AI Query Builder queries");
});
it('explains an unsupported query type', () => {
expect(
getPanelTypeDisabledReason({

View File

@@ -12,7 +12,6 @@ const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
[EQueryType.QUERY_BUILDER]: 'Query Builder',
[EQueryType.CLICKHOUSE]: 'ClickHouse',
[EQueryType.PROM]: 'PromQL',
[EQueryType.AI_QUERY_BUILDER]: 'AI Query Builder',
};
const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {

View File

@@ -1,6 +1,5 @@
import { useMemo } from 'react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { useDashboardVariableNames } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardVariableNames';
import type { VariableItem } from './types';
@@ -21,16 +20,13 @@ const GLOBAL_TIMESTAMP_VARIABLES: VariableItem[] = [
*/
export function useContextLinkVariables(): VariableItem[] {
const { currentQuery } = useQueryBuilder();
const activeQueryData = currentQuery
? getActiveBuilder(currentQuery).queryData
: undefined;
const dashboardVariableNames = useDashboardVariableNames();
// `_`-prefixed to match V1 and avoid colliding with dashboard-variable names.
const fieldVariableNames = useMemo(() => {
const names = new Set<string>();
(activeQueryData ?? []).forEach((query) => {
(currentQuery?.builder?.queryData ?? []).forEach((query) => {
(query.groupBy ?? []).forEach((field) => {
if (field.key) {
names.add(`_${field.key}`);
@@ -38,7 +34,7 @@ export function useContextLinkVariables(): VariableItem[] {
});
});
return Array.from(names);
}, [activeQueryData]);
}, [currentQuery?.builder?.queryData]);
return useMemo(
() => [

View File

@@ -14,7 +14,7 @@ jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
displayName: 'Time Series',
icon: (): null => null,
mode: 'query',
supportedQueryTypesKindMap: {
supportedQueryModesKindMap: {
builder: { kind: 'signal', signals: ['metrics', 'logs', 'traces'] },
clickhouse_sql: { kind: 'signal-less' },
promql: { kind: 'signal-less' },

View File

@@ -1,4 +1,3 @@
import { EQueryType } from 'types/common/dashboard';
import {
type KeyboardEvent,
type ReactNode,
@@ -24,20 +23,22 @@ import { useSyncQueryBuilderFields } from 'components/QueryBuilderV2/useSyncQuer
import { DataSource } from 'types/common/queryBuilder';
import {
getQueryTypeSignals,
getQueryModeSignals,
isRawRequest,
} from '../../Panels/types/panelCapabilities';
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
import { toPanelType } from '../../Panels/types/panelKind';
import { isBuilderQueryType } from 'lib/query/builderSlot';
import { getQueryMode, isBuilderMode } from 'lib/query/queryMode';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { useQueryTypeChange } from './useQueryTypeChange';
import styles from './PanelEditorQueryBuilder.module.scss';
interface PanelEditorQueryBuilderProps {
/** The edited kind's definition — drives supported query types, the signals the
* builder may be pointed at, and how it narrows the builder's fields. */
panelDefinition: RenderableQueryPanelDefinition;
/** Switch authoring tab; owned by the host, which remembers the Query Builder query. */
onChangeQueryMode: (key: string) => void;
/** Preview fetch in flight — drives the Stage & Run button's loading/cancel state. */
isLoadingQueries: boolean;
/** Run the current query (Stage & Run button / ⌘↵). Always re-runs. */
@@ -57,6 +58,7 @@ interface PanelEditorQueryBuilderProps {
*/
function PanelEditorQueryBuilder({
panelDefinition,
onChangeQueryMode,
isLoadingQueries,
onStageRunQuery,
onCancelQuery,
@@ -66,10 +68,6 @@ function PanelEditorQueryBuilder({
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
// builder offers for this kind comes from the kind's own declaration.
const panelType = toPanelType(panelDefinition.kind);
const onChangeQueryType = useQueryTypeChange({
panelType,
supportedQueryTypesKindMap: panelDefinition.supportedQueryTypesKindMap,
});
// Raw rows: the builder drops its aggregation controls, and with them the trace
// operator that combines aggregated trace queries (V1 parity).
const isRawQuery = isRawRequest(panelDefinition.queryCapabilities);
@@ -101,10 +99,10 @@ function PanelEditorQueryBuilder({
const items = useMemo(() => {
const queryTypeComponents: Record<
EQueryType,
QueryMode,
{ icon: ReactNode; label: string; component: ReactNode }
> = {
[EQueryType.QUERY_BUILDER]: {
[QueryMode.QUERY_BUILDER]: {
icon: <Atom size={14} />,
label: 'Query Builder',
component: (
@@ -112,9 +110,9 @@ function PanelEditorQueryBuilder({
<QueryBuilderV2
panelType={panelType}
fieldsConfig={panelDefinition.queryBuilderFields}
allowedDataSources={getQueryTypeSignals(
panelDefinition.supportedQueryTypesKindMap,
EQueryType.QUERY_BUILDER,
allowedDataSources={getQueryModeSignals(
panelDefinition.supportedQueryModesKindMap,
QueryMode.QUERY_BUILDER,
)}
showTraceOperator={!isRawQuery}
version="v3"
@@ -125,12 +123,12 @@ function PanelEditorQueryBuilder({
</div>
),
},
[EQueryType.CLICKHOUSE]: {
[QueryMode.CLICKHOUSE]: {
icon: <Terminal size={14} />,
label: 'ClickHouse Query',
component: <ClickHouseQueryContainer />,
},
[EQueryType.PROM]: {
[QueryMode.PROM]: {
icon: (
<PromQLIcon
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
@@ -139,7 +137,7 @@ function PanelEditorQueryBuilder({
label: 'PromQL',
component: <PromQLQueryContainer />,
},
[EQueryType.AI_QUERY_BUILDER]: {
[QueryMode.AI_QUERY_BUILDER]: {
icon: <Brain size={14} />,
label: 'AI Query Builder',
component: (
@@ -162,8 +160,8 @@ function PanelEditorQueryBuilder({
};
const modes = Object.keys(
panelDefinition.supportedQueryTypesKindMap,
) as EQueryType[];
panelDefinition.supportedQueryModesKindMap,
) as QueryMode[];
return modes.map((queryType) => ({
key: queryType,
@@ -178,7 +176,7 @@ function PanelEditorQueryBuilder({
),
children: queryTypeComponents[queryType].component,
// QB wants a null data source and AI pins traces: both mounted, they overwrite each other's provider config in a loop.
destroyInactiveTabPane: isBuilderQueryType(queryType),
destroyInactiveTabPane: isBuilderMode(queryType),
}));
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
@@ -195,8 +193,8 @@ function PanelEditorQueryBuilder({
className={cx(styles.tabsContainer, {
[styles.stickyNav]: stickyHeader,
})}
activeKey={currentQuery.queryType}
onChange={onChangeQueryType}
activeKey={getQueryMode(currentQuery)}
onChange={onChangeQueryMode}
tabBarExtraContent={
<span className={styles.runQueryBtnContainer}>
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />

View File

@@ -8,6 +8,7 @@ import PanelEditorQueryBuilder from './PanelEditorQueryBuilder';
function QueryBuilderEditorPane({
panelDefinition,
isLoadingQueries,
onChangeQueryMode,
onStageRunQuery,
onCancelQuery,
stickyHeader,
@@ -16,6 +17,7 @@ function QueryBuilderEditorPane({
<PanelEditorQueryBuilder
panelDefinition={panelDefinition}
isLoadingQueries={isLoadingQueries}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={onStageRunQuery}
onCancelQuery={onCancelQuery}
stickyHeader={stickyHeader}

View File

@@ -1,11 +1,17 @@
import { render, screen, userEvent } from 'tests/test-utils';
import { useRef } from 'react';
import { fireEvent, render, screen } from '@testing-library/react';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { EQueryType } from 'types/common/dashboard';
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import {
toPanelType,
type PanelKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
import { useQueryModeChange } from '../../hooks/useQueryModeChange';
// Capture the props the (real-guard-fed) QueryBuilderV2 receives without rendering it.
const mockQueryBuilderV2 = jest.fn();
@@ -51,19 +57,25 @@ const BUILDER_QUERY = {
builder: { queryData: [{ dataSource: 'traces' }] },
};
const AI_QUERY = {
queryType: EQueryType.AI_QUERY_BUILDER,
builder: { queryData: [] },
builder_ai_query: {
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
},
};
/** Stands in for the editor shell. */
/** Stands in for the editor shell: owns the tab switch and its parked Query Builder query. */
function BuilderHost({ panelKind }: { panelKind: PanelKind }): JSX.Element {
const panelDefinition = requireQueryPanelDefinition(panelKind);
const parkedBuilders = useRef<BuildersByVariant>({});
const onChangeQueryMode = useQueryModeChange({
panelType: toPanelType(panelKind),
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
parkedBuilders,
});
return (
<PanelEditorQueryBuilder
panelDefinition={panelDefinition}
onChangeQueryMode={onChangeQueryMode}
isLoadingQueries={false}
onStageRunQuery={jest.fn()}
onCancelQuery={jest.fn()}
@@ -160,12 +172,11 @@ describe('PanelEditorQueryBuilder AI tab', () => {
});
});
it('seeds an AI traces query when switching to the AI tab', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
it('seeds an AI traces query when switching to the AI tab', () => {
mockBuilder(BUILDER_QUERY);
renderBuilder('signoz/TimeSeriesPanel');
await user.click(screen.getByText('AI Query Builder'));
fireEvent.click(screen.getByText('AI Query Builder'));
expect(updateAllQueriesOperators).toHaveBeenCalledWith(
expect.anything(),
@@ -173,16 +184,11 @@ describe('PanelEditorQueryBuilder AI tab', () => {
'traces',
);
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
expect(seeded.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(seeded.builder_ai_query.queryData[0]).toMatchObject({
dataSource: 'traces',
});
expect(seeded.builder_ai_query.queryData[0].builderQueryType).toBeUndefined();
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
});
it('keeps the builder query when returning from ClickHouse', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
it('keeps the builder query when returning from ClickHouse', () => {
const logsQuery = {
queryType: EQueryType.QUERY_BUILDER,
builder: { queryData: [{ dataSource: 'logs', legend: 'kept' }] },
@@ -190,19 +196,18 @@ describe('PanelEditorQueryBuilder AI tab', () => {
mockBuilder(logsQuery);
renderBuilder('signoz/TimeSeriesPanel');
await user.click(screen.getByText('ClickHouse Query'));
fireEvent.click(screen.getByText('ClickHouse Query'));
const [onClickHouse] = redirectWithQueryBuilderData.mock.calls[0];
expect(onClickHouse.queryType).toBe(EQueryType.CLICKHOUSE);
// The builder slot already holds the Query Builder query — nothing to swap.
expect(onClickHouse.builder).toBe(logsQuery.builder);
});
it('seeds a default query for the kind when leaving AI for an unused Query Builder', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
it('shows a fresh default query when leaving AI', () => {
mockBuilder(AI_QUERY);
renderBuilder('signoz/TimeSeriesPanel');
await user.click(screen.getByText('Query Builder'));
fireEvent.click(screen.getByText('Query Builder'));
const [next] = redirectWithQueryBuilderData.mock.calls[0];
expect(next.queryType).toBe(EQueryType.QUERY_BUILDER);
@@ -210,22 +215,7 @@ describe('PanelEditorQueryBuilder AI tab', () => {
expect(next.builder.queryData[0].dataSource).toBe('metrics');
});
it("reseeds a Query Builder query this kind can't author when leaving AI", async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
mockBuilder({
...AI_QUERY,
builder: { queryData: [{ dataSource: 'metrics' }] },
});
renderBuilder('signoz/ListPanel');
await user.click(screen.getByText('Query Builder'));
const [next] = redirectWithQueryBuilderData.mock.calls[0];
expect(next.builder.queryData[0].dataSource).toBe('logs');
});
it("keeps each tab's own query across Query Builder ↔ AI round trips", async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
it("keeps each tab's own query across Query Builder ↔ AI round trips", () => {
const logsQuery = {
queryType: EQueryType.QUERY_BUILDER,
builder: { queryData: [{ dataSource: 'logs' }] },
@@ -236,26 +226,23 @@ describe('PanelEditorQueryBuilder AI tab', () => {
mockBuilder(logsQuery);
const { rerender } = render(host());
await user.click(screen.getByText('AI Query Builder'));
fireEvent.click(screen.getByText('AI Query Builder'));
const [aiQuery] = redirectWithQueryBuilderData.mock.calls[0];
// The user writes an AI query, then goes back to Query Builder.
const editedAIBuilder = {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
};
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
rerender(host());
await user.click(screen.getByText('Query Builder'));
fireEvent.click(screen.getByText('Query Builder'));
const [backToQB] = redirectWithQueryBuilderData.mock.calls[1];
expect(backToQB.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(backToQB.builder).toBe(logsQuery.builder);
expect(backToQB.builder_ai_query).toBe(editedAIBuilder);
mockBuilder(backToQB);
rerender(host());
await user.click(screen.getByText('AI Query Builder'));
fireEvent.click(screen.getByText('AI Query Builder'));
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
expect(backToAI.builder).toBe(editedAIBuilder);
});
});

View File

@@ -1,72 +0,0 @@
import { useCallback } from 'react';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
getQueryTypeSignals,
type SupportedQueryTypesKindMap,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
interface UseQueryTypeChangeArgs {
panelType: PANEL_TYPES;
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
}
/**
* Tab switching: each query type keeps its own slot, so a switch only changes `queryType`.
* A builder slot is seeded when it can't be used as-is: an AI slot never opened, or
* Query Builder queries on a signal this kind can't author (an AI panel's default).
*/
export function useQueryTypeChange({
panelType,
supportedQueryTypesKindMap,
}: UseQueryTypeChangeArgs): (key: string) => void {
const {
currentQuery,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
return useCallback(
(key: string): void => {
const queryType = key as EQueryType;
const builderSignals = getQueryTypeSignals(
supportedQueryTypesKindMap,
EQueryType.QUERY_BUILDER,
);
const seed = (): Query['builder'] =>
seedBuilderForMode({
mode: queryType,
defaultSignal: builderSignals[0],
panelType,
updateAllQueriesOperators,
});
const next: Query = { ...currentQuery, queryType };
if (queryType === EQueryType.AI_QUERY_BUILDER && !next.builder_ai_query) {
next.builder_ai_query = seed();
}
const authorableSignals = new Set<string>(builderSignals);
const builderFitsKind =
next.builder.queryData.length > 0 &&
next.builder.queryData.every((query) =>
authorableSignals.has(query.dataSource),
);
if (queryType === EQueryType.QUERY_BUILDER && !builderFitsKind) {
next.builder = seed();
}
redirectWithQueryBuilderData(next);
},
[
currentQuery,
panelType,
redirectWithQueryBuilderData,
supportedQueryTypesKindMap,
updateAllQueriesOperators,
],
);
}

View File

@@ -72,6 +72,8 @@ interface QueryEditorBodyProps {
/** Kind switch, owned by the shell (its cache must survive the fork swap). */
onChangePanelKind: (kind: PanelKind) => void;
originalPanelKind?: PanelKind;
/** Tab switch, owned by the shell alongside the kind switch it shares memory with. */
onChangeQueryMode: (key: string) => void;
}
/**
@@ -93,6 +95,7 @@ function QueryEditorBody({
panelDefinition,
onChangePanelKind,
originalPanelKind,
onChangeQueryMode,
}: QueryEditorBodyProps): JSX.Element {
// Read here rather than taken as props: this renders inside a loaded dashboard
// subtree, so it resolves the same context every other consumer does.
@@ -309,6 +312,7 @@ function QueryEditorBody({
panelDefinition={panelDefinition}
signal={listSignal}
isLoadingQueries={isFetching}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}
spec={spec}

View File

@@ -60,8 +60,9 @@ jest.mock('../hooks/usePanelEditorQuerySync', () => ({
}));
const mockUseTypeSwitch = jest.fn();
jest.mock('../hooks/usePanelKindChange', () => ({
usePanelKindChange: (args: unknown): unknown => mockUseTypeSwitch(args),
jest.mock('../hooks/usePanelKindAndQueryModeSwitch', () => ({
usePanelKindAndQueryModeSwitch: (args: unknown): unknown =>
mockUseTypeSwitch(args),
}));
jest.mock('../hooks/usePanelEditorSave', () => ({

View File

@@ -1,6 +1,6 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider as ReduxProvider } from 'react-redux';
@@ -22,6 +22,8 @@ import { usePanelEditorQuerySync } from '../hooks/usePanelEditorQuerySync';
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
import { useQueryModeChange } from '../hooks/useQueryModeChange';
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
jest.mock('hooks/useSafeNavigate', () => {
@@ -79,10 +81,17 @@ function EditorRoute({ panelId }: { panelId: string }): JSX.Element {
savedQueries: panel.spec.queries,
});
const panelDefinition = requireQueryPanelDefinition('signoz/TimeSeriesPanel');
const parkedBuilders = useRef<BuildersByVariant>({});
const onChangeQueryMode = useQueryModeChange({
panelType: PANEL_TYPES.TIME_SERIES,
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
parkedBuilders,
});
return (
<PanelEditorQueryBuilder
panelDefinition={panelDefinition}
onChangeQueryMode={onChangeQueryMode}
isLoadingQueries={false}
onStageRunQuery={noop}
onCancelQuery={noop}

View File

@@ -5,11 +5,11 @@ import { handleQueryChange } from 'lib/query/panelQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { resolveQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { resolveQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../../queryV5/persesQueryAdapters';
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
import { usePanelKindChange } from '../usePanelKindChange';
import { usePanelKindAndQueryModeSwitch } from '../usePanelKindAndQueryModeSwitch';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
@@ -18,8 +18,10 @@ jest.mock('lib/query/panelQuery', () => ({
handleQueryChange: jest.fn(),
}));
jest.mock('../../../Panels/capabilities', () => ({
resolveQueryType: jest.fn(),
resolveQueryMode: jest.fn(),
getSupportedSignals: jest.fn(() => ['metrics']),
getQueryPanelDefinition: jest.requireActual('../../../Panels/capabilities')
.getQueryPanelDefinition,
// Real predicate: these specs use real (query) kinds and the static path is
// exercised through its own cases below.
isStaticPanelKind: jest.requireActual('../../../Panels/capabilities')
@@ -37,7 +39,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
const mockResolveQueryMode = resolveQueryMode as unknown as jest.Mock;
const mockToPerses = toPerses as unknown as jest.Mock;
const mockGetSwitchedPluginSpec = getSwitchedPluginSpec as unknown as jest.Mock;
const mockGetBuilderQueries = getBuilderQueries as unknown as jest.Mock;
@@ -100,7 +102,7 @@ function builderState(currentQuery: Query): {
};
}
describe('usePanelKindChange', () => {
describe('usePanelKindAndQueryModeSwitch', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
@@ -109,7 +111,7 @@ describe('usePanelKindChange', () => {
mockGetBuilderQueries.mockReturnValue([{ signal: 'logs' }]);
// The guard owns coercion (tested in capabilities.test.ts); here it always
// resolves to Query Builder so the coerced type flows into handleQueryChange.
mockResolveQueryType.mockReturnValue('builder');
mockResolveQueryMode.mockReturnValue('builder');
});
it('does nothing when switching to the current kind', () => {
@@ -118,7 +120,7 @@ describe('usePanelKindChange', () => {
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
@@ -137,7 +139,7 @@ describe('usePanelKindChange', () => {
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
@@ -169,7 +171,7 @@ describe('usePanelKindChange', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -200,7 +202,7 @@ describe('usePanelKindChange', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -236,7 +238,7 @@ describe('usePanelKindChange', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -277,7 +279,7 @@ describe('usePanelKindChange', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.HEATMAP,
setSpec,
@@ -318,7 +320,7 @@ describe('usePanelKindChange', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.HEATMAP,
setSpec,
@@ -336,7 +338,7 @@ describe('usePanelKindChange', () => {
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
const { result } = renderHook(() =>
usePanelKindChange({
usePanelKindAndQueryModeSwitch({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -345,7 +347,7 @@ describe('usePanelKindChange', () => {
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// The hook asks the guard to resolve the active query type against the new kind…
expect(mockResolveQueryType).toHaveBeenCalledWith(
expect(mockResolveQueryMode).toHaveBeenCalledWith(
'signoz/ListPanel',
'promql',
);
@@ -354,128 +356,211 @@ describe('usePanelKindChange', () => {
expect((queryArg as Query).queryType).toBe('builder');
});
describe('AI query slot', () => {
const QB_BUILDER = { queryData: [{ dataSource: 'logs' }] };
const AI_BUILDER = {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
};
const withBothSlots = (id: string, queryType: string): Query =>
({
id,
queryType,
builder: QB_BUILDER,
builder_ai_query: AI_BUILDER,
}) as unknown as Query;
const lastRedirect = (state: ReturnType<typeof builderState>): Query =>
state.redirectWithQueryBuilderData.mock.calls[
state.redirectWithQueryBuilderData.mock.calls.length - 1
][0] as Query;
const lastSpec = (setSpec: jest.Mock): DashboardtypesPanelSpecDTO =>
setSpec.mock.calls[setSpec.mock.calls.length - 1][0];
it('keeps the AI tag when the new kind supports AI', () => {
const setSpec = jest.fn();
const aiQuery = fakeQuery('ai', 'builder', [
{ builderQueryType: 'builder_ai_query' },
]);
mockUseQueryBuilder.mockReturnValue(builderState(aiQuery));
mockResolveQueryMode.mockReturnValue('builder_ai_query');
beforeEach(() => {
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
expect((queryArg as Query).builder).toBe(aiQuery.builder);
expect((queryArg as Query).builder.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
});
it('drops the AI tag when the new kind does not support AI', () => {
const setSpec = jest.fn();
const aiQuery = fakeQuery('ai', 'builder', [
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
]);
const state = builderState(aiQuery);
mockUseQueryBuilder.mockReturnValue(state);
mockHandleQueryChange.mockImplementation((_type, query) => query);
// The guard coerces to Query Builder; the tag must not survive it.
mockResolveQueryMode.mockReturnValue('builder');
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
const redirected = state.redirectWithQueryBuilderData.mock
.calls[0][0] as Query;
expect(redirected.builder.queryData[0].builderQueryType).toBeUndefined();
expect(redirected.builder.queryData[0].dataSource).toBe('traces');
});
it('stays on the AI tab when returning to a kind last left on Query Builder', () => {
const setSpec = jest.fn();
const qbQuery = fakeQuery('qb', 'builder', [{ dataSource: 'logs' }]);
const aiQuery = fakeQuery('ai', 'builder', [
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
]);
let state = builderState(qbQuery);
mockUseQueryBuilder.mockImplementation(() => state);
mockHandleQueryChange.mockImplementation((_type, query) => query);
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);
// Leave Table on Query Builder — its stash holds the QB query.
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// On List the user moves to the AI tab, then returns to Table.
state = builderState(aiQuery);
mockResolveQueryMode.mockReturnValue('builder_ai_query');
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
const redirected = state.redirectWithQueryBuilderData.mock
.calls[0][0] as Query;
expect(redirected.builder.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
const restored = setSpec.mock.calls[
setSpec.mock.calls.length - 1
][0] as DashboardtypesPanelSpecDTO;
// The kind's own display spec still comes back; only the query follows the tab.
expect(restored.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
expect(restored.queries).toBe(CONVERTED);
// Table's Query Builder query waited behind the AI tab — the tab switch gives it back.
act(() => result.current.onChangeQueryMode('builder'));
const backToQB = state.redirectWithQueryBuilderData.mock.calls[1][0] as Query;
expect(backToQB.builder).toBe(qbQuery.builder);
});
describe('both builder tabs, per kind', () => {
const qbQuery = fakeQuery('qb', 'builder', [{ dataSource: 'logs' }]);
const aiQuery = fakeQuery('ai', 'builder', [
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
]);
// Table gets a Query Builder query and an AI one; the user leaves it on AI for List.
function leaveTableOnAI(): {
result: { current: ReturnType<typeof usePanelKindAndQueryModeSwitch> };
rerender: (props: {
spec: DashboardtypesPanelSpecDTO;
panelType: PANEL_TYPES;
}) => void;
setSpec: jest.Mock;
setState: (query: Query) => ReturnType<typeof builderState>;
} {
const setSpec = jest.fn();
let state = builderState(qbQuery);
mockUseQueryBuilder.mockImplementation(() => state);
mockHandleQueryChange.mockImplementation((_type, query) => query);
});
it('rebuilds both builder slots for the new kind on a first visit', () => {
const setSpec = jest.fn();
const state = builderState(withBothSlots('q', 'builder_ai_query'));
mockUseQueryBuilder.mockReturnValue(state);
mockResolveQueryType.mockReturnValue('builder_ai_query');
const { result } = renderHook(() =>
usePanelKindChange({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
const rebuilt = mockHandleQueryChange.mock.calls.map(
([, query]) => (query as Query).builder,
);
expect(rebuilt).toStrictEqual([QB_BUILDER, AI_BUILDER]);
const redirected = lastRedirect(state);
expect(redirected.queryType).toBe('builder_ai_query');
expect(redirected.builder.queryData[0].dataSource).toBe('logs');
expect(redirected.builder_ai_query?.queryData[0].dataSource).toBe('traces');
});
it('falls back to Query Builder, keeping the AI query, when the new kind has no AI', () => {
const setSpec = jest.fn();
const state = builderState(withBothSlots('q', 'builder_ai_query'));
mockUseQueryBuilder.mockReturnValue(state);
mockResolveQueryType.mockReturnValue('builder');
const { result } = renderHook(() =>
usePanelKindChange({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
}),
);
act(() => result.current.onChangePanelKind('signoz/TimeSeriesPanel'));
const redirected = lastRedirect(state);
expect(redirected.queryType).toBe('builder');
expect(redirected.builder.queryData[0].dataSource).toBe('logs');
expect(redirected.builder_ai_query).toBeDefined();
});
it('restores a revisited kind on the tab in use, regenerating its preview queries', () => {
const setSpec = jest.fn();
let state = builderState(withBothSlots('table', 'builder_ai_query'));
mockUseQueryBuilder.mockImplementation(() => state);
mockResolveQueryType.mockReturnValue('builder_ai_query');
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindChange({ ...props, setSpec }),
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);
// Leave Table on AI; on List the user moves to Query Builder, then returns.
const setState = (query: Query): ReturnType<typeof builderState> => {
state = builderState(query);
return state;
};
act(() => result.current.onChangeQueryMode('builder_ai_query'));
setState(aiQuery);
rerender({ spec: tableSpec, panelType: PANEL_TYPES.TABLE });
mockResolveQueryMode.mockReturnValue('builder_ai_query');
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
state = builderState(withBothSlots('list', 'builder'));
mockResolveQueryType.mockReturnValue('builder');
return { result, rerender, setSpec, setState };
}
it('rebuilds the parked query for the new kind on a first visit', () => {
const { result } = leaveTableOnAI();
// Table's Query Builder query was rebuilt for List, and is one tab away there.
expect(mockHandleQueryChange).toHaveBeenCalledWith(
'list',
expect.objectContaining({ builder: qbQuery.builder }),
PANEL_TYPES.TABLE,
);
act(() => result.current.onChangeQueryMode('builder'));
const { redirectWithQueryBuilderData } =
mockUseQueryBuilder.mock.results[
mockUseQueryBuilder.mock.results.length - 1
].value;
const backToQB = redirectWithQueryBuilderData.mock.calls[
redirectWithQueryBuilderData.mock.calls.length - 1
][0] as Query;
expect(backToQB.builder.queryData[0].dataSource).toBe('logs');
expect(backToQB.builder.queryData[0].builderQueryType).toBeUndefined();
});
it("brings back both tabs' queries when revisiting a kind", () => {
const { result, rerender, setSpec, setState } = leaveTableOnAI();
// Back to Table from List, still on the AI tab.
const state = setState(
fakeQuery('list-ai', 'builder', [
{ dataSource: 'traces', builderQueryType: 'builder_ai_query' },
]),
);
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
const restored = lastRedirect(state);
expect(restored.id).toBe('table');
expect(restored.queryType).toBe('builder');
expect(restored.builder).toBe(QB_BUILDER);
expect(restored.builder_ai_query).toBe(AI_BUILDER);
expect(lastSpec(setSpec).plugin.spec).toBe(TABLE_PLUGIN_SPEC);
expect(lastSpec(setSpec).queries).toBe(CONVERTED);
const restored = state.redirectWithQueryBuilderData.mock
.calls[0][0] as Query;
expect(restored.builder).toBe(aiQuery.builder);
const restoredSpec = setSpec.mock.calls[
setSpec.mock.calls.length - 1
][0] as DashboardtypesPanelSpecDTO;
expect(restoredSpec.plugin.spec).toBe(TABLE_PLUGIN_SPEC);
expect(restoredSpec.queries).toBe(TABLE_QUERIES);
// Table's Query Builder query is one tab away.
act(() => result.current.onChangeQueryMode('builder'));
const backToQB = state.redirectWithQueryBuilderData.mock
.calls[1][0] as Query;
expect(backToQB.builder).toBe(qbQuery.builder);
});
it('carries the AI query into a revisited kind that never had one', () => {
const setSpec = jest.fn();
let state = builderState(
fakeQuery('table', 'builder', [{ dataSource: 'logs' }]),
);
mockUseQueryBuilder.mockImplementation(() => state);
it('swaps in the Query Builder query when revisiting a kind left on AI', () => {
const { result, rerender, setSpec, setState } = leaveTableOnAI();
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindChange({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
// On List the user is back on Query Builder, then returns to Table.
const state = setState(
fakeQuery('list-qb', 'builder', [{ dataSource: 'logs' }]),
);
// Leave Table on Query Builder; on List the user writes an AI query, then returns.
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
state = builderState(withBothSlots('list', 'builder_ai_query'));
mockResolveQueryType.mockReturnValue('builder_ai_query');
mockResolveQueryMode.mockReturnValue('builder');
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
const restored = lastRedirect(state);
expect(restored.id).toBe('table');
expect(restored.queryType).toBe('builder_ai_query');
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
expect(lastSpec(setSpec).queries).toBe(CONVERTED);
const restored = state.redirectWithQueryBuilderData.mock
.calls[0][0] as Query;
expect(restored.builder).toBe(qbQuery.builder);
// The stashed preview queries were the AI query's; they're rebuilt for the swap.
const restoredSpec = setSpec.mock.calls[
setSpec.mock.calls.length - 1
][0] as DashboardtypesPanelSpecDTO;
expect(restoredSpec.queries).toBe(CONVERTED);
// Table's AI query is one tab away.
act(() => result.current.onChangeQueryMode('builder_ai_query'));
const backToAI = state.redirectWithQueryBuilderData.mock
.calls[1][0] as Query;
expect(backToAI.builder).toBe(aiQuery.builder);
});
});
@@ -488,7 +573,7 @@ describe('usePanelKindChange', () => {
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindChange({ ...props, setSpec }),
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);

View File

@@ -9,7 +9,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { getIsQueryModified } from 'lib/query/panelQuery';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { isEqual } from 'lodash-es';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -112,10 +111,9 @@ export function usePanelEditorQuerySync({
// Re-commit on a query-type/datasource switch so the preview refetches. Skip
// mount: the initial query is synced into the draft by the staged-query effect below.
const activeQueryData = getActiveBuilder(currentQuery).queryData;
const dataSources = useMemo(
() => (activeQueryData ?? []).map((q) => q.dataSource),
[activeQueryData],
() => (currentQuery.builder?.queryData ?? []).map((q) => q.dataSource),
[currentQuery.builder],
);
const dataSourceSignature = dataSources.join(',');
const prevDataSourcesRef = useRef(dataSources);

View File

@@ -11,16 +11,27 @@ import { handleQueryChange } from 'lib/query/panelQuery';
import type { PartialPanelTypes } from 'lib/query/panelTypeDataSourceFormValuesMap';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import type {
OrderByPayload,
Query,
import {
type BuilderVariant,
type BuildersByVariant,
type OrderByPayload,
type Query,
QueryMode,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
getQueryPanelDefinition,
isStaticPanelKind,
resolveQueryType,
resolveQueryMode,
} from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { getQueryModeSignals } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
import {
getBuilderVariant,
getQueryMode,
toBuilderVariant,
withoutAIQueryTag,
} from 'lib/query/queryMode';
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
import { toPanelType, type PanelKind } from '../../Panels/types/panelKind';
import { getBuilderQueries } from '../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../queryV5/persesQueryAdapters';
@@ -32,6 +43,7 @@ import {
withPercentileHistogramAggregation,
withSingleHeatmapQuery,
} from '../utils/heatmapQuery';
import { useQueryModeChange } from './useQueryModeChange';
// V1's handleQueryChange clears orderBy for lists; re-seed the fresh-list default (timestamp desc).
const DEFAULT_LIST_ORDER_BY: OrderByPayload[] = [
@@ -57,17 +69,21 @@ interface KindState {
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
queries: DashboardtypesQueryDTO[];
query: Query;
/** Both builder variants' queries as the kind was left. */
builders: BuildersByVariant;
}
interface UsePanelKindChangeArgs {
interface UsePanelKindAndQueryModeSwitchArgs {
spec: DashboardtypesPanelSpecDTO;
panelType: PANEL_TYPES;
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
}
interface UsePanelKindChangeApi {
interface UsePanelKindAndQueryModeSwitchApi {
/** Switch the panel to `newKind`, transforming/restoring its query + spec. */
onChangePanelKind: (newKind: PanelKind) => void;
/** Switch authoring tab; shares the off-screen builder stash with the kind switch. */
onChangeQueryMode: (key: string) => void;
}
/**
@@ -76,15 +92,22 @@ interface UsePanelKindChangeApi {
* the two things that don't: a per-kind session cache that makes switching reversible
* (`Table → List → Table` restores the original query + spec), and, on first visit to a
* kind, a query rebuild (`handleQueryChange`) + spec reset (`getSwitchedPluginSpec`).
* It also owns the tab switch, stashing the off-screen builder variant per kind.
*/
export function usePanelKindChange({
export function usePanelKindAndQueryModeSwitch({
spec,
panelType,
setSpec,
}: UsePanelKindChangeArgs): UsePanelKindChangeApi {
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
}: UsePanelKindAndQueryModeSwitchArgs): UsePanelKindAndQueryModeSwitchApi {
const {
currentQuery,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
const cacheRef = useRef<Map<PanelKind, KindState>>(new Map());
// Builder queries for the tabs not on screen; the live one is `currentQuery.builder`.
const parkedBuilders = useRef<BuildersByVariant>({});
// Latest spec/query/type, read inside the stable callback without re-subscribing.
const specRef = useRef(spec);
@@ -94,6 +117,14 @@ export function usePanelKindChange({
const panelTypeRef = useRef(panelType);
panelTypeRef.current = panelType;
const onChangeQueryMode = useQueryModeChange({
panelType,
supportedQueryModesKindMap:
getQueryPanelDefinition(spec.plugin.kind as PanelKind)
?.supportedQueryModesKindMap ?? {},
parkedBuilders,
});
const onChangePanelKind = useCallback(
(newKind: PanelKind): void => {
const currentSpec = specRef.current;
@@ -111,9 +142,20 @@ export function usePanelKindChange({
pluginSpec: currentSpec.plugin.spec,
queries: currentSpec.queries,
query,
builders: {
...parkedBuilders.current,
[getBuilderVariant(query.builder)]: query.builder,
},
});
const newPanelType = toPanelType(newKind);
const targetMode = resolveQueryMode(newKind, getQueryMode(query));
const targetVariant = toBuilderVariant(targetMode);
// An AI query is a builder query carrying the tag, so it rides on `builder`.
const targetQueryType =
targetMode === QueryMode.AI_QUERY_BUILDER
? QueryMode.QUERY_BUILDER
: targetMode;
// Only `plugin` needs a cast: it's a discriminated union over `kind`, and a
// dynamically-chosen kind can't be correlated with its spec statically (as in
@@ -130,12 +172,10 @@ export function usePanelKindChange({
} as DashboardtypesPanelPluginDTO,
queries,
});
const queryType = resolveQueryType(newKind, query.queryType);
const rebuildForNewKind = (builder: Query['builder']): Query['builder'] => {
const rebuildForNewKind = (source: Query): Query => {
const transformed = handleQueryChange(
newPanelType as keyof PartialPanelTypes,
{ ...query, queryType: EQueryType.QUERY_BUILDER, builder },
{ ...source, queryType: targetQueryType },
panelTypeRef.current,
);
// Match a fresh list panel's default order so the builder's Order By isn't empty.
@@ -143,41 +183,62 @@ export function usePanelKindChange({
newKind === 'signoz/ListPanel'
? withDefaultListOrder(transformed)
: transformed;
return (
newKind === 'signoz/HeatmapPanel'
? withSingleHeatmapQuery(ordered)
: withPercentileHistogramAggregation(ordered)
).builder;
return newKind === 'signoz/HeatmapPanel'
? withSingleHeatmapQuery(ordered)
: withPercentileHistogramAggregation(ordered);
};
// Tab unused on this kind: AI gets a fresh seed, Query Builder reuses `source` untagged.
const seedTargetTab = (source: Query): Query =>
targetMode === QueryMode.AI_QUERY_BUILDER
? {
...source,
builder: seedBuilderForMode({
mode: targetMode,
defaultSignal: getQueryModeSignals(
getQueryPanelDefinition(newKind)?.supportedQueryModesKindMap,
QueryMode.QUERY_BUILDER,
)[0],
panelType: newPanelType,
updateAllQueriesOperators,
}),
}
: withoutAIQueryTag(source);
// Target tab's query comes from the stash; the rest stay parked.
const unparkTargetTab = (
source: Query,
builders: BuildersByVariant,
): { query: Query; parked: BuildersByVariant } => {
const stored = targetVariant ? builders[targetVariant] : source.builder;
const next = stored
? { ...source, builder: stored }
: seedTargetTab(source);
const parked: BuildersByVariant = { ...builders };
delete parked[getBuilderVariant(next.builder)];
return { query: next, parked };
};
// Revisit → restore the stash (the reversibility path) on the tab in use, an AI
// query the kind never had riding along. A static kind's stash carries
// `queries: []` and its builder query is untouched — there is no builder to
// re-seed for it.
// Revisit → restore the stash in the active mode; a static kind has no builder to re-seed.
const cached = cacheRef.current.get(newKind);
if (cached) {
if (isStaticPanelKind(newKind)) {
setSpec(buildSpec(cached.pluginSpec, cached.queries));
return;
}
const restored: Query = {
...cached.query,
queryType,
...(!cached.query.builder_ai_query &&
query.builder_ai_query && {
builder_ai_query: rebuildForNewKind(query.builder_ai_query),
}),
};
// The stashed preview queries belong to the stashed query's type.
const restored = unparkTargetTab(cached.query, cached.builders);
parkedBuilders.current = restored.parked;
// The stashed preview queries belong to the stashed query, not a swapped-in one.
setSpec(
buildSpec(
cached.pluginSpec,
queryType === cached.query.queryType
restored.query.builder === cached.query.builder
? cached.queries
: toPerses(restored, newPanelType),
: toPerses(restored.query, newPanelType),
),
);
redirectWithQueryBuilderData(restored);
redirectWithQueryBuilderData({
...restored.query,
queryType: targetQueryType,
});
return;
}
@@ -191,15 +252,25 @@ export function usePanelKindChange({
return;
}
// First visit → rebuild every builder slot for the new kind.
const nextQuery: Query = {
...query,
queryType,
builder: rebuildForNewKind(query.builder),
...(query.builder_ai_query && {
builder_ai_query: rebuildForNewKind(query.builder_ai_query),
}),
};
// First visit → rebuild both builder tabs' queries for the new panel type.
const transformed = rebuildForNewKind(query);
const rebuiltBuilders: BuildersByVariant = {};
(Object.keys(parkedBuilders.current) as BuilderVariant[]).forEach(
(variant) => {
const parked = parkedBuilders.current[variant];
if (parked) {
rebuiltBuilders[variant] = rebuildForNewKind({
...query,
builder: parked,
}).builder;
}
},
);
rebuiltBuilders[getBuilderVariant(transformed.builder)] =
transformed.builder;
const picked = unparkTargetTab(transformed, rebuiltBuilders);
parkedBuilders.current = picked.parked;
const nextQuery = picked.query;
const signal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;
@@ -211,8 +282,8 @@ export function usePanelKindChange({
);
redirectWithQueryBuilderData(nextQuery);
},
[setSpec, redirectWithQueryBuilderData],
[setSpec, redirectWithQueryBuilderData, updateAllQueriesOperators],
);
return { onChangePanelKind };
return { onChangePanelKind, onChangeQueryMode };
}

View File

@@ -0,0 +1,83 @@
import { type MutableRefObject, useCallback } from 'react';
import type { PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
getQueryModeSignals,
type SupportedQueryModesKindMap,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
import {
type BuildersByVariant,
type Query,
QueryMode,
} from 'types/api/queryBuilder/queryBuilderData';
import { getBuilderVariant, toBuilderVariant } from 'lib/query/queryMode';
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
interface UseQueryModeChangeArgs {
panelType: PANEL_TYPES;
supportedQueryModesKindMap: SupportedQueryModesKindMap;
/** Builder queries for the variant not on screen. */
parkedBuilders: MutableRefObject<BuildersByVariant>;
}
/**
* Tab switching. Query Builder and AI share `currentQuery.builder`, so crossing between
* them swaps in the parked query, seeding a first visit; ClickHouse and PromQL pass through.
*/
export function useQueryModeChange({
panelType,
supportedQueryModesKindMap,
parkedBuilders,
}: UseQueryModeChangeArgs): (key: string) => void {
const {
currentQuery,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
return useCallback(
(key: string): void => {
const targetMode = key as QueryMode;
const targetVariant = toBuilderVariant(targetMode);
const heldVariant = getBuilderVariant(currentQuery.builder);
const seedFor = (mode: QueryMode): Query['builder'] =>
seedBuilderForMode({
mode,
defaultSignal: getQueryModeSignals(
supportedQueryModesKindMap,
QueryMode.QUERY_BUILDER,
)[0],
panelType,
updateAllQueriesOperators,
});
// Keyed on what `builder` holds, not the active tab: ClickHouse → QB must not reseed.
let { builder } = currentQuery;
if (targetVariant && targetVariant !== heldVariant) {
parkedBuilders.current[heldVariant] = currentQuery.builder;
builder = parkedBuilders.current[targetVariant] ?? seedFor(targetMode);
delete parkedBuilders.current[targetVariant];
}
redirectWithQueryBuilderData({
...currentQuery,
// An AI query is a builder query carrying the AI tag; its query type stays `builder`.
queryType:
targetMode === QueryMode.AI_QUERY_BUILDER
? QueryMode.QUERY_BUILDER
: targetMode,
builder,
});
},
[
currentQuery,
panelType,
parkedBuilders,
redirectWithQueryBuilderData,
supportedQueryModesKindMap,
updateAllQueriesOperators,
],
);
}

View File

@@ -6,7 +6,7 @@ import type { NewPanelTarget } from '../patchOps';
import QueryEditorBody from './QueryEditorBody';
import StaticEditorBody from './StaticEditorBody';
import { usePanelEditorDraft } from './hooks/usePanelEditorDraft';
import { usePanelKindChange } from './hooks/usePanelKindChange';
import { usePanelKindAndQueryModeSwitch } from './hooks/usePanelKindAndQueryModeSwitch';
export interface PanelEditorContainerProps {
dashboardId: string;
@@ -40,11 +40,12 @@ function PanelEditorContainer(props: PanelEditorContainerProps): JSX.Element {
const panelDefinition = getPanelDefinition(panelKind);
const originalPanelKind = (savedPanel ?? panel).spec.plugin.kind;
const { onChangePanelKind } = usePanelKindChange({
spec: draftApi.draft.spec,
panelType: toPanelType(panelKind),
setSpec: draftApi.setSpec,
});
const { onChangePanelKind, onChangeQueryMode } =
usePanelKindAndQueryModeSwitch({
spec: draftApi.draft.spec,
panelType: toPanelType(panelKind),
setSpec: draftApi.setSpec,
});
if (panelDefinition.mode === 'static') {
return (
@@ -65,6 +66,7 @@ function PanelEditorContainer(props: PanelEditorContainerProps): JSX.Element {
panelDefinition={panelDefinition}
onChangePanelKind={onChangePanelKind}
originalPanelKind={originalPanelKind}
onChangeQueryMode={onChangeQueryMode}
/>
);
}

View File

@@ -3,6 +3,7 @@ import {
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { UNSUPPORTED_PANEL } from '../kinds/UnsupportedPanel/definition';
import { getPanelDefinition, isPanelKindSupported } from '../registry';
@@ -11,12 +12,13 @@ import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
import {
getQueryPanelDefinition,
requireQueryPanelDefinition,
getSupportedQueryModes,
getSupportedQueryTypes,
getSupportedSignals,
isPanelCombinationValid,
isQueryTypeSupportedByPanelKind,
isSignalSupported,
resolveQueryType,
resolveQueryMode,
} from '../capabilities';
import type { PanelKind } from '../types/panelKind';
@@ -25,44 +27,59 @@ const { logs, traces, metrics } = TelemetrytypesSignalDTO;
const { time_series, scalar, raw, heatmap } = Querybuildertypesv5RequestTypeDTO;
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/AreaChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/ListPanel': [QUERY_BUILDER],
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
// Static kind: no query surface at all.
'signoz/TextPanel': [],
};
const EXPECTED_QUERY_MODES: Record<PanelKind, QueryMode[]> = {
'signoz/TimeSeriesPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/BarChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/AreaChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/NumberPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/HistogramPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/PieChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
EQueryType.AI_QUERY_BUILDER,
QueryMode.AI_QUERY_BUILDER,
],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, EQueryType.AI_QUERY_BUILDER],
'signoz/ListPanel': [QUERY_BUILDER, EQueryType.AI_QUERY_BUILDER],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, QueryMode.AI_QUERY_BUILDER],
'signoz/ListPanel': [QUERY_BUILDER, QueryMode.AI_QUERY_BUILDER],
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/TextPanel': [],
};
@@ -264,10 +281,10 @@ describe('panel capabilities guard', () => {
it('reads a specific mode when one is given', () => {
expect(
getSupportedSignals('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
getSupportedSignals('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
).toStrictEqual([traces]);
expect(
getSupportedSignals('signoz/TablePanel', EQueryType.CLICKHOUSE),
getSupportedSignals('signoz/TablePanel', QueryMode.CLICKHOUSE),
).toStrictEqual([]);
});
});
@@ -318,21 +335,36 @@ describe('panel capabilities guard', () => {
});
});
describe('resolveQueryType', () => {
describe('getSupportedQueryModes', () => {
it.each(Object.entries(EXPECTED_QUERY_MODES))(
'%s supports %j',
(kind, modes) => {
expect(getSupportedQueryModes(kind as PanelKind)).toStrictEqual(modes);
},
);
it('keeps AI out of the query types', () => {
expect(getSupportedQueryTypes('signoz/TimeSeriesPanel')).not.toContain(
QueryMode.AI_QUERY_BUILDER,
);
});
});
describe('resolveQueryMode', () => {
it('keeps a supported mode', () => {
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
expect(
resolveQueryType('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
).toBe(EQueryType.AI_QUERY_BUILDER);
resolveQueryMode('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
).toBe(QueryMode.AI_QUERY_BUILDER);
expect(
resolveQueryType('signoz/ListPanel', EQueryType.AI_QUERY_BUILDER),
).toBe(EQueryType.AI_QUERY_BUILDER);
resolveQueryMode('signoz/ListPanel', QueryMode.AI_QUERY_BUILDER),
).toBe(QueryMode.AI_QUERY_BUILDER);
});
it('coerces an unsupported mode to the first supported one', () => {
// PromQL → List has no PromQL, falls back to its first (and only) type.
expect(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryMode('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
});
});
});

View File

@@ -1,8 +1,9 @@
import { type TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { getPanelDefinition } from './registry';
import { getQueryTypeSignals } from './types/panelCapabilities';
import { getQueryModeSignals } from './types/panelCapabilities';
import type { RenderableQueryPanelDefinition } from './types/panelDefinition';
import type { PanelKind } from './types/panelKind';
@@ -49,16 +50,16 @@ export function requireQueryPanelDefinition(
return definition;
}
const DEFAULT_QUERY_TYPE = EQueryType.QUERY_BUILDER;
const DEFAULT_QUERY_MODE = QueryMode.QUERY_BUILDER;
/** Signals a kind can visualize in `queryType`. */
/** Signals a kind can visualize in `mode`. */
export function getSupportedSignals(
kind: PanelKind,
queryType: EQueryType = DEFAULT_QUERY_TYPE,
mode: QueryMode = DEFAULT_QUERY_MODE,
): TelemetrytypesSignalDTO[] {
return getQueryTypeSignals(
getQueryPanelDefinition(kind)?.supportedQueryTypesKindMap,
queryType,
return getQueryModeSignals(
getQueryPanelDefinition(kind)?.supportedQueryModesKindMap,
mode,
);
}
@@ -69,11 +70,16 @@ export function isSignalSupported(
return getSupportedSignals(kind).includes(signal);
}
/** Query types a kind supports, in tab order. */
/** Authoring modes a kind supports, in tab order. */
export function getSupportedQueryModes(kind: PanelKind): QueryMode[] {
const modes = getQueryPanelDefinition(kind)?.supportedQueryModesKindMap ?? {};
return Object.keys(modes) as QueryMode[];
}
/** Query languages a kind supports (Query Builder / ClickHouse / PromQL). */
export function getSupportedQueryTypes(kind: PanelKind): EQueryType[] {
const queryTypes =
getQueryPanelDefinition(kind)?.supportedQueryTypesKindMap ?? {};
return Object.keys(queryTypes) as EQueryType[];
return getSupportedQueryModes(kind).filter(
(mode): mode is EQueryType => mode !== QueryMode.AI_QUERY_BUILDER,
);
}
export function isQueryTypeSupportedByPanelKind(
@@ -110,15 +116,15 @@ export function isPanelCombinationValid({
return true;
}
/** `preferred` if the kind supports it, else its first supported query type (e.g. PromQL → List gives Query Builder). */
export function resolveQueryType(
/** `preferred` if the kind supports it, else its first supported mode (e.g. PromQL → List gives Query Builder). */
export function resolveQueryMode(
kind: PanelKind,
preferred: EQueryType,
): EQueryType {
const supported = getSupportedQueryTypes(kind);
preferred: QueryMode,
): QueryMode {
const supported = getSupportedQueryModes(kind);
if (supported.includes(preferred)) {
return preferred;
}
// A query-less kind has no supported query types; the builder is the neutral answer.
return supported[0] ?? DEFAULT_QUERY_TYPE;
// A query-less kind has no supported modes; the builder is the neutral answer.
return supported[0] ?? DEFAULT_QUERY_MODE;
}

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartArea } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/AreaChartPanel'> = {
kind: 'signoz/AreaChartPanel',
@@ -19,19 +19,19 @@ export const definition: PanelDefinition<'signoz/AreaChartPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
kind: 'signoz/BarChartPanel',
@@ -19,19 +19,19 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,10 +1,9 @@
import { EQueryType } from 'types/common/dashboard';
import { LayoutGrid } from '@signozhq/icons';
import { QueryBuilderField } from 'components/QueryBuilderV2/queryBuilderFields.types';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -12,6 +11,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/HeatmapPanel'> = {
kind: 'signoz/HeatmapPanel',
@@ -21,14 +21,14 @@ export const definition: PanelDefinition<'signoz/HeatmapPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
supportedQueryModesKindMap: {
// Only metrics carry a bucket axis; the request rejects the other signals.
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.metrics],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
},
// The request rejects both: a point is a count per bucket, not a single value.
queryBuilderFields: {

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
kind: 'signoz/HistogramPanel',
@@ -19,19 +19,19 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -10,6 +10,7 @@ function ListEditorPane({
panelDefinition,
signal,
isLoadingQueries,
onChangeQueryMode,
onStageRunQuery,
onCancelQuery,
stickyHeader,
@@ -20,6 +21,7 @@ function ListEditorPane({
<PanelEditorQueryBuilder
panelDefinition={panelDefinition}
isLoadingQueries={isLoadingQueries}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={onStageRunQuery}
onCancelQuery={onCancelQuery}
stickyHeader={stickyHeader}

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { List } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import ListEditorPane from './ListEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/ListPanel'> = {
kind: 'signoz/ListPanel',
@@ -19,13 +19,13 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
Renderer,
EditorPane: ListEditorPane,
// Raw records come from logs and traces; metrics don't produce row data.
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.logs, TelemetrytypesSignalDTO.traces],
},
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { Hash } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/NumberPanel'> = {
kind: 'signoz/NumberPanel',
@@ -19,19 +19,19 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartPie } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
kind: 'signoz/PieChartPanel',
@@ -19,18 +19,18 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,7 @@
import { ChartScatter } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { MAX_PLOTTED_GROUPS } from './utils/prepareData';
@@ -12,7 +12,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { EQueryType } from 'types/common/dashboard';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { QueryBuilderField } from 'components/QueryBuilderV2/queryBuilderFields.types';
export const definition: PanelDefinition<'signoz/ScatterPlotPanel'> = {
@@ -23,16 +23,16 @@ export const definition: PanelDefinition<'signoz/ScatterPlotPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
},
queryBuilderFields: {
// Pinned so it opens empty: the group-by template a click would seed names nothing here.

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { Table } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/TablePanel'> = {
kind: 'signoz/TablePanel',
@@ -19,18 +19,18 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,8 +1,7 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartLine } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryTypeKind } from '../../types/panelCapabilities';
import { QueryModeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -10,6 +9,7 @@ import {
Querybuildertypesv5RequestTypeDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
kind: 'signoz/TimeSeriesPanel',
@@ -19,19 +19,19 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -25,7 +25,7 @@ export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections: [],
supportedQueryTypesKindMap: {},
supportedQueryModesKindMap: {},
queryBuilderFields: {},
queryCapabilities: {
requestType: Querybuildertypesv5RequestTypeDTO.time_series,

View File

@@ -1,8 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import {
Querybuildertypesv5RequestTypeDTO,
type TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import type { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
export type { QueryBuilderFieldsConfig } from 'components/QueryBuilderV2/queryBuilderFields.types';
@@ -44,24 +44,24 @@ export function isRawRequest(capabilities: PanelQueryCapabilities): boolean {
return capabilities.requestType === Querybuildertypesv5RequestTypeDTO.raw;
}
export enum QueryTypeKind {
export enum QueryModeKind {
SIGNAL = 'signal',
SIGNAL_LESS = 'signal-less',
}
export type QueryTypeCapability =
| { kind: QueryTypeKind.SIGNAL; signals: TelemetrytypesSignalDTO[] }
| { kind: QueryTypeKind.SIGNAL_LESS };
export type QueryModeCapability =
| { kind: QueryModeKind.SIGNAL; signals: TelemetrytypesSignalDTO[] }
| { kind: QueryModeKind.SIGNAL_LESS };
export type SupportedQueryTypesKindMap = Partial<
Record<EQueryType, QueryTypeCapability>
export type SupportedQueryModesKindMap = Partial<
Record<QueryMode, QueryModeCapability>
>;
/** Signals `queryType` accepts; empty for a signal-less or undeclared query type. */
export function getQueryTypeSignals(
queryTypes: SupportedQueryTypesKindMap | undefined,
queryType: EQueryType,
/** Signals `mode` accepts; empty for a signal-less or undeclared mode. */
export function getQueryModeSignals(
modes: SupportedQueryModesKindMap | undefined,
mode: QueryMode,
): TelemetrytypesSignalDTO[] {
const capability = queryTypes?.[queryType];
return capability?.kind === QueryTypeKind.SIGNAL ? capability.signals : [];
const capability = modes?.[mode];
return capability?.kind === QueryModeKind.SIGNAL ? capability.signals : [];
}

View File

@@ -13,7 +13,7 @@ import type { SyncedQueryBuilderField } from 'components/QueryBuilderV2/queryBui
import type {
PanelQueryCapabilities,
QueryBuilderFieldsConfig,
SupportedQueryTypesKindMap,
SupportedQueryModesKindMap,
} from './panelCapabilities';
import type {
BaseRendererProps,
@@ -99,6 +99,8 @@ export interface QueryEditorPaneProps {
panelDefinition: RenderableQueryPanelDefinition;
signal: TelemetrytypesSignalDTO;
isLoadingQueries: boolean;
/** Switch authoring tab (Query Builder / AI / ClickHouse / PromQL), owned by the host. */
onChangeQueryMode: (key: string) => void;
onStageRunQuery: () => void;
onCancelQuery: () => void;
/** Pin the tabs row to the pane top; the View modal opts out. */
@@ -120,7 +122,7 @@ export interface QueryPanelDefinition<
/** Lower editor pane — the shared query-builder pane, or a kind wrapper of it. */
EditorPane: ComponentType<QueryEditorPaneProps>;
/** Authoring modes (Query Builder / ClickHouse / PromQL / AI) and the signals each accepts. */
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
supportedQueryModesKindMap: SupportedQueryModesKindMap;
queryBuilderFields: QueryBuilderFieldsConfig;
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
queryCapabilities: PanelQueryCapabilities;

View File

@@ -4,16 +4,15 @@ import {
initialQueryAIWithType,
type PANEL_TYPES,
} from 'constants/queryBuilder';
import { getActiveBuilder } from 'lib/query/builderSlot';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import {
DataSource,
type QueryBuilderContextType,
} from 'types/common/queryBuilder';
interface SeedBuilderForModeArgs {
mode: EQueryType;
mode: QueryMode;
/** The kind's first Query Builder signal; what a fresh panel of this kind starts on. */
defaultSignal?: TelemetrytypesSignalDTO;
panelType: PANEL_TYPES;
@@ -27,14 +26,12 @@ export function seedBuilderForMode({
panelType,
updateAllQueriesOperators,
}: SeedBuilderForModeArgs): Query['builder'] {
if (mode === EQueryType.AI_QUERY_BUILDER) {
return getActiveBuilder(
updateAllQueriesOperators(
initialQueryAIWithType,
panelType,
DataSource.TRACES,
),
);
if (mode === QueryMode.AI_QUERY_BUILDER) {
return updateAllQueriesOperators(
initialQueryAIWithType,
panelType,
DataSource.TRACES,
).builder;
}
const dataSource = (defaultSignal ??
TelemetrytypesSignalDTO.metrics) as unknown as DataSource;

View File

@@ -31,6 +31,8 @@ interface QueryViewModalBodyProps {
draftApi: PanelEditorDraftApi;
/** Kind switch, owned by the shell (its cache must survive the fork swap). */
onChangePanelKind: (kind: PanelKind) => void;
/** Tab switch, owned by the shell alongside the kind switch it shares memory with. */
onChangeQueryMode: (key: string) => void;
}
/**
@@ -45,6 +47,7 @@ function QueryViewModalBody({
onClose,
draftApi,
onChangePanelKind,
onChangeQueryMode,
}: QueryViewModalBodyProps): JSX.Element | null {
const {
timeOverride,
@@ -162,6 +165,7 @@ function QueryViewModalBody({
panelDefinition={panelDefinition}
signal={signal}
isLoadingQueries={isFetching}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}
stickyHeader={false}

View File

@@ -8,7 +8,7 @@ import { QueryParams } from 'constants/query';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import useUrlQuery from 'hooks/useUrlQuery';
import { usePanelEditorDraft } from 'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelEditorDraft';
import { usePanelKindChange } from 'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindChange';
import { usePanelKindAndQueryModeSwitch } from 'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindAndQueryModeSwitch';
import { getPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/registry';
import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import { buildViewPanelSpec } from 'pages/DashboardPage/DashboardContainer/Panels/utils/drilldown/buildViewPanelSpec';
@@ -73,11 +73,12 @@ function ViewPanelModalContent({
const draftKind = draftApi.draft.spec.plugin.kind;
const panelDefinition = getPanelDefinition(draftKind);
const { onChangePanelKind } = usePanelKindChange({
spec: draftApi.draft.spec,
panelType: toPanelType(draftKind),
setSpec: draftApi.setSpec,
});
const { onChangePanelKind, onChangeQueryMode } =
usePanelKindAndQueryModeSwitch({
spec: draftApi.draft.spec,
panelType: toPanelType(draftKind),
setSpec: draftApi.setSpec,
});
if (panelDefinition.mode === 'static') {
return (
@@ -97,6 +98,7 @@ function ViewPanelModalContent({
onClose={onClose}
draftApi={draftApi}
onChangePanelKind={onChangePanelKind}
onChangeQueryMode={onChangeQueryMode}
/>
);
}

View File

@@ -39,9 +39,9 @@ jest.mock('hooks/useUrlQuery', () => ({
default: (): URLSearchParams => new URLSearchParams(),
}));
jest.mock(
'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindChange',
'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindAndQueryModeSwitch',
() => ({
usePanelKindChange: (): unknown => ({
usePanelKindAndQueryModeSwitch: (): unknown => ({
onChangePanelKind: jest.fn(),
}),
}),

View File

@@ -157,12 +157,6 @@ describe('useDrilldown', () => {
expect(result.current.enableDrillDown).toBe(true);
});
it('is true for an AI query', () => {
mockGetPanelQueryType.mockReturnValue(EQueryType.AI_QUERY_BUILDER);
const { result } = renderHook(() => useDrilldown(tsPanel, 'p1'));
expect(result.current.enableDrillDown).toBe(true);
});
it('is false when there is no builder query', () => {
mockGetBuilderQueries.mockReturnValue([]);
const { result } = renderHook(() => useDrilldown(tsPanel, 'p1'));

Some files were not shown because too many files have changed in this diff Show More