Compare commits

...

35 Commits

Author SHA1 Message Date
Abhi Kumar
9e0269e2bc test(query-builder): cover AI-slot quick filters and AI explorer links 2026-11-08 00:16:33 +05:30
Abhi Kumar
baa277d434 fix(dashboard): follow the AI slot in panel editor query sync and context-link variables 2026-11-08 00:16:26 +05:30
Abhi Kumar
54a6bc216d refactor(query-builder): pick AI field suggestions from the query type
Alert editors keep AI queries in `builder`, so the per-query tag still counts.
2026-11-06 17:51:10 +05:30
Abhi Kumar
c78bd4366c refactor(llm-observability): run the AI explorer on its own query type and slot
Older links with AI queries inside `builder` are moved into the slot. The
per-query tag is now only kept on AI queries that stay in `builder` (alerts);
`toWireQuery` still sets it on the wire.
2026-11-06 17:51:02 +05:30
Abhi Kumar
5b269e384b refactor(query-builder): read the active builder slot in quick filters and AI explorer helpers 2026-11-06 17:50:40 +05:30
Abhi Kumar
54ab158634 refactor(dashboard): move scatter panel and url normalizer tests onto EQueryType 2026-11-06 17:20:36 +05:30
Abhi Kumar
f78711e42b refactor(dashboard): remove the query-mode leftovers
The tab switch now lives in PanelEditorQueryBuilder, so the editor panes stop passing it down.
2026-11-06 17:18:19 +05:30
Abhi Kumar
03d0d3a9fb feat(dashboard): edit AI panels in their own builder slot
Saved AI panels open on the AI query type. A tab switch only changes queryType, so
the parked-builder stash goes; the kind switch rebuilds both slots. Drilldown and
create-alert keep reading the wire shape.
2026-11-06 17:18:19 +05:30
Abhi Kumar
084e17f527 feat(query-builder): render the active builder slot 2026-11-06 17:18:10 +05:30
Abhi Kumar
c19ddba548 feat(query-builder): edit the active builder slot in the provider
Builder edits follow queryType. The AI slot is filled on first use and only written
to the URL when present, so links without it load unchanged.
2026-11-06 17:18:02 +05:30
Abhi Kumar
ccbb3b89a6 refactor(query-builder): let AI queries through builder-only checks
An AI query is a builder query under its own type, so legends, labels, table columns,
variable references and drilldown treat it as one and read its slot.
2026-11-06 17:17:35 +05:30
Abhi Kumar
d6d4ff3df7 feat(query-builder): map AI composites into the AI slot 2026-11-06 17:17:26 +05:30
Abhi Kumar
377e31b014 feat(query-builder): add a builder slot for AI queries
AI still goes out as a builder query with builder_ai_query envelopes;
the backend has no AI query type.
2026-11-06 17:17:26 +05:30
Abhi Kumar
36fb1fc2d0 refactor(dashboard): fold QueryMode into EQueryType
AI joins EQueryType so tabs and query data share one enum; capability names follow it.
2026-11-06 17:17:26 +05:30
Abhi Kumar
4e4db33bdf style(e2e): format query-builder alerts spec 2026-11-05 23:47:26 +05:30
Abhi Kumar
c92ef6079a Merge remote-tracking branch 'origin/main' into test/qb-e2e
# Conflicts:
#	tests/e2e/helpers/alert-forms/shared.ts
#	tests/e2e/helpers/alerts/seeding.ts
2026-11-04 15:15:57 +05:30
Abhi Kumar
9b976c81b1 test(query-builder): expect a negative limit to drop its sign 2026-11-03 14:51:03 +05:30
Abhi Kumar
cb48993c8d Merge remote-tracking branch 'origin/fix/qb-add-query-signal' into HEAD 2026-11-03 14:51:03 +05:30
Abhi Kumar
07a59a2560 Merge remote-tracking branch 'origin/fix/qb-legacy-having-and-limit' into HEAD 2026-11-03 14:50:51 +05:30
Abhi Kumar
c3e0de9eb0 fix(query-builder): keep limit inputs to digits so the shown value is what's sent 2026-11-03 14:48:59 +05:30
Abhi Kumar
c31cb99273 test(query-builder): split the qb driver into modules 2026-11-03 12:39:52 +05:30
Abhi Kumar
5497146f98 test(query-builder): an added panel-editor query keeps the previous signal 2026-11-03 12:39:52 +05:30
Abhi Kumar
13625708a0 test(query-builder): run the legacy having and negative limit cases 2026-11-03 12:39:52 +05:30
Abhi Kumar
a415902357 test(query-builder): scope selectors to the query block and wait on own requests
Run waits on a request it started; panel-editor structural edits re-run
until the newest request matches, since their draft can lag the DOM.
2026-11-03 12:39:52 +05:30
Abhi Kumar
8d97b8ef74 test(query-builder): cover trace operator, signal switching, query tabs and run/cancel
holdQueryRange ignores a request cancelled while held: there is nothing
left to answer.
2026-11-03 12:39:52 +05:30
Abhi Kumar
9dda0825bd test(query-builder): cover panel-kind add-ons, reduce-to, multi-query and formulas 2026-11-03 12:39:52 +05:30
Abhi Kumar
99f9e634eb test(query-builder): cover group by, having, order, limit and legend add-ons
Negative limit stays fixme: the input accepts it and the backend
rejects it with a 400.
2026-11-03 12:39:52 +05:30
Abhi Kumar
7f1b6bd594 test(query-builder): cover metric-type defaults, aggregations and step interval
Gauge and histogram metrics are seeded per worker: the golden dataset
only has sums.
2026-11-03 12:39:52 +05:30
Abhi Kumar
98e6975358 test(query-builder): cover explorer-to-dashboard and alert trace operator round trips
pickChannelByName re-clicks while the select is still closed: a condition
re-render could close it as the click opened it.
2026-11-03 12:39:52 +05:30
Abhi Kumar
5b646396a9 test(query-builder): cover legacy conversion, history, IN lists and quick filters
Legacy having stays fixme: convertHavingToExpression keeps the stored
COUNT() casing and the v5 backend rejects it with a 400.
2026-11-03 12:39:52 +05:30
Abhi Kumar
4ced3b0c13 test(query-builder): add qb driver with url and filter editor specs
runShortcut waits two frames: a keypress in the same frame as the last
keystroke reads the pre-commit React state, which no user can produce.
2026-11-03 12:39:52 +05:30
Abhi Kumar
3e20fb09f7 test(query-builder): split data-source tests and pin source to the last query 2026-11-03 12:39:51 +05:30
Abhi Kumar
dffb844621 fix(query-builder): added query follows the previous query's signal
Hosts with a fixed data source keep it; only the panel editor, which
has none, changes from always-metrics to the last query's signal.
2026-11-03 12:39:51 +05:30
Abhi Kumar
117183b172 test(query-builder): type the limit through userEvent 2026-11-03 12:39:50 +05:30
Abhi Kumar
6c490be536 fix(query-builder): legacy having casing and non-positive limits
Legacy having referenced COUNT(), which v5 rejects; only the function
name is lower-cased since attribute keys are case-sensitive. Limits
accept positive integers only, for queries and formulas.
2026-11-03 12:39:50 +05:30
130 changed files with 4242 additions and 1425 deletions

View File

@@ -903,6 +903,40 @@ 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,6 +3,7 @@
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';
@@ -559,9 +560,15 @@ export const prepareQueryRangePayloadV5 = (
const requestType = mapPanelTypeToRequestType(graphType);
let queries: QueryEnvelope[] = [];
switch (query.queryType) {
const wireQuery = toWireQuery(query);
switch (wireQuery.queryType) {
case EQueryType.QUERY_BUILDER: {
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
const {
queryData: data,
queryFormulas,
queryTraceOperator,
} = wireQuery.builder;
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
@@ -623,13 +630,13 @@ export const prepareQueryRangePayloadV5 = (
break;
}
case EQueryType.PROM: {
const promQueries = reduceQueriesToObject(query[query.queryType]);
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
queries = convertPromQueriesToV5(promQueries.queries);
legendMap = promQueries.legends;
break;
}
case EQueryType.CLICKHOUSE: {
const chQueries = reduceQueriesToObject(query[query.queryType]);
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
queries = convertClickHouseQueriesToV5(chQueries.queries);
legendMap = chQueries.legends;
break;

View File

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

View File

@@ -1,4 +1,5 @@
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';
@@ -15,28 +16,25 @@ 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 = currentQuery.builder.queryData.length > 1;
const hasMultipleQueries = activeBuilder.queryData.length > 1;
const hasTraceOperator =
currentQuery.builder.queryTraceOperator &&
currentQuery.builder.queryTraceOperator.length > 0;
activeBuilder.queryTraceOperator &&
activeBuilder.queryTraceOperator.length > 0;
return isRawQueryPanel && hasMultipleQueries && !hasTraceOperator;
}, [
currentQuery?.builder?.queryData,
currentQuery?.builder?.queryTraceOperator,
panelType,
]);
}, [activeBuilder.queryData, activeBuilder.queryTraceOperator, panelType]);
const traceOperatorWarning = useMemo(() => {
if (currentQuery.builder.queryData.length === 0) {
if (activeBuilder.queryData.length === 0) {
return '';
}
const firstQuery = currentQuery.builder.queryData[0];
const firstQuery = activeBuilder.queryData[0];
return `Currently, you are only seeing results from query ${firstQuery.queryName}. Add a trace operator to combine results of multiple queries.`;
}, [currentQuery]);
}, [activeBuilder.queryData]);
return (
<div className="qb-trace-operator-button-container">
<Tooltip

View File

@@ -26,6 +26,7 @@ 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';
@@ -132,6 +133,7 @@ function QuerySearch({
metricNamespace,
valueSuggestionsOverride,
}: QuerySearchProps): JSX.Element {
const builderQueryType = useBuilderQueryType(queryData);
const isDarkMode = useIsDarkMode();
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
const [activeKey, setActiveKey] = useState<string>('');
@@ -330,7 +332,7 @@ function QuerySearch({
source: signalSource as TelemetrytypesSourceDTO,
metricNamespace,
},
queryData.builderQueryType,
builderQueryType,
);
if (response.data.keys) {
@@ -368,7 +370,7 @@ function QuerySearch({
hardcodedAttributeKeys,
showFilterSuggestionsWithoutMetric,
metricNamespace,
queryData.builderQueryType,
builderQueryType,
],
);
@@ -510,7 +512,7 @@ function QuerySearch({
source: signalSource as TelemetrytypesSourceDTO,
metricName: debouncedMetricName ?? undefined,
},
queryData.builderQueryType,
builderQueryType,
).then((response) => {
const responseData = response.data;
const responseDataValues = responseData.values;
@@ -613,7 +615,7 @@ function QuerySearch({
signalSource,
toggleSuggestions,
valueSuggestionsOverride,
queryData.builderQueryType,
builderQueryType,
],
);

View File

@@ -12,6 +12,7 @@ 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';
@@ -62,7 +63,8 @@ export const QueryV2 = forwardRef(function QueryV2(
const { cloneQuery, panelType } = useQueryBuilder();
const hasQueryFunctions = query?.functions?.length > 0;
const { dataSource, builderQueryType } = query;
const { dataSource } = query;
const builderQueryType = useBuilderQueryType(query);
const [isCollapsed, setIsCollapsed] = useState(false);

View File

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

View File

@@ -0,0 +1,71 @@
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: QueryMode.AI_QUERY_BUILDER,
builderQueryType: EQueryType.AI_QUERY_BUILDER,
})),
},
};

View File

@@ -1,13 +1,16 @@
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
import {
BaseAutocompleteData,
DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
import { extractQueryPairs } from 'utils/queryContextUtils';
import {
adjustQueryForV5,
convertAggregationToExpression,
convertExpressionToFilters,
convertFiltersToExpression,
@@ -1684,3 +1687,31 @@ 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,4 +1,5 @@
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';
@@ -20,7 +21,8 @@ export function useSyncQueryBuilderFields(
fields: SyncedQueryBuilderField[] = NO_FIELDS,
): void {
const { currentQuery, handleSetQueryData } = useQueryBuilder();
const queries = currentQuery.builder.queryData;
const activeBuilder = getActiveBuilder(currentQuery);
const queries = activeBuilder.queryData;
const previousRef = useRef<Map<string, IBuilderQuery>>();
const enabled = fields.length > 0;

View File

@@ -9,6 +9,11 @@ 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,
@@ -33,7 +38,6 @@ 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';
@@ -1030,8 +1034,9 @@ export function getQueryLabelWithAggregation(
}
export const adjustQueryForV5 = (currentQuery: Query): Query => {
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
const newQueryData = currentQuery.builder.queryData.map((query) => {
if (isBuilderQueryType(currentQuery.queryType)) {
const activeBuilder = getActiveBuilder(currentQuery);
const newQueryData = activeBuilder.queryData.map((query) => {
const aggregations = query.aggregations?.map((aggregation) => {
if (query.dataSource === DataSource.METRICS) {
const metricAggregation = aggregation as MetricAggregation;
@@ -1072,8 +1077,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
return {
...currentQuery,
builder: {
...currentQuery.builder,
[getBuilderSlotKey(currentQuery.queryType)]: {
...activeBuilder,
queryData: newQueryData,
},
};

View File

@@ -4,6 +4,8 @@ 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,6 +3,7 @@ import {
convertFiltersToExpressionWithExistingQuery,
} from 'components/QueryBuilderV2/utils';
import { QuickFiltersSource } from 'components/QuickFilters/types';
import { EQueryType } from 'types/common/dashboard';
import {
Query,
TagFilter,
@@ -556,3 +557,49 @@ 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,6 +10,11 @@ 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';
@@ -157,8 +162,9 @@ export function applyCheckboxToggle({
previousState?: CheckedState;
sectionType?: SectionType;
}): Query {
const activeItems =
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
const slot = getBuilderSlotKey(currentQuery.queryType);
const builder = getBuilderAt(currentQuery, slot);
const activeItems = builder.queryData?.[activeQueryIndex]?.filters?.items;
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
@@ -173,7 +179,7 @@ export function applyCheckboxToggle({
const isMultipleValuesTrueForTheKey =
Object.values(currentFilterState).filter((val) => val).length > 1;
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
const query = cloneDeep(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.
@@ -430,18 +436,15 @@ export function applyCheckboxToggle({
query.filters = synced.filters;
}
return {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: [
...currentQuery.builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
},
};
return withBuilderAt(currentQuery, slot, {
...builder,
queryData: [
...builder.queryData.map((q, idx) => {
if (idx === activeQueryIndex) {
return query;
}
return q;
}),
],
});
}

View File

@@ -1,6 +1,7 @@
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';
@@ -37,12 +38,13 @@ function useCheckboxDisclosure({
DEFAULT_VISIBLE_ITEMS_COUNT,
);
const { queryData } = getActiveBuilder(currentQuery);
const isSomeFilterPresentForCurrentAttribute = useMemo(
() =>
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
!!queryData?.[activeQueryIndex]?.filters?.items?.some((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
),
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
[queryData, activeQueryIndex, filter.attributeKey.key],
);
const isOpen = useMemo(() => {

View File

@@ -4,6 +4,7 @@ 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';
@@ -77,7 +78,7 @@ function useCheckboxFilterActions({
dispatch(updatedQuery);
if (onQuickFilterChange) {
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
const queryData = getActiveBuilder(updatedQuery).queryData[activeQueryIndex];
const expression = queryData?.filter?.expression || '';
const filterItemKeys = (queryData?.filters?.items || [])
.map((item) => item.key?.key)

View File

@@ -1,6 +1,7 @@
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';
@@ -31,33 +32,28 @@ 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:
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
filterItems: activeQueryData?.[activeQueryIndex]?.filters?.items,
filterKey: filter.attributeKey.key,
}),
[
attributeValues,
currentQuery?.builder.queryData,
filter.attributeKey,
activeQueryIndex,
],
[attributeValues, activeQueryData, filter.attributeKey, activeQueryIndex],
);
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
const isFilterDisabled = useMemo(
() =>
(currentQuery?.builder?.queryData?.[
activeQueryIndex
]?.filters?.items?.filter((item) =>
(activeQueryData?.[activeQueryIndex]?.filters?.items?.filter((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
)?.length || 0) > 1,
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
[activeQueryData, activeQueryIndex, filter.attributeKey],
);
// whether the current filter has multiple values to its name in the key op value section

View File

@@ -11,6 +11,7 @@ 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';
@@ -128,18 +129,15 @@ export default function CheckboxFilterV2(
onToggleOpen();
};
const activeQueryData = currentQuery
? getActiveBuilder(currentQuery).queryData
: undefined;
const currentFilterOp = useMemo(() => {
const filterSync = currentQuery?.builder.queryData?.[
activeQueryIndex
]?.filters?.items.find((item) =>
isKeyMatch(item.key?.key, filter.attributeKey.key),
const filterSync = activeQueryData?.[activeQueryIndex]?.filters?.items.find(
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
);
return filterSync?.op;
}, [
currentQuery?.builder.queryData,
activeQueryIndex,
filter.attributeKey.key,
]);
}, [activeQueryData, activeQueryIndex, filter.attributeKey.key]);
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');

View File

@@ -1,5 +1,6 @@
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';
@@ -18,6 +19,9 @@ export function useExistingQuery({
activeQueryIndex,
}: UseExistingQueryParams): UseExistingQueryResult {
const { stagedQuery } = useQueryBuilder();
const stagedQueryData = stagedQuery
? getActiveBuilder(stagedQuery).queryData
: undefined;
const existingQuery = useMemo(() => {
if (useFieldApis.existingQuery === null) {
@@ -28,7 +32,7 @@ export function useExistingQuery({
return useFieldApis.existingQuery;
}
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
const queryData = stagedQueryData?.[activeQueryIndex];
// Prefer V5 filter.expression
if (queryData?.filter?.expression) {
@@ -41,21 +45,17 @@ export function useExistingQuery({
}
return undefined;
}, [
useFieldApis.existingQuery,
stagedQuery?.builder.queryData,
activeQueryIndex,
]);
}, [useFieldApis.existingQuery, stagedQueryData, 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 = stagedQuery?.builder.queryData?.[activeQueryIndex];
const queryData = stagedQueryData?.[activeQueryIndex];
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
const hasV5Expression = !!queryData?.filter?.expression;
return hasV3Items || hasV5Expression || !!existingQuery;
}, [stagedQuery?.builder.queryData, activeQueryIndex, existingQuery]);
}, [stagedQueryData, activeQueryIndex, existingQuery]);
return { existingQuery, hasExistingQuery };
}

View File

@@ -9,6 +9,12 @@ 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,
@@ -57,7 +63,8 @@ function Duration({
// eslint-disable-next-line sonarjs/cognitive-complexity
const syncSelectedFilters = useMemo((): FilterType => {
const filters =
compositeQuery?.builder.queryData?.[activeQueryIndex]?.filters;
compositeQuery &&
getActiveBuilder(compositeQuery).queryData?.[activeQueryIndex]?.filters;
if (!filters) {
return {} as FilterType;
}
@@ -183,49 +190,51 @@ function Duration({
};
const removeFilterItemIds = (query: Query): Query => {
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 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 handleRun = useCallback(
(props?: HandleRunProps): void => {
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 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 currentQueryWithoutIds = removeFilterItemIds(currentQuery);
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);

View File

@@ -1,4 +1,9 @@
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';
@@ -20,34 +25,33 @@ export function clearFilterFromQuery({
activeQueryIndex: number;
operators?: string[];
}): Query {
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',
},
};
}),
},
};
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',
},
};
}),
});
}

View File

@@ -32,6 +32,12 @@ 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';
@@ -87,15 +93,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,
(currentQuery?.builder?.queryData?.length || 1) - 1,
),
[lastUsedQuery, currentQuery?.builder?.queryData?.length],
Math.min(lastUsedQuery || 0, (activeBuilder?.queryData?.length || 1) - 1),
[lastUsedQuery, activeBuilder?.queryData?.length],
);
// Determine if we're in ListView mode
@@ -103,15 +109,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
// Generate query options based on available queries
const queryOptions = useMemo(() => {
if (!currentQuery?.builder?.queryData) {
if (!activeBuilder?.queryData) {
return [];
}
return currentQuery.builder.queryData.map((query, index) => ({
return activeBuilder.queryData.map((query, index) => ({
label: query.queryName || String.fromCharCode(65 + index),
value: index,
}));
}, [currentQuery?.builder?.queryData]);
}, [activeBuilder?.queryData]);
// Show dropdown in ListView only for TRACES_EXPLORER source
const shouldShowDropdownInListView =
@@ -127,36 +133,35 @@ 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 = currentQuery?.builder.queryData?.[activeQueryIndex];
const activeQuery = activeBuilder?.queryData?.[activeQueryIndex];
if (!activeQuery) {
return;
}
const preparedQuery: Query = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item, idx) => {
if (idx !== activeQueryIndex) {
return item;
}
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,
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);
@@ -166,13 +171,12 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
};
const lastQueryName =
showQueryName &&
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
showQueryName && activeBuilder?.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 && currentQuery.builder.queryData?.[0]?.queryName
? showQueryName && activeBuilder?.queryData?.[0]?.queryName
: lastQueryName;
const handleQueryChange = (value: number): void => {

View File

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

View File

@@ -13,6 +13,11 @@ 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. */
@@ -56,12 +61,11 @@ export function replaceFirstQueryData(
query: Query,
updated: IBuilderQuery,
): Query {
const queryData = [...query.builder.queryData];
const slot = getBuilderSlotKey(query.queryType);
const builder = getBuilderAt(query, slot);
const queryData = [...builder.queryData];
queryData[0] = updated;
return {
...query,
builder: { ...query.builder, queryData },
};
return withBuilderAt(query, slot, { ...builder, queryData });
}
/** Page-state deps required by the runQuery / addFilter actions. */

View File

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

View File

@@ -20,6 +20,7 @@ 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';
@@ -177,7 +178,9 @@ function Explorer(): JSX.Element {
const isFilterApplied = useMemo(() => {
// if any of the non-disabled queries has filters applied, return true
const result = stagedQuery?.builder?.queryData?.filter(
const result = (
stagedQuery ? getActiveBuilder(stagedQuery) : undefined
)?.queryData?.filter(
(item) => !isEmpty(item.filters?.items) && !item.disabled,
);
return !!result?.length;

View File

@@ -15,6 +15,7 @@ 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';
@@ -35,7 +36,7 @@ function TimeSeriesViewContainer({
const isValidToConvertToMs = useMemo(() => {
const isValid: boolean[] = [];
currentQuery.builder.queryData.forEach(
getActiveBuilder(currentQuery).queryData.forEach(
({ aggregateAttribute, aggregateOperator }) => {
const isExistDurationNanoAttribute =
aggregateAttribute?.key === 'durationNano' ||

View File

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

View File

@@ -1,4 +1,5 @@
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';
@@ -15,8 +16,9 @@ export const getListViewQuery = (
? [parseListOrderBy(orderBy)]
: [];
for (let i = 0; i < query.builder.queryData.length; i++) {
const queryData = query.builder.queryData[i];
const { queryData: builderQueries } = getActiveBuilder(query);
for (let i = 0; i < builderQueries.length; i++) {
const queryData = builderQueries[i];
queryData.groupBy = [];
queryData.having = {
expression: '',

View File

@@ -29,6 +29,7 @@ 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,
@@ -45,6 +46,7 @@ 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],
@@ -52,7 +54,7 @@ export const GroupByFilter = memo(function GroupByFilter({
metricName: getMetricNameForQuery(query),
searchText: debouncedValue,
},
query.builderQueryType,
builderQueryType,
{ enabled: !disabled && isFocused, keepPreviousData: true },
);

View File

@@ -14,6 +14,7 @@ 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,
@@ -34,6 +35,7 @@ export function OrderByFilter({
handleSearchKeys,
} = useOrderByFilter({ query, onChange, entityVersion });
const builderQueryType = useBuilderQueryType(query);
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
{
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
@@ -41,7 +43,7 @@ export function OrderByFilter({
metricName: getMetricNameForQuery(query),
searchText: debouncedSearchText,
},
query.builderQueryType,
builderQueryType,
{
enabled: !!query.aggregateAttribute?.key || isRawQuery,
keepPreviousData: true,

View File

@@ -9,6 +9,7 @@ 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 {
@@ -856,14 +857,10 @@ function QueryBuilderSearchV2(
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [query]);
const isLastQuery = useMemo(
() =>
isEqual(
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
query,
),
[currentQuery, query],
);
const isLastQuery = useMemo(() => {
const { queryData } = getActiveBuilder(currentQuery);
return isEqual(queryData[queryData.length - 1], query);
}, [currentQuery, query]);
useEffect(() => {
if (isLastQuery) {

View File

@@ -2,6 +2,7 @@ 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';
@@ -41,7 +42,7 @@ export function useGraphContextMenu({
} {
const drilldownQuery = useGetCompositeQueryParam() || query;
const isQueryTypeBuilder = drilldownQuery?.queryType === 'builder';
const isQueryTypeBuilder = isBuilderQueryType(drilldownQuery?.queryType);
const { aggregateDrilldownConfig } = useAggregateDrilldown({
query: drilldownQuery,

View File

@@ -3,6 +3,7 @@ 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,
@@ -33,7 +34,7 @@ export function QueryTable({
...props
}: QueryTableProps): JSX.Element {
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
const isQueryTypeBuilder = query.queryType === 'builder';
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
const { servicename: encodedServiceName } = useAppParams<'servicename'>();
const servicename = decodeURIComponent(encodedServiceName || '');

View File

@@ -1,6 +1,7 @@
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';
@@ -29,6 +30,21 @@ 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: (widget?.query?.builder?.queryData &&
Array.isArray(widget.query.builder.queryData)
? widget.query.builder.queryData
: []
).some(
isButtonEnabled: (Array.isArray(widgetQueryData) ? widgetQueryData : []).some(
(q) =>
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
),
@@ -219,7 +219,7 @@ function WidgetGraphComponent({
customOnDragSelect={customOnDragSelect}
setCurrentGraphRef={setCurrentGraphRef}
enableDrillDown={
enableDrillDown && widget?.query?.queryType === EQueryType.QUERY_BUILDER
enableDrillDown && isBuilderQueryType(widget?.query?.queryType)
}
/>
</Modal>

View File

@@ -1,4 +1,5 @@
import { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
createColumnsAndDataSource,
@@ -47,6 +48,18 @@ 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,6 +4,7 @@ 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';
@@ -119,14 +120,15 @@ 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 = currentQuery?.builder?.queryData?.find(
legend = getActiveBuilder(currentQuery)?.queryData?.find(
(query) => query.queryName === queryName,
)?.legend;
if (!legend) {
// check if the value is present in the formula
legend = currentQuery?.builder?.queryFormulas?.find(
legend = getActiveBuilder(currentQuery)?.queryFormulas?.find(
(query) => query.queryName === queryName,
)?.legend;
}
@@ -199,7 +201,7 @@ export function createColumnsAndDataSource(
: undefined;
const isNewAggregation =
currentQuery?.builder?.queryData?.find(
getActiveBuilder(currentQuery).queryData?.find(
(query) => query.queryName === item.queryName,
)?.aggregations?.length || 0;

View File

@@ -6,6 +6,7 @@ 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 {
@@ -72,8 +73,7 @@ function GridValueComponent({
);
}
const isQueryTypeBuilder =
widget?.query?.queryType === EQueryType.QUERY_BUILDER;
const isQueryTypeBuilder = isBuilderQueryType(widget?.query?.queryType);
return (
<>

View File

@@ -58,6 +58,27 @@ 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

@@ -0,0 +1,38 @@
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,6 +2,7 @@ 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';
@@ -105,18 +106,25 @@ describe('useGetCompositeQueryParam', () => {
},
});
const { QUERY_BUILDER, AI_QUERY_BUILDER } = EQueryType;
[
[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]) => {
[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]) => {
const query = renderOn(clickhouseWithOperator, route);
const builder = query && getActiveBuilder(query);
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([]);
expect(query?.queryType).toBe(queryType);
expect(builder?.queryData[0].dataSource).toBe(dataSource);
expect(builder?.queryData[0].source ?? '').toBe(source);
expect(builder?.queryTraceOperator).toStrictEqual([]);
});
});

View File

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

View File

@@ -8,11 +8,8 @@ import {
initialQueryState,
} from 'constants/queryBuilder';
import ROUTES from 'constants/routes';
import {
IBuilderQuery,
Query,
QueryMode,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
@@ -44,8 +41,9 @@ function firstBuilderQuery(
queryData: unknown[],
pathname: string,
): IBuilderQuery {
return normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname)
.query.builder.queryData[0];
return getActiveBuilder(
normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname).query,
).queryData[0];
}
function without(builderQuery: object, key: keyof IBuilderQuery): object {
@@ -62,9 +60,11 @@ describe('normalizeCompositeQuery', () => {
queryType: EQueryType.CLICKHOUSE,
extra: 'kept',
};
const aiQuery = withBuilderQueries(initialQueriesMap.traces, [
{ ...tracesQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER },
]);
const aiQuery: Query = {
...initialQueriesMap.traces,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: initialQueriesMap.traces.builder,
};
[NOT_AN_EXPLORER, ROUTES.LOGS_EXPLORER].forEach((pathname) =>
expect(
@@ -86,6 +86,33 @@ 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,
@@ -209,7 +236,7 @@ describe('normalizeCompositeQuery', () => {
expect(
firstBuilderQuery(
logs,
[{ ...logsQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER }],
[{ ...logsQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER }],
ROUTES.LOGS_EXPLORER,
).builderQueryType,
).toBeUndefined();
@@ -220,7 +247,7 @@ describe('normalizeCompositeQuery', () => {
{
...tracesQuery,
queryName: 'D',
builderQueryType: QueryMode.AI_QUERY_BUILDER,
builderQueryType: EQueryType.AI_QUERY_BUILDER,
...llmFilter,
},
],
@@ -243,10 +270,7 @@ describe('normalizeCompositeQuery', () => {
).toBe(TelemetrytypesSourceDTO.meter);
expect(
firstBuilderQuery(traces, [{ ...tracesQuery, ...llmFilter }], ai),
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
...llmFilter,
});
).toStrictEqual({ ...tracesQuery, ...llmFilter });
expect(
firstBuilderQuery(
traces,
@@ -260,17 +284,13 @@ describe('normalizeCompositeQuery', () => {
],
ai,
),
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
queryName: 'B',
filter: { expression: '' },
});
).toStrictEqual({ ...tracesQuery, queryName: 'B', ...llmFilter });
expect(
firstBuilderQuery(traces, [{ ...logsQuery, queryName: 'C' }], ai),
).toMatchObject({
builderQueryType: QueryMode.AI_QUERY_BUILDER,
dataSource: DataSource.TRACES,
queryName: 'C',
filter: { expression: '' },
});
});

View File

@@ -7,8 +7,10 @@ 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 {
@@ -78,6 +80,14 @@ 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;
}
}
/**
@@ -93,8 +103,21 @@ function isListOfObjects(value: unknown): boolean {
}
function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
// Explorers only have the query builder: no PromQL or ClickHouse editor.
query.queryType = EQueryType.QUERY_BUILDER;
// 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);
}
if (query.promql.some((promql) => promql.query !== '')) {
query.promql = cloneDeep(initialQueryState.promql);
@@ -105,10 +128,10 @@ function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
}
if (!rules.allowsTraceOperator) {
query.builder.queryTraceOperator = [];
builder.queryTraceOperator = [];
}
query.builder.queryData = query.builder.queryData.map((builderQuery) =>
builder.queryData = builder.queryData.map((builderQuery) =>
fitBuilderQueryToExplorer(builderQuery, rules),
);
}
@@ -148,12 +171,9 @@ function fillMissingSignal(
builderQuery.source = rules.source;
}
// 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;
// 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;
}
}
@@ -169,15 +189,11 @@ function belongsToExplorer(
return false;
}
const builderQueryType =
builderQuery.builderQueryType ??
Querybuildertypesv5QueryTypeDTO.builder_query;
if (builderQueryType !== rules.builderQueryType) {
return false;
}
return true;
// Only the AI explorer runs AI queries.
return (
builderQuery.builderQueryType !==
Querybuildertypesv5QueryTypeDTO.builder_ai_query
);
}
function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
@@ -186,11 +202,5 @@ function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
);
builderQuery.source = rules.source;
if (
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
) {
builderQuery.builderQueryType = rules.builderQueryType;
}
return builderQuery;
}

View File

@@ -0,0 +1,18 @@
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,6 +7,7 @@ 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,
@@ -488,13 +489,15 @@ export const useQueryOperations: UseQueryOperations = ({
],
);
const activeQueryCount = getActiveBuilder(currentQuery).queryData.length;
const handleDeleteQuery = useCallback(() => {
if (currentQuery.builder.queryData.length > 1) {
if (activeQueryCount > 1) {
removeQueryBuilderEntityByIndex('queryData', index);
}
setLastUsedQuery(0);
}, [
currentQuery.builder.queryData.length,
activeQueryCount,
setLastUsedQuery,
removeQueryBuilderEntityByIndex,
index,

View File

@@ -67,6 +67,23 @@ 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,5 +1,6 @@
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';
@@ -7,11 +8,12 @@ export const useGetQueryLabels = (
currentQuery: Query,
): { label: string; value: string }[] =>
useMemo(() => {
if (currentQuery?.queryType === EQueryType.QUERY_BUILDER) {
if (currentQuery && isBuilderQueryType(currentQuery.queryType)) {
const activeBuilder = getActiveBuilder(currentQuery);
const queryLabels = getQueryLabelWithAggregation(
currentQuery?.builder?.queryData || [],
activeBuilder?.queryData || [],
);
const formulaLabels = (currentQuery?.builder?.queryFormulas ?? []).map(
const formulaLabels = (activeBuilder?.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 { EQueryType } from 'types/common/dashboard';
import { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
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 (query.queryType !== 'builder') {
if (!isBuilderQueryType(query.queryType)) {
return true; // Non-builder queries don't need this validation
}
const { queryData } = query.builder;
const { queryData } = getActiveBuilder(query);
// Check if any METRICS data source queries exist
const metricsQueries = queryData.filter(
@@ -124,14 +124,15 @@ export const getLegend = (
labelName: string,
): string => {
// For non-query builder queries, return the label name directly
if (payloadQuery.queryType !== EQueryType.QUERY_BUILDER) {
if (!isBuilderQueryType(payloadQuery.queryType)) {
return labelName;
}
const activeBuilder = getActiveBuilder(payloadQuery);
// Combine queryData and queryTraceOperator
const allQueries = [
...(payloadQuery?.builder?.queryData || []),
...(payloadQuery?.builder?.queryTraceOperator || []),
...(activeBuilder?.queryData || []),
...(activeBuilder?.queryTraceOperator || []),
];
const aggregationPerQuery = allQueries.reduce((acc, query) => {

View File

@@ -161,6 +161,35 @@ 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,6 +1,8 @@
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:
@@ -50,9 +52,11 @@ export function containsAnyVariableReference(text: string): boolean {
return !!text && ANY_VARIABLE_REFERENCE.test(text);
}
function extractQueryBuilderTexts(query: Query): string[] {
function extractQueryBuilderTexts(
builder: QueryBuilderData | undefined,
): string[] {
const texts: string[] = [];
const queryDataList = query.builder?.queryData;
const queryDataList = builder?.queryData;
if (isArray(queryDataList)) {
queryDataList.forEach((queryData) => {
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
@@ -102,13 +106,13 @@ function extractClickhouseSQLTexts(query: Query): string[] {
/**
* Extracts all text strings from a widget Query that could contain variable
* references. Covers:
* - QUERY_BUILDER: filter items' string values + filter.expression
* - QUERY_BUILDER / AI_QUERY_BUILDER: filter items' string values + filter.expression
* - PROM: each promql[].query
* - CLICKHOUSE: each clickhouse_sql[].query
*/
export function extractQueryTextStrings(query: Query): string[] {
if (query.queryType === EQueryType.QUERY_BUILDER) {
return extractQueryBuilderTexts(query);
if (isBuilderQueryType(query.queryType)) {
return extractQueryBuilderTexts(getActiveBuilder(query));
}
if (query.queryType === EQueryType.PROM) {

View File

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

View File

@@ -15,6 +15,7 @@ import {
PromQuery,
QueryBuilderFormula,
} from 'types/api/v5/queryRange';
import { EQueryType } from 'types/common/dashboard';
import {
convertBuilderQueryToIBuilderQuery,
convertQueryBuilderFormulaToIBuilderFormula,
@@ -37,6 +38,7 @@ 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;
@@ -49,10 +51,13 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
}
} else if (q.type === 'builder_ai_query') {
if (spec.name) {
builderQueries[spec.name] = {
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
builderQueryType: 'builder_ai_query',
};
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' };
builderQueryTypes[spec.name] = 'builder_ai_query';
}
} else if (q.type === 'builder_formula') {
@@ -87,8 +92,14 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
});
}
});
const builder = transformQueryBuilderDataModel(
builderQueries,
builderQueryTypes,
);
return {
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
...(isAIQuery && { builder_ai_query: builder }),
promql: promQueries,
clickhouse_sql: clickhouseQueries,
queryType: compositeQuery.queryType,

View File

@@ -0,0 +1,93 @@
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

@@ -0,0 +1,73 @@
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,6 +12,7 @@ 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,
@@ -93,8 +94,8 @@ const getQueryByName = <T extends keyof QueryBuilderData>(
queryArray.find((q) => q.name === currentQueryName) || defaultQueryValue
);
}
if (query.queryType === EQueryType.QUERY_BUILDER) {
const queryArray = (query.builder[type] || []) as (
if (isBuilderQueryType(query.queryType)) {
const queryArray = (getActiveBuilder(query)[type] || []) as (
| IBuilderQuery
| IBuilderFormula
)[];
@@ -173,7 +174,7 @@ const addOperatorFormulaColumns = (
return;
}
if (queryType === EQueryType.QUERY_BUILDER) {
if (isBuilderQueryType(queryType)) {
const currentQueryData = query as IBuilderQuery;
let operatorLabel = `${currentQueryData.aggregateOperator}`;
if (currentQueryData.aggregateAttribute?.key) {

View File

@@ -1,51 +0,0 @@
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,10 +1,9 @@
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';
@@ -33,7 +32,7 @@ function ConfigActions({
//TODO: AI query check should be removed after we support alerts on ai query @tewarig
if (
!actions.createAlert ||
getQueryMode(currentQuery) === QueryMode.AI_QUERY_BUILDER
currentQuery.queryType === EQueryType.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',
supportedQueryModesKindMap: Object.fromEntries(
supportedQueryTypesKindMap: Object.fromEntries(
queryTypes.map((queryType) => [
queryType,
queryType === EQueryType.QUERY_BUILDER

View File

@@ -25,6 +25,25 @@ 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,6 +12,7 @@ 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,5 +1,6 @@
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';
@@ -20,13 +21,16 @@ 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>();
(currentQuery?.builder?.queryData ?? []).forEach((query) => {
(activeQueryData ?? []).forEach((query) => {
(query.groupBy ?? []).forEach((field) => {
if (field.key) {
names.add(`_${field.key}`);
@@ -34,7 +38,7 @@ export function useContextLinkVariables(): VariableItem[] {
});
});
return Array.from(names);
}, [currentQuery?.builder?.queryData]);
}, [activeQueryData]);
return useMemo(
() => [

View File

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

View File

@@ -1,3 +1,4 @@
import { EQueryType } from 'types/common/dashboard';
import {
type KeyboardEvent,
type ReactNode,
@@ -23,22 +24,20 @@ import { useSyncQueryBuilderFields } from 'components/QueryBuilderV2/useSyncQuer
import { DataSource } from 'types/common/queryBuilder';
import {
getQueryModeSignals,
getQueryTypeSignals,
isRawRequest,
} from '../../Panels/types/panelCapabilities';
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
import { toPanelType } from '../../Panels/types/panelKind';
import { getQueryMode, isBuilderMode } from 'lib/query/queryMode';
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { isBuilderQueryType } from 'lib/query/builderSlot';
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. */
@@ -58,7 +57,6 @@ interface PanelEditorQueryBuilderProps {
*/
function PanelEditorQueryBuilder({
panelDefinition,
onChangeQueryMode,
isLoadingQueries,
onStageRunQuery,
onCancelQuery,
@@ -68,6 +66,10 @@ 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);
@@ -99,10 +101,10 @@ function PanelEditorQueryBuilder({
const items = useMemo(() => {
const queryTypeComponents: Record<
QueryMode,
EQueryType,
{ icon: ReactNode; label: string; component: ReactNode }
> = {
[QueryMode.QUERY_BUILDER]: {
[EQueryType.QUERY_BUILDER]: {
icon: <Atom size={14} />,
label: 'Query Builder',
component: (
@@ -110,9 +112,9 @@ function PanelEditorQueryBuilder({
<QueryBuilderV2
panelType={panelType}
fieldsConfig={panelDefinition.queryBuilderFields}
allowedDataSources={getQueryModeSignals(
panelDefinition.supportedQueryModesKindMap,
QueryMode.QUERY_BUILDER,
allowedDataSources={getQueryTypeSignals(
panelDefinition.supportedQueryTypesKindMap,
EQueryType.QUERY_BUILDER,
)}
showTraceOperator={!isRawQuery}
version="v3"
@@ -123,12 +125,12 @@ function PanelEditorQueryBuilder({
</div>
),
},
[QueryMode.CLICKHOUSE]: {
[EQueryType.CLICKHOUSE]: {
icon: <Terminal size={14} />,
label: 'ClickHouse Query',
component: <ClickHouseQueryContainer />,
},
[QueryMode.PROM]: {
[EQueryType.PROM]: {
icon: (
<PromQLIcon
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
@@ -137,7 +139,7 @@ function PanelEditorQueryBuilder({
label: 'PromQL',
component: <PromQLQueryContainer />,
},
[QueryMode.AI_QUERY_BUILDER]: {
[EQueryType.AI_QUERY_BUILDER]: {
icon: <Brain size={14} />,
label: 'AI Query Builder',
component: (
@@ -160,8 +162,8 @@ function PanelEditorQueryBuilder({
};
const modes = Object.keys(
panelDefinition.supportedQueryModesKindMap,
) as QueryMode[];
panelDefinition.supportedQueryTypesKindMap,
) as EQueryType[];
return modes.map((queryType) => ({
key: queryType,
@@ -176,7 +178,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: isBuilderMode(queryType),
destroyInactiveTabPane: isBuilderQueryType(queryType),
}));
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
@@ -193,8 +195,8 @@ function PanelEditorQueryBuilder({
className={cx(styles.tabsContainer, {
[styles.stickyNav]: stickyHeader,
})}
activeKey={getQueryMode(currentQuery)}
onChange={onChangeQueryMode}
activeKey={currentQuery.queryType}
onChange={onChangeQueryType}
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,7 +8,6 @@ import PanelEditorQueryBuilder from './PanelEditorQueryBuilder';
function QueryBuilderEditorPane({
panelDefinition,
isLoadingQueries,
onChangeQueryMode,
onStageRunQuery,
onCancelQuery,
stickyHeader,
@@ -17,7 +16,6 @@ function QueryBuilderEditorPane({
<PanelEditorQueryBuilder
panelDefinition={panelDefinition}
isLoadingQueries={isLoadingQueries}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={onStageRunQuery}
onCancelQuery={onCancelQuery}
stickyHeader={stickyHeader}

View File

@@ -1,17 +1,11 @@
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 {
toPanelType,
type PanelKind,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
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();
@@ -57,25 +51,19 @@ const BUILDER_QUERY = {
builder: { queryData: [{ dataSource: 'traces' }] },
};
const AI_QUERY = {
queryType: EQueryType.QUERY_BUILDER,
builder: {
queryType: EQueryType.AI_QUERY_BUILDER,
builder: { queryData: [] },
builder_ai_query: {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
},
};
/** Stands in for the editor shell: owns the tab switch and its parked Query Builder query. */
/** Stands in for the editor shell. */
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()}
@@ -184,8 +172,12 @@ describe('PanelEditorQueryBuilder AI tab', () => {
'traces',
);
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
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);
});
it('keeps the builder query when returning from ClickHouse', () => {
@@ -203,7 +195,7 @@ describe('PanelEditorQueryBuilder AI tab', () => {
expect(onClickHouse.builder).toBe(logsQuery.builder);
});
it('shows a fresh default query when leaving AI', () => {
it('seeds a default query for the kind when leaving AI for an unused Query Builder', () => {
mockBuilder(AI_QUERY);
renderBuilder('signoz/TimeSeriesPanel');
@@ -215,6 +207,19 @@ 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", () => {
mockBuilder({
...AI_QUERY,
builder: { queryData: [{ dataSource: 'metrics' }] },
});
renderBuilder('signoz/ListPanel');
fireEvent.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", () => {
const logsQuery = {
queryType: EQueryType.QUERY_BUILDER,
@@ -232,17 +237,20 @@ describe('PanelEditorQueryBuilder AI tab', () => {
const editedAIBuilder = {
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
};
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
rerender(host());
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());
fireEvent.click(screen.getByText('AI Query Builder'));
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
expect(backToAI.builder).toBe(editedAIBuilder);
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
});
});

View File

@@ -0,0 +1,72 @@
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,8 +72,6 @@ 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;
}
/**
@@ -95,7 +93,6 @@ 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.
@@ -312,7 +309,6 @@ function QueryEditorBody({
panelDefinition={panelDefinition}
signal={listSignal}
isLoadingQueries={isFetching}
onChangeQueryMode={onChangeQueryMode}
onStageRunQuery={runQuery}
onCancelQuery={cancelQuery}
spec={spec}

View File

@@ -60,9 +60,8 @@ jest.mock('../hooks/usePanelEditorQuerySync', () => ({
}));
const mockUseTypeSwitch = jest.fn();
jest.mock('../hooks/usePanelKindAndQueryModeSwitch', () => ({
usePanelKindAndQueryModeSwitch: (args: unknown): unknown =>
mockUseTypeSwitch(args),
jest.mock('../hooks/usePanelKindChange', () => ({
usePanelKindChange: (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 { useRef, useState } from 'react';
import { useState } from 'react';
import { QueryClient, QueryClientProvider } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { Provider as ReduxProvider } from 'react-redux';
@@ -22,8 +22,6 @@ 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', () => {
@@ -81,17 +79,10 @@ 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 { resolveQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { resolveQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
import { toPerses } from '../../../queryV5/persesQueryAdapters';
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
import { usePanelKindAndQueryModeSwitch } from '../usePanelKindAndQueryModeSwitch';
import { usePanelKindChange } from '../usePanelKindChange';
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
useQueryBuilder: jest.fn(),
@@ -18,10 +18,8 @@ jest.mock('lib/query/panelQuery', () => ({
handleQueryChange: jest.fn(),
}));
jest.mock('../../../Panels/capabilities', () => ({
resolveQueryMode: jest.fn(),
resolveQueryType: 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')
@@ -39,7 +37,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
const mockResolveQueryMode = resolveQueryMode as unknown as jest.Mock;
const mockResolveQueryType = resolveQueryType 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;
@@ -102,7 +100,7 @@ function builderState(currentQuery: Query): {
};
}
describe('usePanelKindAndQueryModeSwitch', () => {
describe('usePanelKindChange', () => {
beforeEach(() => {
jest.clearAllMocks();
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
@@ -111,7 +109,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
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.
mockResolveQueryMode.mockReturnValue('builder');
mockResolveQueryType.mockReturnValue('builder');
});
it('does nothing when switching to the current kind', () => {
@@ -120,7 +118,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
@@ -139,7 +137,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
mockUseQueryBuilder.mockReturnValue(state);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: tableSpec,
panelType: PANEL_TYPES.TABLE,
setSpec,
@@ -171,7 +169,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -202,7 +200,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -238,7 +236,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -279,7 +277,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.HEATMAP,
setSpec,
@@ -320,7 +318,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
} as unknown as Query);
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.HEATMAP,
setSpec,
@@ -338,7 +336,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
const { result } = renderHook(() =>
usePanelKindAndQueryModeSwitch({
usePanelKindChange({
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
panelType: PANEL_TYPES.TIME_SERIES,
setSpec,
@@ -347,7 +345,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
// The hook asks the guard to resolve the active query type against the new kind…
expect(mockResolveQueryMode).toHaveBeenCalledWith(
expect(mockResolveQueryType).toHaveBeenCalledWith(
'signoz/ListPanel',
'promql',
);
@@ -356,211 +354,128 @@ describe('usePanelKindAndQueryModeSwitch', () => {
expect((queryArg as Query).queryType).toBe('builder');
});
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');
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];
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);
beforeEach(() => {
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 }) =>
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
usePanelKindChange({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);
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');
// Leave Table on AI; on List the user moves to Query Builder, then returns.
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
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' },
]),
);
state = builderState(withBothSlots('list', 'builder'));
mockResolveQueryType.mockReturnValue('builder');
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
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);
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);
});
it('swaps in the Query Builder query when revisiting a kind left on AI', () => {
const { result, rerender, setSpec, setState } = leaveTableOnAI();
// On List the user is back on Query Builder, then returns to Table.
const state = setState(
fakeQuery('list-qb', 'builder', [{ dataSource: 'logs' }]),
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' }]),
);
mockResolveQueryMode.mockReturnValue('builder');
mockUseQueryBuilder.mockImplementation(() => state);
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindChange({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);
// 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');
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
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);
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);
});
});
@@ -573,7 +488,7 @@ describe('usePanelKindAndQueryModeSwitch', () => {
const { result, rerender } = renderHook(
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
usePanelKindChange({ ...props, setSpec }),
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
);

View File

@@ -9,6 +9,7 @@ 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';
@@ -111,9 +112,10 @@ 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(
() => (currentQuery.builder?.queryData ?? []).map((q) => q.dataSource),
[currentQuery.builder],
() => (activeQueryData ?? []).map((q) => q.dataSource),
[activeQueryData],
);
const dataSourceSignature = dataSources.join(',');
const prevDataSourcesRef = useRef(dataSources);

View File

@@ -11,27 +11,16 @@ 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 BuilderVariant,
type BuildersByVariant,
type OrderByPayload,
type Query,
QueryMode,
import type {
OrderByPayload,
Query,
} from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import {
getQueryPanelDefinition,
isStaticPanelKind,
resolveQueryMode,
resolveQueryType,
} 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';
@@ -43,7 +32,6 @@ 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[] = [
@@ -69,21 +57,17 @@ interface KindState {
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
queries: DashboardtypesQueryDTO[];
query: Query;
/** Both builder variants' queries as the kind was left. */
builders: BuildersByVariant;
}
interface UsePanelKindAndQueryModeSwitchArgs {
interface UsePanelKindChangeArgs {
spec: DashboardtypesPanelSpecDTO;
panelType: PANEL_TYPES;
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
}
interface UsePanelKindAndQueryModeSwitchApi {
interface UsePanelKindChangeApi {
/** 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;
}
/**
@@ -92,22 +76,15 @@ interface UsePanelKindAndQueryModeSwitchApi {
* 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 usePanelKindAndQueryModeSwitch({
export function usePanelKindChange({
spec,
panelType,
setSpec,
}: UsePanelKindAndQueryModeSwitchArgs): UsePanelKindAndQueryModeSwitchApi {
const {
currentQuery,
redirectWithQueryBuilderData,
updateAllQueriesOperators,
} = useQueryBuilder();
}: UsePanelKindChangeArgs): UsePanelKindChangeApi {
const { currentQuery, redirectWithQueryBuilderData } = 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);
@@ -117,14 +94,6 @@ export function usePanelKindAndQueryModeSwitch({
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;
@@ -142,20 +111,9 @@ export function usePanelKindAndQueryModeSwitch({
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
@@ -172,10 +130,12 @@ export function usePanelKindAndQueryModeSwitch({
} as DashboardtypesPanelPluginDTO,
queries,
});
const rebuildForNewKind = (source: Query): Query => {
const queryType = resolveQueryType(newKind, query.queryType);
const rebuildForNewKind = (builder: Query['builder']): Query['builder'] => {
const transformed = handleQueryChange(
newPanelType as keyof PartialPanelTypes,
{ ...source, queryType: targetQueryType },
{ ...query, queryType: EQueryType.QUERY_BUILDER, builder },
panelTypeRef.current,
);
// Match a fresh list panel's default order so the builder's Order By isn't empty.
@@ -183,62 +143,41 @@ export function usePanelKindAndQueryModeSwitch({
newKind === 'signoz/ListPanel'
? withDefaultListOrder(transformed)
: transformed;
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 };
return (
newKind === 'signoz/HeatmapPanel'
? withSingleHeatmapQuery(ordered)
: withPercentileHistogramAggregation(ordered)
).builder;
};
// Revisit → restore the stash in the active mode; a static kind has no builder to re-seed.
// 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.
const cached = cacheRef.current.get(newKind);
if (cached) {
if (isStaticPanelKind(newKind)) {
setSpec(buildSpec(cached.pluginSpec, cached.queries));
return;
}
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.
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.
setSpec(
buildSpec(
cached.pluginSpec,
restored.query.builder === cached.query.builder
queryType === cached.query.queryType
? cached.queries
: toPerses(restored.query, newPanelType),
: toPerses(restored, newPanelType),
),
);
redirectWithQueryBuilderData({
...restored.query,
queryType: targetQueryType,
});
redirectWithQueryBuilderData(restored);
return;
}
@@ -252,25 +191,15 @@ export function usePanelKindAndQueryModeSwitch({
return;
}
// 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;
// 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),
}),
};
const signal = getBuilderQueries(currentSpec.queries)[0]
?.signal as TelemetrytypesSignalDTO;
@@ -282,8 +211,8 @@ export function usePanelKindAndQueryModeSwitch({
);
redirectWithQueryBuilderData(nextQuery);
},
[setSpec, redirectWithQueryBuilderData, updateAllQueriesOperators],
[setSpec, redirectWithQueryBuilderData],
);
return { onChangePanelKind, onChangeQueryMode };
return { onChangePanelKind };
}

View File

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

View File

@@ -3,7 +3,6 @@ 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';
@@ -12,13 +11,12 @@ import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
import {
getQueryPanelDefinition,
requireQueryPanelDefinition,
getSupportedQueryModes,
getSupportedQueryTypes,
getSupportedSignals,
isPanelCombinationValid,
isQueryTypeSupportedByPanelKind,
isSignalSupported,
resolveQueryMode,
resolveQueryType,
} from '../capabilities';
import type { PanelKind } from '../types/panelKind';
@@ -27,59 +25,44 @@ 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,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/BarChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/AreaChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/NumberPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/HistogramPanel': [
QUERY_BUILDER,
CLICKHOUSE,
PROM,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
'signoz/PieChartPanel': [
QUERY_BUILDER,
CLICKHOUSE,
QueryMode.AI_QUERY_BUILDER,
EQueryType.AI_QUERY_BUILDER,
],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, QueryMode.AI_QUERY_BUILDER],
'signoz/ListPanel': [QUERY_BUILDER, QueryMode.AI_QUERY_BUILDER],
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, EQueryType.AI_QUERY_BUILDER],
'signoz/ListPanel': [QUERY_BUILDER, EQueryType.AI_QUERY_BUILDER],
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
'signoz/TextPanel': [],
};
@@ -281,10 +264,10 @@ describe('panel capabilities guard', () => {
it('reads a specific mode when one is given', () => {
expect(
getSupportedSignals('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
getSupportedSignals('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
).toStrictEqual([traces]);
expect(
getSupportedSignals('signoz/TablePanel', QueryMode.CLICKHOUSE),
getSupportedSignals('signoz/TablePanel', EQueryType.CLICKHOUSE),
).toStrictEqual([]);
});
});
@@ -335,36 +318,21 @@ describe('panel capabilities guard', () => {
});
});
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', () => {
describe('resolveQueryType', () => {
it('keeps a supported mode', () => {
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
expect(
resolveQueryMode('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
).toBe(QueryMode.AI_QUERY_BUILDER);
resolveQueryType('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
).toBe(EQueryType.AI_QUERY_BUILDER);
expect(
resolveQueryMode('signoz/ListPanel', QueryMode.AI_QUERY_BUILDER),
).toBe(QueryMode.AI_QUERY_BUILDER);
resolveQueryType('signoz/ListPanel', EQueryType.AI_QUERY_BUILDER),
).toBe(EQueryType.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(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryMode('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
});
});
});

View File

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

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartArea } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,9 +1,10 @@
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 { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -11,7 +12,6 @@ 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,
supportedQueryModesKindMap: {
supportedQueryTypesKindMap: {
// Only metrics carry a bucket axis; the request rejects the other signals.
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.metrics],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
},
// The request rejects both: a point is a count per bucket, not a single value.
queryBuilderFields: {

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { BarChart } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

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

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { List } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import ListEditorPane from './ListEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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.
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.logs, TelemetrytypesSignalDTO.traces],
},
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { Hash } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartPie } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,7 @@
import { ChartScatter } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } 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 { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
},
queryBuilderFields: {
// Pinned so it opens empty: the group-by template a click would seed names nothing here.

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { Table } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -1,7 +1,8 @@
import { EQueryType } from 'types/common/dashboard';
import { ChartLine } from '@signozhq/icons';
import type { PanelDefinition } from '../../types/panelDefinition';
import { QueryModeKind } from '../../types/panelCapabilities';
import { QueryTypeKind } from '../../types/panelCapabilities';
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
import Renderer from './Renderer';
import { sections } from './sections';
@@ -9,7 +10,6 @@ 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,
supportedQueryModesKindMap: {
[QueryMode.QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
supportedQueryTypesKindMap: {
[EQueryType.QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [
TelemetrytypesSignalDTO.metrics,
TelemetrytypesSignalDTO.logs,
TelemetrytypesSignalDTO.traces,
],
},
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
[QueryMode.AI_QUERY_BUILDER]: {
kind: QueryModeKind.SIGNAL,
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
[EQueryType.AI_QUERY_BUILDER]: {
kind: QueryTypeKind.SIGNAL,
signals: [TelemetrytypesSignalDTO.traces],
},
},

View File

@@ -25,7 +25,7 @@ export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
Renderer,
EditorPane: QueryBuilderEditorPane,
sections: [],
supportedQueryModesKindMap: {},
supportedQueryTypesKindMap: {},
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 QueryModeKind {
export enum QueryTypeKind {
SIGNAL = 'signal',
SIGNAL_LESS = 'signal-less',
}
export type QueryModeCapability =
| { kind: QueryModeKind.SIGNAL; signals: TelemetrytypesSignalDTO[] }
| { kind: QueryModeKind.SIGNAL_LESS };
export type QueryTypeCapability =
| { kind: QueryTypeKind.SIGNAL; signals: TelemetrytypesSignalDTO[] }
| { kind: QueryTypeKind.SIGNAL_LESS };
export type SupportedQueryModesKindMap = Partial<
Record<QueryMode, QueryModeCapability>
export type SupportedQueryTypesKindMap = Partial<
Record<EQueryType, QueryTypeCapability>
>;
/** Signals `mode` accepts; empty for a signal-less or undeclared mode. */
export function getQueryModeSignals(
modes: SupportedQueryModesKindMap | undefined,
mode: QueryMode,
/** Signals `queryType` accepts; empty for a signal-less or undeclared query type. */
export function getQueryTypeSignals(
queryTypes: SupportedQueryTypesKindMap | undefined,
queryType: EQueryType,
): TelemetrytypesSignalDTO[] {
const capability = modes?.[mode];
return capability?.kind === QueryModeKind.SIGNAL ? capability.signals : [];
const capability = queryTypes?.[queryType];
return capability?.kind === QueryTypeKind.SIGNAL ? capability.signals : [];
}

View File

@@ -13,7 +13,7 @@ import type { SyncedQueryBuilderField } from 'components/QueryBuilderV2/queryBui
import type {
PanelQueryCapabilities,
QueryBuilderFieldsConfig,
SupportedQueryModesKindMap,
SupportedQueryTypesKindMap,
} from './panelCapabilities';
import type {
BaseRendererProps,
@@ -99,8 +99,6 @@ 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. */
@@ -122,7 +120,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. */
supportedQueryModesKindMap: SupportedQueryModesKindMap;
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
queryBuilderFields: QueryBuilderFieldsConfig;
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
queryCapabilities: PanelQueryCapabilities;

View File

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

View File

@@ -31,8 +31,6 @@ 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;
}
/**
@@ -47,7 +45,6 @@ function QueryViewModalBody({
onClose,
draftApi,
onChangePanelKind,
onChangeQueryMode,
}: QueryViewModalBodyProps): JSX.Element | null {
const {
timeOverride,
@@ -165,7 +162,6 @@ 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 { usePanelKindAndQueryModeSwitch } from 'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindAndQueryModeSwitch';
import { usePanelKindChange } from 'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindChange';
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,12 +73,11 @@ function ViewPanelModalContent({
const draftKind = draftApi.draft.spec.plugin.kind;
const panelDefinition = getPanelDefinition(draftKind);
const { onChangePanelKind, onChangeQueryMode } =
usePanelKindAndQueryModeSwitch({
spec: draftApi.draft.spec,
panelType: toPanelType(draftKind),
setSpec: draftApi.setSpec,
});
const { onChangePanelKind } = usePanelKindChange({
spec: draftApi.draft.spec,
panelType: toPanelType(draftKind),
setSpec: draftApi.setSpec,
});
if (panelDefinition.mode === 'static') {
return (
@@ -98,7 +97,6 @@ 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/usePanelKindAndQueryModeSwitch',
'pages/DashboardPage/DashboardContainer/PanelEditor/hooks/usePanelKindChange',
() => ({
usePanelKindAndQueryModeSwitch: (): unknown => ({
usePanelKindChange: (): unknown => ({
onChangePanelKind: jest.fn(),
}),
}),

View File

@@ -157,6 +157,12 @@ 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