Compare commits

...

9 Commits

Author SHA1 Message Date
Abhi Kumar
9909d8e222 refactor(dashboard): remove the query-mode leftovers
The tab switch now lives in PanelEditorQueryBuilder, so the editor panes stop passing it down.
2026-10-21 23:13:45 +05:30
Abhi Kumar
31bde4d502 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-10-21 23:13:23 +05:30
Abhi Kumar
f9330ddf82 feat(query-builder): render the active builder slot 2026-10-21 23:11:20 +05:30
Abhi Kumar
0ba63f0757 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-10-21 23:10:58 +05:30
Abhi Kumar
731a169962 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-10-21 23:10:36 +05:30
Abhi Kumar
7e66be7152 feat(query-builder): map AI composites into the AI slot 2026-10-21 23:10:08 +05:30
Abhi Kumar
c61da39761 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-10-21 23:09:50 +05:30
Abhi Kumar
feda4133a1 refactor(dashboard): fold QueryMode into EQueryType
AI joins EQueryType so tabs and query data share one enum; capability names follow it.
2026-10-21 23:09:18 +05:30
Abhi Kumar
7b0ce0b448 fix(query-builder): stop forcing metrics data source on bar panels
Bar panels support logs and traces now; the AI override only existed to undo it.
2026-10-21 14:36:39 +05:30
83 changed files with 1634 additions and 1161 deletions

View File

@@ -902,6 +902,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('convertBuilderQueriesToV5 having normalization', () => {

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';
@@ -557,9 +558,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');
@@ -621,13 +628,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,14 +1,10 @@
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { PANEL_TYPES } from 'constants/queryBuilder';
import { Formula } from 'container/QueryBuilder/components/Formula';
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { getBuilderVariant } from 'lib/query/queryMode';
import {
BuilderVariant,
IBuilderTraceOperator,
} from 'types/api/queryBuilder/queryBuilderData';
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderField } from './queryBuilderFields.types';
@@ -54,6 +50,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
initialDataSource,
handleRunQuery,
} = useQueryBuilder();
const activeBuilder = getActiveBuilder(currentQuery);
const containerRef = useRef(null);
@@ -64,20 +61,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
[config],
);
const isAIQuery =
getBuilderVariant(currentQuery.builder) === BuilderVariant.AI;
useEffect(() => {
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
// AI queries are traces-only; BAR must not force metrics on them.
if (isAIQuery) {
handleSetConfig(newPanelType, DataSource.TRACES);
return;
}
if (newPanelType === PANEL_TYPES.BAR) {
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
return;
}
handleSetConfig(newPanelType, currentDataSource);
}
}, [
@@ -86,7 +71,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
initialDataSource,
currentDataSource,
newPanelType,
isAIQuery,
]);
useEffect(() => {
@@ -139,28 +123,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(
@@ -234,9 +218,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}
@@ -247,7 +231,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}
@@ -273,28 +257,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
@@ -328,7 +310,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

@@ -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,67 @@
import {
initialQueriesMap,
initialQueryAIWithType,
PANEL_TYPES,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
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(initialQueryAIWithType.builder, '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

@@ -0,0 +1,109 @@
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { render } from 'tests/test-utils';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
import { DataSource } from 'types/common/queryBuilder';
import { QueryBuilderV2 } from '../QueryBuilderV2';
jest.mock('hooks/queryBuilder/useQueryBuilder');
jest.mock('../QueryV2/QueryV2', () => ({ QueryV2: (): null => null }));
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
const aiQuery: Query = {
...initialQueriesMap.traces,
builder: {
...initialQueriesMap.traces.builder,
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
...query,
builderQueryType: EQueryType.AI_QUERY_BUILDER,
})),
},
};
function mockContext({
panelType = null,
initialDataSource = null,
currentQuery = initialQueriesMap.metrics,
}: {
panelType?: PANEL_TYPES | null;
initialDataSource?: DataSource | null;
currentQuery?: Query;
}): jest.Mock {
const handleSetConfig = jest.fn();
mockedUseQueryBuilder.mockReturnValue({
currentQuery,
panelType,
initialDataSource,
handleSetConfig,
addNewBuilderQuery: jest.fn(),
addNewFormula: jest.fn(),
addTraceOperator: jest.fn(),
handleRunQuery: jest.fn(),
} as unknown as ReturnType<typeof useQueryBuilder>);
return handleSetConfig;
}
describe('QueryBuilderV2 provider config sync', () => {
it('leaves the data source unset for a bar panel without a static config', () => {
const handleSetConfig = mockContext({});
render(<QueryBuilderV2 panelType={PANEL_TYPES.BAR} version="v3" />);
expect(handleSetConfig).toHaveBeenCalledWith(PANEL_TYPES.BAR, null);
});
it('syncs the static data source', () => {
const handleSetConfig = mockContext({});
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.BAR}
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).toHaveBeenCalledWith(
PANEL_TYPES.BAR,
DataSource.LOGS,
);
});
it('keeps an AI query on traces for a bar panel', () => {
const handleSetConfig = mockContext({ currentQuery: aiQuery });
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.BAR}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).toHaveBeenCalledWith(
PANEL_TYPES.BAR,
DataSource.TRACES,
);
});
it('does not reset the config when the provider already matches', () => {
const handleSetConfig = mockContext({
panelType: PANEL_TYPES.TIME_SERIES,
initialDataSource: DataSource.TRACES,
});
render(
<QueryBuilderV2
version="v3"
panelType={PANEL_TYPES.TIME_SERIES}
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
/>,
);
expect(handleSetConfig).not.toHaveBeenCalled();
});
});

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,
@@ -1632,3 +1635,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';
@@ -1013,8 +1017,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;
@@ -1055,8 +1060,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

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

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

View File

@@ -9,6 +9,7 @@ import {
useState,
} from 'react';
import { Select, Spin } from 'antd';
import { getActiveBuilder } from 'lib/query/builderSlot';
import { Pill } from '@signozhq/ui/pill';
import cx from 'classnames';
import {
@@ -857,14 +858,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

@@ -4,6 +4,7 @@ import cx from 'classnames';
import { ResizeTable } from 'components/ResizeTable';
import Download from 'container/Download/Download';
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
import { isBuilderQueryType } from 'lib/query/builderSlot';
import {
createTableColumnsFromQuery,
RowData,
@@ -34,7 +35,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 } = useParams<IServiceName>();
const servicename = decodeURIComponent(encodedServiceName);

View File

@@ -1,5 +1,6 @@
import WidgetHeader from 'container/WidgetCard/Header';
import { fireEvent, render } from 'tests/test-utils';
import { EQueryType } from 'types/common/dashboard';
import { QueryTable } from '../QueryTable';
import { QueryTableProps, WidgetHeaderProps } from './mocks';
@@ -33,6 +34,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

@@ -28,12 +28,15 @@ export const useGetCompositeQueryParam = (): Query | null => {
);
// Add default values for optional fields if empty
if (parsedCompositeQuery?.builder) {
parsedCompositeQuery.builder.queryFormulas =
parsedCompositeQuery.builder.queryFormulas ?? [];
parsedCompositeQuery.builder.queryTraceOperator =
parsedCompositeQuery.builder.queryTraceOperator ?? [];
}
[
parsedCompositeQuery?.builder,
parsedCompositeQuery?.builder_ai_query,
].forEach((builder) => {
if (builder) {
builder.queryFormulas = builder.queryFormulas ?? [];
builder.queryTraceOperator = builder.queryTraceOperator ?? [];
}
});
// Convert old format to new format for each query in builder.queryData
if (parsedCompositeQuery?.builder?.queryData) {

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,
@@ -87,8 +88,16 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
});
}
});
const builder = transformQueryBuilderDataModel(
builderQueries,
builderQueryTypes,
);
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
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 = initialQueryAIWithType.builder;
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

@@ -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,
@@ -173,7 +175,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]);
@@ -190,8 +192,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,11 @@ 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].builderQueryType).toBe(
'builder_ai_query',
);
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
});
it('keeps the builder query when returning from ClickHouse', () => {
@@ -203,7 +194,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 +206,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 +236,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

@@ -71,8 +71,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;
}
/**
@@ -94,7 +92,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.
@@ -310,7 +307,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';
@@ -20,8 +20,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', () => {
@@ -84,17 +82,10 @@ function EditorRoute(): 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

@@ -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,58 +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],
// 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/TextPanel': [],
};
@@ -270,10 +254,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([]);
});
});
@@ -324,36 +308,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,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

@@ -12,7 +12,7 @@ import type { SyncedQueryBuilderField } from 'components/QueryBuilderV2/queryBui
import type {
PanelQueryCapabilities,
QueryBuilderFieldsConfig,
SupportedQueryModesKindMap,
SupportedQueryTypesKindMap,
} from './panelCapabilities';
import type {
BaseRendererProps,
@@ -95,8 +95,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. */
@@ -118,7 +116,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

@@ -5,14 +5,14 @@ import {
type PANEL_TYPES,
} from 'constants/queryBuilder';
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,7 +26,7 @@ export function seedBuilderForMode({
panelType,
updateAllQueriesOperators,
}: SeedBuilderForModeArgs): Query['builder'] {
if (mode === QueryMode.AI_QUERY_BUILDER) {
if (mode === EQueryType.AI_QUERY_BUILDER) {
return updateAllQueriesOperators(
initialQueryAIWithType,
panelType,

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'));

View File

@@ -12,6 +12,7 @@ import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types
import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getPanelQueryType';
import { useDashboardEventMeta } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardEventMeta';
import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/queryV5/buildQueryRangeRequest';
import { toWireQuery } from 'lib/query/builderSlot';
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
@@ -81,9 +82,8 @@ export function useCreateAlertFromPanel(): (
{ data: request },
{
onSuccess: (response) => {
const query = envelopesToQuery(
response.data.compositeQuery?.queries ?? [],
panelType,
const query = toWireQuery(
envelopesToQuery(response.data.compositeQuery?.queries ?? [], panelType),
);
const unit = readPanelUnit(panel.spec.plugin);
const url = buildAlertUrl(

View File

@@ -22,7 +22,7 @@ import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import { dynamicSignalFromQuerySignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/drilldown/signal';
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
import { EQueryType } from 'types/common/dashboard';
import { isBuilderQueryType, toWireQuery } from 'lib/query/builderSlot';
import DrilldownAggregateMenu from '../DrilldownMenu/DrilldownAggregateMenu';
import DrilldownBreakoutMenu from '../DrilldownMenu/DrilldownBreakoutMenu';
@@ -87,13 +87,13 @@ export function useDrilldown(
const enableDrillDown = useMemo(
() =>
getPanelDefinition(kind).actions.drilldown &&
getPanelQueryType(panel) === EQueryType.QUERY_BUILDER &&
isBuilderQueryType(getPanelQueryType(panel)) &&
getBuilderQueries(queries).length > 0,
[kind, panel, queries],
);
const v1Query = useMemo(
() => fromPerses(queries, panelType),
() => toWireQuery(fromPerses(queries, panelType)),
[queries, panelType],
);

View File

@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
import { useReplaceVariables } from 'api/generated/services/querier';
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/queryV5/buildQueryRangeRequest';
import { toWireQuery } from 'lib/query/builderSlot';
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
@@ -90,9 +91,11 @@ export function useResolvedDrilldownQuery({
}
// View-in-X navigates to a V1 explorer, so the resolved query crosses back into the
// V1 `Query` shape — the one place this hook still needs a legacy panel type.
return envelopesToQuery(
data.data.compositeQuery?.queries ?? [],
toPanelType(panelKind),
return toWireQuery(
envelopesToQuery(
data.data.compositeQuery?.queries ?? [],
toPanelType(panelKind),
),
);
}, [hasVariables, data, v1Query, panelKind]);

View File

@@ -13,6 +13,7 @@ import {
type PanelFormattingSlice,
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
import { toWireQuery } from 'lib/query/builderSlot';
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
@@ -74,7 +75,7 @@ export function buildAlertUrl(
*/
export function buildCreateAlertUrl(panel: DashboardtypesPanelDTO): string {
const panelType = toPanelType(panel.spec.plugin.kind);
const query = fromPerses(panel.spec.queries, panelType);
const query = toWireQuery(fromPerses(panel.spec.queries, panelType));
const unit = readPanelUnit(panel.spec.plugin);
return buildAlertUrl(
query,

View File

@@ -87,6 +87,19 @@ describe('persesQueryAdapters', () => {
EQueryType.CLICKHOUSE,
);
});
it('opens an AI panel on the AI query type with its queries in the AI slot', () => {
const restored = fromPerses(
toPerses(initialQueryAIWithType, PANEL_TYPES.LIST),
PANEL_TYPES.LIST,
);
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(restored.builder.queryData).toStrictEqual([]);
expect(restored.builder_ai_query?.queryData[0].dataSource).toBe(
DataSource.TRACES,
);
});
});
describe('envelopesToQuery', () => {
@@ -232,13 +245,41 @@ describe('persesQueryAdapters', () => {
expect(result[0].spec.plugin.kind).toBe('signoz/AIBuilderQuery');
});
it('emits signoz/AIBuilderQuery for an AI-typed List query, from its own slot', () => {
const query: Query = {
...initialQueriesMap[DataSource.METRICS],
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: initialQueryAIWithType.builder,
};
const result = toPerses(query, PANEL_TYPES.LIST);
expect(result).toHaveLength(1);
expect(result[0].spec.plugin.kind).toBe('signoz/AIBuilderQuery');
});
it('emits only the active slot, never a parked AI query', () => {
const query: Query = {
...initialQueriesMap[DataSource.METRICS],
builder_ai_query: initialQueryAIWithType.builder,
};
const result = toPerses(query, PANEL_TYPES.TIME_SERIES);
const envelopes = (
result[0].spec.plugin.spec as { queries?: { type?: string }[] }
).queries;
expect(envelopes?.map((e) => e.type)).toStrictEqual(['builder_query']);
});
it('preserves an AI List query through toPerses → fromPerses', () => {
const restored = fromPerses(
toPerses(initialQueryAIWithType, PANEL_TYPES.LIST),
PANEL_TYPES.LIST,
);
expect(restored.builder.queryData[0].builderQueryType).toBe(
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
});
@@ -251,7 +292,8 @@ describe('persesQueryAdapters', () => {
savedAgain[0].spec.plugin.spec as { queries?: { type?: string }[] }
).queries;
expect(restored.builder.queryData[0].builderQueryType).toBe(
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
'builder_ai_query',
);
expect(envelopes?.[0]?.type).toBe('builder_ai_query');

View File

@@ -85,6 +85,9 @@ export function deriveQueryType(
) {
return EQueryType.CLICKHOUSE;
}
if (envelopes.some(isAIBuilderEnvelope)) {
return EQueryType.AI_QUERY_BUILDER;
}
return EQueryType.QUERY_BUILDER;
}

View File

@@ -18,6 +18,7 @@ import {
initialClickHouseData,
initialFormulaBuilderFormValues,
initialQueriesMap,
initialQueryAIWithType,
initialQueryBuilderFormTraceOperatorValues,
initialQueryBuilderFormValuesMap,
initialQueryPromQLData,
@@ -35,11 +36,15 @@ import {
} from 'lib/query/panelTypeDataSourceFormValuesMap';
import { OptionsQuery } from 'container/OptionsMenu/types';
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
import { useSafeNavigate } from 'hooks/useSafeNavigate';
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
import {
getBuilderAt,
getBuilderSlotKey,
withBuilderAt,
} from 'lib/query/builderSlot';
import { saveRecentQuery } from 'lib/recentQueries/saveRecentQuery';
import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields';
import { cloneDeep, get, isEqual, set } from 'lodash-es';
@@ -136,6 +141,7 @@ export function QueryBuilderProvider({
const [stagedQuery, setStagedQuery] = useState<Query | null>(null);
const [queryType, setQueryType] = useState<EQueryType>(queryTypeParam);
const builderSlot = getBuilderSlotKey(queryType);
const getElementWithActualOperator = useCallback(
(
@@ -163,62 +169,73 @@ export function QueryBuilderProvider({
const prepareQueryBuilderData = useCallback(
(query: Query): Query => {
const builder: QueryBuilderData = {
queryData:
query.builder.queryData?.map((item) => ({
...initialQueryBuilderFormValuesMap[
initialDataSource || DataSource.METRICS
],
...item,
})) ?? [],
queryFormulas:
query.builder.queryFormulas?.map((item) => ({
...initialFormulaBuilderFormValues,
...item,
})) ?? [],
queryTraceOperator:
query.builder.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})) ?? [],
};
const urlPanelType = readPanelTypeFromUrl(PANEL_TYPES.LIST);
const isRawPanel =
urlPanelType === PANEL_TYPES.LIST || urlPanelType === PANEL_TYPES.TRACE;
const setupedQueryData = builder.queryData.map((item) => {
const currentElement: IBuilderQuery = {
...item,
groupBy: item.groupBy.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})),
aggregateAttribute: {
...item.aggregateAttribute,
id: createIdFromObjectFields(
item.aggregateAttribute as BaseAutocompleteData,
baseAutoCompleteIdKeysOrder,
),
} as BaseAutocompleteData,
const prepareBuilder = (source: QueryBuilderData): QueryBuilderData => {
const builder: QueryBuilderData = {
queryData:
source.queryData?.map((item) => ({
...initialQueryBuilderFormValuesMap[
initialDataSource || DataSource.METRICS
],
...item,
})) ?? [],
queryFormulas:
source.queryFormulas?.map((item) => ({
...initialFormulaBuilderFormValues,
...item,
})) ?? [],
queryTraceOperator:
source.queryTraceOperator?.map((item) => ({
...initialQueryBuilderFormTraceOperatorValues,
...item,
})) ?? [],
};
// Explorer pages: sanitize stale orderBy before first query
const isExplorer =
(location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER ||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER) &&
!isRawPanel;
if (isExplorer) {
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
return calledFromHandleRunQuery
? currentElement
: { ...currentElement, orderBy: sanitizedOrderBy };
}
const setupedQueryData = builder.queryData.map((item) => {
const currentElement: IBuilderQuery = {
...item,
groupBy: item.groupBy.map(({ id: _, ...item }) => ({
...item,
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
})),
aggregateAttribute: {
...item.aggregateAttribute,
id: createIdFromObjectFields(
item.aggregateAttribute as BaseAutocompleteData,
baseAutoCompleteIdKeysOrder,
),
} as BaseAutocompleteData,
};
return currentElement;
});
// Explorer pages: sanitize stale orderBy before first query
const isExplorer =
(location.pathname === ROUTES.LOGS_EXPLORER ||
location.pathname === ROUTES.TRACES_EXPLORER ||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER) &&
!isRawPanel;
if (isExplorer) {
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
return calledFromHandleRunQuery
? currentElement
: { ...currentElement, orderBy: sanitizedOrderBy };
}
return currentElement;
});
return { ...builder, queryData: setupedQueryData };
};
// Filled the first time AI is the active type, then kept so a tab switch can return to it.
const aiBuilder =
query.builder_ai_query ??
(query.queryType === EQueryType.AI_QUERY_BUILDER
? initialQueryAIWithType.builder
: undefined);
const promql: IPromQLQuery[] =
query.promql?.map((item) => ({
@@ -235,10 +252,8 @@ export function QueryBuilderProvider({
const newQueryState: QueryState = {
clickhouse_sql: clickHouse,
promql,
builder: {
...builder,
queryData: setupedQueryData,
},
builder: prepareBuilder(query.builder),
...(aiBuilder && { builder_ai_query: prepareBuilder(aiBuilder) }),
id: query.id,
unit: query.unit,
};
@@ -283,20 +298,19 @@ export function QueryBuilderProvider({
dataSource: DataSource,
signalSource?: 'meter' | '',
): Query => {
const queryData = query.builder.queryData?.map((item) =>
const slot = getBuilderSlotKey(query.queryType);
const builder = getBuilderAt(query, slot);
const queryData = builder.queryData?.map((item) =>
getElementWithActualOperator(item, dataSource, panelType),
);
return {
...query,
builder: {
...query.builder,
queryData: queryData.map((item) => ({
...item,
source: signalSource,
})),
},
};
return withBuilderAt(query, slot, {
...builder,
queryData: queryData.map((item) => ({
...item,
source: signalSource,
})),
});
},
[getElementWithActualOperator],
@@ -393,9 +407,11 @@ export function QueryBuilderProvider({
index: number,
) => QueryBuilderData[T][number],
): Query => {
const result = query.builder[type].map(updateCallback);
const slot = getBuilderSlotKey(query.queryType);
const builder = getBuilderAt(query, slot);
const result = builder[type].map(updateCallback);
return { ...query, builder: { ...query.builder, [type]: result } };
return withBuilderAt(query, slot, { ...builder, [type]: result });
},
[],
);
@@ -407,17 +423,14 @@ export function QueryBuilderProvider({
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
)[] = getBuilderAt(prevState, builderSlot)[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
return {
...prevState,
builder: {
...prevState.builder,
[type]: filteredArray,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
[type]: filteredArray,
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
@@ -425,34 +438,35 @@ export function QueryBuilderProvider({
| IBuilderQuery
| IBuilderFormula
| IBuilderTraceOperator
)[] = prevState.builder[type];
)[] = getBuilderAt(prevState, builderSlot)[type];
const filteredArray = currentArray.filter((_, i) => index !== i);
return {
...prevState,
builder: {
...prevState.builder,
[type]: filteredArray,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
[type]: filteredArray,
});
});
},
[],
[builderSlot],
);
const removeAllQueryBuilderEntities = useCallback(
(type: keyof QueryBuilderData) => {
setCurrentQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
setSupersetQuery((prevState) => ({
...prevState,
builder: { ...prevState.builder, [type]: [] },
}));
setCurrentQuery((prevState) =>
withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
[type]: [],
}),
);
setSupersetQuery((prevState) =>
withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
[type]: [],
}),
);
},
[setCurrentQuery, setSupersetQuery],
[builderSlot, setCurrentQuery, setSupersetQuery],
);
const removeQueryTypeItemByIndex = useCallback(
@@ -582,177 +596,180 @@ export function QueryBuilderProvider({
const addNewBuilderQuery = useCallback(() => {
setCurrentQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) {
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
return prevState;
}
const newQuery = createNewBuilderQuery(prevState.builder.queryData);
const newQuery = createNewBuilderQuery(
getBuilderAt(prevState, builderSlot).queryData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: [...prevState.builder.queryData, newQuery],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: [...getBuilderAt(prevState, builderSlot).queryData, newQuery],
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) {
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
return prevState;
}
const newQuery = createNewBuilderQuery(prevState.builder.queryData);
const newQuery = createNewBuilderQuery(
getBuilderAt(prevState, builderSlot).queryData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: [...prevState.builder.queryData, newQuery],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: [...getBuilderAt(prevState, builderSlot).queryData, newQuery],
});
});
}, [createNewBuilderQuery]);
}, [builderSlot, createNewBuilderQuery]);
const cloneQuery = useCallback(
(type: string, query: IBuilderQuery): void => {
setCurrentQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) {
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
return prevState;
}
const clonedQuery = cloneNewBuilderQuery(
prevState.builder.queryData,
getBuilderAt(prevState, builderSlot).queryData,
query,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: [...prevState.builder.queryData, clonedQuery],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: [
...getBuilderAt(prevState, builderSlot).queryData,
clonedQuery,
],
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
if (prevState.builder.queryData.length >= MAX_QUERIES) {
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
return prevState;
}
const clonedQuery = cloneNewBuilderQuery(
prevState.builder.queryData,
getBuilderAt(prevState, builderSlot).queryData,
query,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: [...prevState.builder.queryData, clonedQuery],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: [
...getBuilderAt(prevState, builderSlot).queryData,
clonedQuery,
],
});
});
},
[cloneNewBuilderQuery],
[builderSlot, cloneNewBuilderQuery],
);
const addNewFormula = useCallback(() => {
setCurrentQuery((prevState) => {
if (prevState.builder.queryFormulas.length >= MAX_FORMULAS) {
if (
getBuilderAt(prevState, builderSlot).queryFormulas.length >= MAX_FORMULAS
) {
return prevState;
}
const newFormula = createNewBuilderFormula(prevState.builder.queryFormulas);
const newFormula = createNewBuilderFormula(
getBuilderAt(prevState, builderSlot).queryFormulas,
);
return {
...prevState,
builder: {
...prevState.builder,
queryFormulas: [...prevState.builder.queryFormulas, newFormula],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryFormulas: [
...getBuilderAt(prevState, builderSlot).queryFormulas,
newFormula,
],
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
if (prevState.builder.queryFormulas.length >= MAX_FORMULAS) {
if (
getBuilderAt(prevState, builderSlot).queryFormulas.length >= MAX_FORMULAS
) {
return prevState;
}
const newFormula = createNewBuilderFormula(prevState.builder.queryFormulas);
const newFormula = createNewBuilderFormula(
getBuilderAt(prevState, builderSlot).queryFormulas,
);
return {
...prevState,
builder: {
...prevState.builder,
queryFormulas: [...prevState.builder.queryFormulas, newFormula],
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryFormulas: [
...getBuilderAt(prevState, builderSlot).queryFormulas,
newFormula,
],
});
});
}, [createNewBuilderFormula]);
}, [builderSlot, createNewBuilderFormula]);
const addTraceOperator = useCallback((expression = '') => {
const trimmed = (expression || '').trim();
const addTraceOperator = useCallback(
(expression = '') => {
const trimmed = (expression || '').trim();
setCurrentQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
setCurrentQuery((prevState) => {
const existing =
getBuilderAt(prevState, builderSlot).queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const existing = prevState.builder.queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const existing =
getBuilderAt(prevState, builderSlot).queryTraceOperator?.[0] || null;
const updated: IBuilderTraceOperator = existing
? { ...existing, expression: trimmed }
: {
...initialQueryBuilderFormTraceOperatorValues,
queryName: TRACE_OPERATOR_QUERY_NAME,
expression: trimmed,
};
return {
...prevState,
builder: {
...prevState.builder,
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
// enforce single trace operator and replace only expression
queryTraceOperator: [updated],
},
};
});
}, []);
});
});
},
[builderSlot],
);
const removeTraceOperator = useCallback(() => {
setCurrentQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
setCurrentQuery((prevState) =>
withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryTraceOperator: [],
},
}));
}),
);
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => ({
...prevState,
builder: {
...prevState.builder,
setSupersetQuery((prevState) =>
withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryTraceOperator: [],
},
}));
}, []);
}),
);
}, [builderSlot]);
const updateQueryBuilderData: <T>(
arr: T[],
@@ -832,110 +849,92 @@ export function QueryBuilderProvider({
(index: number, newQueryData: IBuilderQuery): void => {
setCurrentQuery((prevState) => {
const updatedQueryBuilderData = updateQueryBuilderData(
prevState.builder.queryData,
getBuilderAt(prevState, builderSlot).queryData,
index,
newQueryData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: updatedQueryBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: updatedQueryBuilderData,
});
});
setSupersetQuery((prevState) => {
const updatedQueryBuilderData = updateSuperSetQueryBuilderData(
prevState.builder.queryData,
getBuilderAt(prevState, builderSlot).queryData,
index,
newQueryData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryData: updatedQueryBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryData: updatedQueryBuilderData,
});
});
},
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
[builderSlot, updateQueryBuilderData, updateSuperSetQueryBuilderData],
);
const handleSetTraceOperatorData = useCallback(
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
setCurrentQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
getBuilderAt(prevState, builderSlot).queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryTraceOperator: updatedTraceOperatorBuilderData,
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
prevState.builder.queryTraceOperator,
getBuilderAt(prevState, builderSlot).queryTraceOperator,
index,
traceOperatorData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryTraceOperator: updatedTraceOperatorBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryTraceOperator: updatedTraceOperatorBuilderData,
});
});
},
[updateQueryBuilderData],
[builderSlot, updateQueryBuilderData],
);
const handleSetFormulaData = useCallback(
(index: number, formulaData: IBuilderFormula): void => {
setCurrentQuery((prevState) => {
const updatedFormulasBuilderData = updateQueryBuilderData(
prevState.builder.queryFormulas,
getBuilderAt(prevState, builderSlot).queryFormulas,
index,
formulaData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryFormulas: updatedFormulasBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryFormulas: updatedFormulasBuilderData,
});
});
// eslint-disable-next-line sonarjs/no-identical-functions
setSupersetQuery((prevState) => {
const updatedFormulasBuilderData = updateQueryBuilderData(
prevState.builder.queryFormulas,
getBuilderAt(prevState, builderSlot).queryFormulas,
index,
formulaData,
);
return {
...prevState,
builder: {
...prevState.builder,
queryFormulas: updatedFormulasBuilderData,
},
};
return withBuilderAt(prevState, builderSlot, {
...getBuilderAt(prevState, builderSlot),
queryFormulas: updatedFormulasBuilderData,
});
});
},
[updateQueryBuilderData],
[builderSlot, updateQueryBuilderData],
);
const isStagedQueryUpdated = useCallback(
@@ -989,6 +988,9 @@ export function QueryBuilderProvider({
const currentGeneratedQuery: Query = {
queryType,
builder,
...(query.builder_ai_query && {
builder_ai_query: query.builder_ai_query,
}),
promql,
clickhouse_sql: clickhouseSql,
id: uuid(),
@@ -1055,42 +1057,34 @@ export function QueryBuilderProvider({
}
saveRecentQuery(currentQuery);
const currentQueryData = {
...currentQuery,
builder: {
...currentQuery.builder,
queryData: currentQuery.builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
})),
},
};
redirectWithQueryBuilderData({
...{
...currentQueryData,
...updateStepInterval({
builder: currentQueryData.builder,
clickhouse_sql: currentQueryData.clickhouse_sql,
promql: currentQueryData.promql,
id: currentQueryData.id,
queryType,
unit: currentQueryData.unit,
}),
},
queryType,
const builder = getBuilderAt(currentQuery, builderSlot);
const currentQueryData = withBuilderAt(currentQuery, builderSlot, {
...builder,
queryData: builder.queryData.map((item) => ({
...item,
filter: {
...item.filter,
expression:
item.filter?.expression.trim() === ''
? ''
: (item.filter?.expression ?? ''),
},
filters: {
items: [],
op: 'AND',
},
stepInterval: item.stepInterval ?? null,
})),
});
}, [currentQuery, location.pathname, queryType, redirectWithQueryBuilderData]);
redirectWithQueryBuilderData({ ...currentQueryData, queryType });
}, [
builderSlot,
currentQuery,
location.pathname,
queryType,
redirectWithQueryBuilderData,
]);
useEffect(() => {
if (location.pathname !== currentPathnameRef.current) {
@@ -1121,7 +1115,10 @@ export function QueryBuilderProvider({
);
if (!isValid) {
redirectWithQueryBuilderData(validData);
redirectWithQueryBuilderData({
...validData,
builder_ai_query: compositeQueryParam.builder_ai_query,
});
} else {
initQueryBuilderData(compositeQueryParam);
}

View File

@@ -0,0 +1,87 @@
import {
initialQueriesMap,
initialQueryAIWithType,
} from 'constants/queryBuilder';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
import { EQueryType } from 'types/common/dashboard';
const renderQueryBuilder = (
initialQuery: Query,
): ReturnType<
typeof renderHook<ReturnType<typeof useQueryBuilder>, unknown>
> => {
const hook = renderHook(() => useQueryBuilder(), {
wrapper: AllTheProviders,
});
act(() => {
hook.result.current.initQueryBuilderData(initialQuery);
});
return hook;
};
const aiQuery: Query = {
...initialQueriesMap.metrics,
queryType: EQueryType.AI_QUERY_BUILDER,
builder_ai_query: initialQueryAIWithType.builder,
};
describe('QueryBuilder AI slot', () => {
it('leaves the AI slot absent for a query that never used it', () => {
const { result } = renderQueryBuilder(initialQueriesMap.metrics);
expect(result.current.currentQuery).not.toHaveProperty('builder_ai_query');
});
it('fills an unused AI slot when AI becomes the query type', () => {
const { result } = renderQueryBuilder({
...initialQueriesMap.metrics,
queryType: EQueryType.AI_QUERY_BUILDER,
});
expect(
result.current.currentQuery.builder_ai_query?.queryData[0],
).toMatchObject({
dataSource: 'traces',
builderQueryType: 'builder_ai_query',
});
});
it('edits the AI slot and leaves builder alone while AI is active', () => {
const { result } = renderQueryBuilder(aiQuery);
const { builder } = result.current.currentQuery;
act(() => {
result.current.addNewBuilderQuery();
});
act(() => {
result.current.handleSetQueryData(0, {
...initialQueryAIWithType.builder.queryData[0],
legend: 'ai legend',
});
});
const { currentQuery } = result.current;
expect(currentQuery.builder).toStrictEqual(builder);
expect(currentQuery.builder_ai_query?.queryData).toHaveLength(2);
expect(currentQuery.builder_ai_query?.queryData[0].legend).toBe('ai legend');
});
it('edits builder and keeps the AI slot while Query Builder is active', () => {
const { result } = renderQueryBuilder({
...aiQuery,
queryType: EQueryType.QUERY_BUILDER,
});
const aiBuilder = result.current.currentQuery.builder_ai_query;
act(() => {
result.current.addNewBuilderQuery();
});
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
expect(result.current.currentQuery.builder_ai_query).toStrictEqual(aiBuilder);
});
});

View File

@@ -234,4 +234,40 @@ describe('mapQueryDataFromApi', (): void => {
expect(result.builder.queryData[0].queryName).toBe('A');
expect(result.builder.queryData[0].dataSource).toBe(DataSource.TRACES);
});
it('puts an AI-typed composite in the AI slot, leaving builder empty', () => {
const compositeQuery: ICompositeMetricQuery = {
queryType: EQueryType.AI_QUERY_BUILDER,
panelType: PANEL_TYPES.TIME_SERIES,
unit: undefined,
queries: [
{
type: 'builder_ai_query',
spec: {
name: 'A',
signal: 'traces',
aggregations: [{ expression: 'count()' }],
},
} as QueryEnvelope,
{
type: 'builder_formula',
spec: { name: 'F1', expression: 'A * 2' },
} as QueryEnvelope,
],
};
const result = mapQueryDataFromApi(compositeQuery);
expect(result.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
expect(result.builder).toStrictEqual({
queryData: [],
queryFormulas: [],
queryTraceOperator: [],
});
expect(result.builder_ai_query?.queryData[0]).toMatchObject({
queryName: 'A',
builderQueryType: 'builder_ai_query',
});
expect(result.builder_ai_query?.queryFormulas[0].queryName).toBe('F1');
});
});

View File

@@ -1,7 +1,4 @@
import {
Querybuildertypesv5BucketOptionsDTO,
Querybuildertypesv5QueryTypeDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Querybuildertypesv5BucketOptionsDTO } from 'api/generated/services/sigNoz.schemas';
import { TelemetryFieldKey } from 'api/v5/v5';
import { Format } from 'constants/formats/types';
import { EQueryType } from 'types/common/dashboard';
@@ -23,14 +20,6 @@ import {
} from '../v5/queryRange';
import { BaseAutocompleteData } from './queryAutocompleteResponse';
export const QueryMode = {
...EQueryType,
/** Not an `EQueryType`: AI queries are builder queries tagged `builderQueryType: builder_ai_query`. */
AI_QUERY_BUILDER: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
} as const;
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode];
// Type for Formula
export interface IBuilderFormula {
expression: string;
@@ -128,6 +117,8 @@ export interface Query {
queryType: EQueryType;
promql: IPromQLQuery[];
builder: QueryBuilderData;
/** AI Query Builder queries; absent until the AI tab is used. */
builder_ai_query?: QueryBuilderData;
clickhouse_sql: IClickHouseQuery[];
id: string;
unit?: Format['id'];
@@ -153,17 +144,3 @@ export type MapQueryDataToApiResult<T> = {
data: T;
newLegendMap: Record<string, string>;
};
/** Query Builder and AI share `Query.builder`; the variant says which one it holds. */
export const BuilderVariant = {
MAIN: 'main',
AI: 'ai',
} as const;
export type BuilderVariant =
(typeof BuilderVariant)[keyof typeof BuilderVariant];
/** Builder queries per variant; `Query` has one `builder` slot, so the off-screen variant lives here. */
export type BuildersByVariant = Partial<
Record<BuilderVariant, Query['builder']>
>;

View File

@@ -2,4 +2,5 @@ export enum EQueryType {
QUERY_BUILDER = 'builder',
CLICKHOUSE = 'clickhouse_sql',
PROM = 'promql',
AI_QUERY_BUILDER = 'builder_ai_query',
}