mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-01 17:10:42 +01:00
Compare commits
9 Commits
main
...
feat/ai-qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
75e6ceb4bb | ||
|
|
7d3273c423 | ||
|
|
a88cc79ef9 | ||
|
|
e424082835 | ||
|
|
2afed07b5b | ||
|
|
1c0dc018e0 | ||
|
|
bb2511ce53 | ||
|
|
33d22c8b59 | ||
|
|
c1b9de0c8a |
@@ -2,8 +2,10 @@ import { cloneDeep, isEmpty } from 'lodash-es';
|
||||
import { SuccessResponse, Warning } from 'types/api';
|
||||
import { MetricRangePayloadV3 } from 'types/api/metrics/getQueryRange';
|
||||
import {
|
||||
BuilderQuery,
|
||||
DistributionData,
|
||||
MetricRangePayloadV5,
|
||||
QueryEnvelope,
|
||||
QueryRangeRequestV5,
|
||||
RawData,
|
||||
ScalarData,
|
||||
@@ -11,6 +13,11 @@ import {
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { QueryDataV3 } from 'types/api/widgets/getQuery';
|
||||
|
||||
const isBuilderQueryEnvelope = (
|
||||
envelope: QueryEnvelope,
|
||||
): envelope is QueryEnvelope & { spec: BuilderQuery } =>
|
||||
envelope.type === 'builder_query' || envelope.type === 'builder_ai_query';
|
||||
|
||||
function getColName(
|
||||
col: ScalarData['columns'][number],
|
||||
legendMap: Record<string, string>,
|
||||
@@ -409,21 +416,15 @@ export function convertV5ResponseToLegacy(
|
||||
const v5Data = payload?.data;
|
||||
|
||||
const aggregationPerQuery =
|
||||
params?.compositeQuery?.queries
|
||||
?.filter((query) => query.type === 'builder_query')
|
||||
.reduce(
|
||||
(acc, query) => {
|
||||
if (
|
||||
query.type === 'builder_query' &&
|
||||
'aggregations' in query.spec &&
|
||||
query.spec.name
|
||||
) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
params?.compositeQuery?.queries?.filter(isBuilderQueryEnvelope).reduce(
|
||||
(acc, query) => {
|
||||
if ('aggregations' in query.spec && query.spec.name) {
|
||||
acc[query.spec.name] = query.spec.aggregations;
|
||||
}
|
||||
return acc;
|
||||
},
|
||||
{} as Record<string, any>,
|
||||
) || {};
|
||||
|
||||
// clickhouse_sql queries have no aggregation metadata; their value columns
|
||||
// are named/keyed by the real SQL alias the response carries (see getColId).
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
QueryBuilderFormula as V5QueryBuilderFormula,
|
||||
QueryEnvelope,
|
||||
QueryRangePayloadV5,
|
||||
RequestType,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
@@ -935,3 +936,41 @@ describe('convertBuilderQueriesToV5 having normalization', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertBuilderQueriesToV5 builder query type', () => {
|
||||
const buildEnvelope = (
|
||||
builderQueryType: IBuilderQuery['builderQueryType'],
|
||||
requestType: RequestType,
|
||||
): QueryEnvelope => {
|
||||
const [envelope] = convertBuilderQueriesToV5(
|
||||
{
|
||||
A: {
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'A',
|
||||
builderQueryType,
|
||||
} as unknown as IBuilderQuery,
|
||||
},
|
||||
requestType,
|
||||
);
|
||||
return envelope;
|
||||
};
|
||||
|
||||
it.each<[RequestType]>([
|
||||
['trace'],
|
||||
['raw'],
|
||||
['time_series'],
|
||||
['scalar'],
|
||||
['distribution'],
|
||||
])('sends builder_ai_query for the %s request type', (requestType) => {
|
||||
expect(buildEnvelope('builder_ai_query', requestType).type).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it.each<[string, IBuilderQuery['builderQueryType']]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('sends builder_query for %s', (_label, builderQueryType) => {
|
||||
expect(buildEnvelope(builderQueryType, 'trace').type).toBe('builder_query');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -364,7 +364,7 @@ export function convertBuilderQueriesToV5(
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'builder_query' as QueryType,
|
||||
type: queryData.builderQueryType ?? 'builder_query',
|
||||
spec,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -16,8 +16,6 @@ import { githubLight } from '@uiw/codemirror-theme-github';
|
||||
import CodeMirror, { EditorView, keymap, Prec } from '@uiw/react-codemirror';
|
||||
import { Button, Card, Collapse, Popover, Tooltip } from 'antd';
|
||||
import { Badge } from '@signozhq/ui/badge';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
negationQueryOperatorSuggestions,
|
||||
@@ -54,6 +52,12 @@ import {
|
||||
SUGGESTION_FETCH_DEBOUNCE_MS,
|
||||
SUGGESTIONS_SECTION,
|
||||
} from './constants';
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
SuggestedFieldKey,
|
||||
SuggestedFieldKeysByName,
|
||||
} from './fieldSuggestions';
|
||||
import {
|
||||
combineInitialAndUserExpression,
|
||||
dedupeOptionsByLabel,
|
||||
@@ -264,10 +268,8 @@ function QuerySearch({
|
||||
);
|
||||
|
||||
// Add back the generateOptions function and useEffect
|
||||
const generateOptions = (keys: {
|
||||
[key: string]: QueryKeyDataSuggestionsProps[];
|
||||
}): any[] =>
|
||||
Object.values(keys).flatMap((items: QueryKeyDataSuggestionsProps[]) =>
|
||||
const generateOptions = (keys: SuggestedFieldKeysByName): any[] =>
|
||||
Object.values(keys).flatMap((items: SuggestedFieldKey[]) =>
|
||||
items.map(({ name, fieldDataType, fieldContext }) => ({
|
||||
label: name,
|
||||
type: fieldDataType === 'string' ? 'keyword' : fieldDataType,
|
||||
@@ -320,8 +322,9 @@ function QuerySearch({
|
||||
|
||||
lastFetchedKeyRef.current = searchText || '';
|
||||
|
||||
const response = await getKeySuggestions({
|
||||
signal: dataSource,
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
searchText: searchText || '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
@@ -363,6 +366,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -496,10 +500,11 @@ function QuerySearch({
|
||||
try {
|
||||
const values = valueSuggestionsOverride
|
||||
? await valueSuggestionsOverride(key, sanitizedSearchText)
|
||||
: await getValueSuggestions({
|
||||
: await fetchFieldValuesForQuery({
|
||||
builderQueryType: queryData.builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText: sanitizedSearchText,
|
||||
signal: dataSource,
|
||||
signalSource: signalSource as 'meter' | '',
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
}).then((response) => {
|
||||
@@ -604,6 +609,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,215 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
fetchFieldKeysForQuery,
|
||||
fetchFieldValuesForQuery,
|
||||
} from '../fieldSuggestions';
|
||||
|
||||
jest.mock('api/generated/services/ai-observability', () => ({
|
||||
getAIObservabilityFieldsKeys: jest.fn(),
|
||||
getAIObservabilityFieldsValues: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getKeySuggestions', () => ({
|
||||
getKeySuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('api/querySuggestions/getValueSuggestion', () => ({
|
||||
getValueSuggestions: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockedAIKeys = getAIObservabilityFieldsKeys as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsKeys
|
||||
>;
|
||||
const mockedGenericKeys = getKeySuggestions as jest.MockedFunction<
|
||||
typeof getKeySuggestions
|
||||
>;
|
||||
const mockedAIValues = getAIObservabilityFieldsValues as jest.MockedFunction<
|
||||
typeof getAIObservabilityFieldsValues
|
||||
>;
|
||||
const mockedGenericValues = getValueSuggestions as jest.MockedFunction<
|
||||
typeof getValueSuggestions
|
||||
>;
|
||||
|
||||
const aiValuesResponse = (
|
||||
values: { stringValues?: string[]; numberValues?: number[] } | null,
|
||||
complete = true,
|
||||
): Awaited<ReturnType<typeof getAIObservabilityFieldsValues>> =>
|
||||
({
|
||||
status: 'success',
|
||||
data: { complete, values },
|
||||
}) as Awaited<ReturnType<typeof getAIObservabilityFieldsValues>>;
|
||||
|
||||
describe('fetchFieldKeysForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: {
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
},
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const keys = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'llm',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).toHaveBeenCalledWith({ searchText: 'llm' });
|
||||
expect(mockedGenericKeys).not.toHaveBeenCalled();
|
||||
expect(keys.data.data).toStrictEqual({
|
||||
complete: true,
|
||||
keys: { llm_call_count: [{ name: 'llm_call_count' }] },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
mockedGenericKeys.mockResolvedValue({
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as Awaited<ReturnType<typeof getKeySuggestions>>);
|
||||
|
||||
await fetchFieldKeysForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: 'svc',
|
||||
});
|
||||
|
||||
expect(mockedAIKeys).not.toHaveBeenCalled();
|
||||
expect(mockedGenericKeys).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ signal: DataSource.TRACES, searchText: 'svc' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('normalizes a null ai_observability keys payload to an empty map', async () => {
|
||||
mockedAIKeys.mockResolvedValue({
|
||||
status: 'success',
|
||||
data: { complete: false, keys: null },
|
||||
} as Awaited<ReturnType<typeof getAIObservabilityFieldsKeys>>);
|
||||
|
||||
const response = await fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(response.data.data).toStrictEqual({ complete: false, keys: {} });
|
||||
});
|
||||
|
||||
it('passes the generic response through untouched', async () => {
|
||||
const genericResponse = {
|
||||
data: { status: 'success', data: { complete: true, keys: {} } },
|
||||
} as unknown as Awaited<ReturnType<typeof getKeySuggestions>>;
|
||||
mockedGenericKeys.mockResolvedValue(genericResponse);
|
||||
|
||||
await expect(
|
||||
fetchFieldKeysForQuery({
|
||||
builderQueryType: 'builder_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fetchFieldValuesForQuery', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('reads the ai_observability endpoint for a builder_ai_query', async () => {
|
||||
mockedAIValues.mockResolvedValue(
|
||||
aiValuesResponse({ stringValues: ['gpt-4o'], numberValues: [] }),
|
||||
);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'gen_ai.request.model',
|
||||
searchText: 'gpt',
|
||||
});
|
||||
|
||||
expect(mockedGenericValues).not.toHaveBeenCalled();
|
||||
expect(response).toStrictEqual({
|
||||
data: {
|
||||
data: {
|
||||
complete: true,
|
||||
values: { stringValues: ['gpt-4o'], numberValues: [] },
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('forwards the key as the name the endpoint expects', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse({}));
|
||||
|
||||
await fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).toHaveBeenCalledWith({
|
||||
name: 'total_tokens',
|
||||
searchText: '',
|
||||
});
|
||||
});
|
||||
|
||||
it('wraps the ai_observability payload in the envelope the call site unwraps', async () => {
|
||||
mockedAIValues.mockResolvedValue(aiValuesResponse(null, false));
|
||||
|
||||
await expect(
|
||||
fetchFieldValuesForQuery({
|
||||
builderQueryType: 'builder_ai_query',
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'llm_call_count',
|
||||
searchText: '',
|
||||
}),
|
||||
).resolves.toStrictEqual({
|
||||
data: { data: { complete: false, values: null } },
|
||||
});
|
||||
});
|
||||
|
||||
it.each<[string, 'builder_query' | undefined]>([
|
||||
['an unmarked query', undefined],
|
||||
['an explicitly generic query', 'builder_query'],
|
||||
])('reads the generic endpoint for %s', async (_label, builderQueryType) => {
|
||||
const genericResponse = {
|
||||
data: {
|
||||
data: { complete: false, values: { stringValues: ['frontend'] } },
|
||||
},
|
||||
} as unknown as Awaited<ReturnType<typeof getValueSuggestions>>;
|
||||
mockedGenericValues.mockResolvedValue(genericResponse);
|
||||
|
||||
const response = await fetchFieldValuesForQuery({
|
||||
builderQueryType,
|
||||
dataSource: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
});
|
||||
|
||||
expect(mockedAIValues).not.toHaveBeenCalled();
|
||||
expect(mockedGenericValues).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
signal: DataSource.TRACES,
|
||||
key: 'service.name',
|
||||
searchText: 'front',
|
||||
}),
|
||||
);
|
||||
expect(response).toBe(genericResponse);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,111 @@
|
||||
import {
|
||||
getAIObservabilityFieldsKeys,
|
||||
getAIObservabilityFieldsValues,
|
||||
} from 'api/generated/services/ai-observability';
|
||||
import { getKeySuggestions } from 'api/querySuggestions/getKeySuggestions';
|
||||
import { getValueSuggestions } from 'api/querySuggestions/getValueSuggestion';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export interface SuggestedFieldKey {
|
||||
name: string;
|
||||
fieldContext?: string;
|
||||
fieldDataType?: string;
|
||||
}
|
||||
|
||||
export type SuggestedFieldKeysByName = Record<string, SuggestedFieldKey[]>;
|
||||
|
||||
export interface SuggestedFieldKeysPayload {
|
||||
complete: boolean;
|
||||
keys: SuggestedFieldKeysByName;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldKeysResponse {
|
||||
data: { data?: SuggestedFieldKeysPayload };
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesPayload {
|
||||
complete?: boolean;
|
||||
values?: {
|
||||
stringValues?: string[] | null;
|
||||
numberValues?: number[] | null;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface SuggestedFieldValuesResponse {
|
||||
data: { data?: SuggestedFieldValuesPayload };
|
||||
}
|
||||
|
||||
interface FetchFieldKeysParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
metricNamespace?: string;
|
||||
}
|
||||
|
||||
interface FetchFieldValuesParams {
|
||||
builderQueryType: IBuilderQuery['builderQueryType'];
|
||||
dataSource: DataSource;
|
||||
key: string;
|
||||
searchText: string;
|
||||
metricName?: string;
|
||||
signalSource?: 'meter' | '';
|
||||
}
|
||||
|
||||
export const fetchFieldKeysForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
}: FetchFieldKeysParams): Promise<SuggestedFieldKeysResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsKeys({ searchText });
|
||||
|
||||
return {
|
||||
data: {
|
||||
data: response.data
|
||||
? { complete: response.data.complete, keys: response.data.keys ?? {} }
|
||||
: undefined,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return getKeySuggestions({
|
||||
signal: dataSource,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
metricNamespace,
|
||||
});
|
||||
};
|
||||
|
||||
export const fetchFieldValuesForQuery = async ({
|
||||
builderQueryType,
|
||||
dataSource,
|
||||
key,
|
||||
searchText,
|
||||
metricName,
|
||||
signalSource,
|
||||
}: FetchFieldValuesParams): Promise<SuggestedFieldValuesResponse> => {
|
||||
if (builderQueryType === 'builder_ai_query') {
|
||||
const response = await getAIObservabilityFieldsValues({
|
||||
name: key,
|
||||
searchText,
|
||||
});
|
||||
|
||||
return { data: { data: response.data } };
|
||||
}
|
||||
|
||||
// getValueSuggestions' declared response type does not match what the endpoint returns.
|
||||
return getValueSuggestions({
|
||||
signal: dataSource,
|
||||
key,
|
||||
searchText,
|
||||
signalSource,
|
||||
metricName,
|
||||
}) as unknown as Promise<SuggestedFieldValuesResponse>;
|
||||
};
|
||||
@@ -54,7 +54,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const showFunctions = query?.functions?.length > 0;
|
||||
const { dataSource } = query;
|
||||
const { dataSource, builderQueryType } = query;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
@@ -94,8 +94,9 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
);
|
||||
|
||||
const showSpanScopeSelector = useMemo(
|
||||
() => dataSource === DataSource.TRACES,
|
||||
[dataSource],
|
||||
() =>
|
||||
dataSource === DataSource.TRACES && builderQueryType !== 'builder_ai_query',
|
||||
[dataSource, builderQueryType],
|
||||
);
|
||||
|
||||
const showInlineQuerySearch = useMemo(() => {
|
||||
|
||||
@@ -348,6 +348,19 @@ export const initialQueryMeterWithType: Query = {
|
||||
},
|
||||
};
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
builder: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export const operatorsByTypes: Record<LocalDataType, string[]> = {
|
||||
string: Object.values(StringOperators),
|
||||
number: Object.values(NumberOperators),
|
||||
|
||||
@@ -11,7 +11,7 @@ import QuickFilters from 'components/QuickFilters/QuickFilters';
|
||||
import { QuickFiltersSource, SignalType } from 'components/QuickFilters/types';
|
||||
import WarningPopover from 'components/WarningPopover/WarningPopover';
|
||||
import { AVAILABLE_EXPORT_PANEL_TYPES } from 'constants/panelTypes';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { usePageActions } from 'container/AIAssistant/pageActions/usePageActions';
|
||||
import ExplorerOptionWrapper from 'container/ExplorerOptions/ExplorerOptionWrapper';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
@@ -51,7 +51,7 @@ import {
|
||||
} from 'utils/explorerUtils';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
import { TOOLBAR_VIEWS } from './constants';
|
||||
import { DEFAULT_PANEL_TYPE, TOOLBAR_VIEWS } from './constants';
|
||||
import ListView from './ListView/ListView';
|
||||
import { defaultSelectedColumns } from './ListView/configs';
|
||||
import QuerySection from './QuerySection/QuerySection';
|
||||
@@ -88,7 +88,7 @@ function Explorer(): JSX.Element {
|
||||
const listQueryKeyRef = useRef<any>();
|
||||
|
||||
// Get panel type from URL
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const panelTypesFromUrl = useGetPanelTypesQueryParam(DEFAULT_PANEL_TYPE);
|
||||
const [isLoadingQueries, setIsLoadingQueries] = useState<boolean>(false);
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
|
||||
@@ -118,8 +118,8 @@ function Explorer(): JSX.Element {
|
||||
const defaultQuery = useMemo(
|
||||
(): Query =>
|
||||
updateAllQueriesOperators(
|
||||
initialQueriesMap.traces,
|
||||
PANEL_TYPES.LIST,
|
||||
initialQueryAIWithType,
|
||||
DEFAULT_PANEL_TYPE,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
[updateAllQueriesOperators],
|
||||
@@ -185,8 +185,8 @@ function Explorer(): JSX.Element {
|
||||
const exportDefaultQuery = useMemo(
|
||||
() =>
|
||||
getQueryByPanelType(
|
||||
stagedQuery || initialQueriesMap.traces,
|
||||
panelType || PANEL_TYPES.LIST,
|
||||
stagedQuery || initialQueryAIWithType,
|
||||
panelType || DEFAULT_PANEL_TYPE,
|
||||
),
|
||||
[stagedQuery, panelType],
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ import ListViewOrderBy from 'components/OrderBy/ListViewOrderBy';
|
||||
import type { TableColumnDef } from 'components/TanStackTableView/types';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import { useOptionsMenu } from 'container/OptionsMenu';
|
||||
import { CustomTimeType } from 'container/TopNav/DateTimeSelectionV2/types';
|
||||
@@ -94,7 +94,7 @@ function ListView({
|
||||
paginationQueryData ?? getDefaultPaginationConfig(PER_PAGE_OPTIONS);
|
||||
|
||||
const requestQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces, orderBy),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType, orderBy),
|
||||
[stagedQuery, orderBy],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,42 +1,25 @@
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { QueryBuilderV2 } from 'components/QueryBuilderV2/QueryBuilderV2';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ExplorerOrderBy from 'container/ExplorerOrderBy';
|
||||
import { OrderByFilterProps } from 'container/QueryBuilder/filters/OrderByFilter/OrderByFilter.interfaces';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useGetPanelTypesQueryParam } from 'hooks/queryBuilder/useGetPanelTypesQueryParam';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { DEFAULT_PANEL_TYPE } from '../constants';
|
||||
|
||||
function QuerySection(): JSX.Element {
|
||||
const panelTypes = useGetPanelTypesQueryParam(PANEL_TYPES.LIST);
|
||||
const panelTypes = useGetPanelTypesQueryParam(DEFAULT_PANEL_TYPE);
|
||||
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(() => {
|
||||
const isList = panelTypes === PANEL_TYPES.LIST;
|
||||
const config: QueryBuilderProps['filterConfigs'] = {
|
||||
// Only reaches the builder for timeseries/table; list/trace panels use QueryBuilderV2's listViewTracesFilterConfigs.
|
||||
const filterConfigs: QueryBuilderProps['filterConfigs'] = useMemo(
|
||||
() => ({
|
||||
stepInterval: { isHidden: false, isDisabled: false },
|
||||
limit: { isHidden: isList, isDisabled: true },
|
||||
having: { isHidden: isList, isDisabled: true },
|
||||
};
|
||||
|
||||
return config;
|
||||
}, [panelTypes]);
|
||||
|
||||
const renderOrderBy = useCallback(
|
||||
({ query, onChange }: OrderByFilterProps) => (
|
||||
<ExplorerOrderBy query={query} onChange={onChange} />
|
||||
),
|
||||
limit: { isHidden: false, isDisabled: true },
|
||||
having: { isHidden: false, isDisabled: true },
|
||||
}),
|
||||
[],
|
||||
);
|
||||
|
||||
const queryComponents = useMemo((): QueryBuilderProps['queryComponents'] => {
|
||||
const shouldRenderCustomOrderBy =
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE;
|
||||
|
||||
return {
|
||||
...(shouldRenderCustomOrderBy ? { renderOrderBy } : {}),
|
||||
};
|
||||
}, [panelTypes, renderOrderBy]);
|
||||
|
||||
const isListViewPanel = useMemo(
|
||||
() => panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE,
|
||||
[panelTypes],
|
||||
@@ -45,14 +28,10 @@ function QuerySection(): JSX.Element {
|
||||
return (
|
||||
<QueryBuilderV2
|
||||
isListViewPanel={isListViewPanel}
|
||||
showTraceOperator
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
queryComponents={queryComponents}
|
||||
panelType={panelTypes}
|
||||
filterConfigs={filterConfigs}
|
||||
showOnlyWhereClause={
|
||||
panelTypes === PANEL_TYPES.LIST || panelTypes === PANEL_TYPES.TRACE
|
||||
}
|
||||
showOnlyWhereClause={isListViewPanel}
|
||||
version="v3" // setting this to v3 as we this is rendered in logs explorer
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import logEvent from 'api/common/logEvent';
|
||||
import DownloadOptionsMenu from 'components/DownloadOptionsMenu/DownloadOptionsMenu';
|
||||
import { ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { initialQueryAIWithType, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
|
||||
import TraceExplorerControls from 'container/TracesExplorer/Controls';
|
||||
import { getListViewQuery } from 'container/TracesExplorer/explorerUtils';
|
||||
@@ -60,7 +60,7 @@ function TracesView({
|
||||
);
|
||||
|
||||
const transformedQuery = useMemo(
|
||||
() => getListViewQuery(stagedQuery || initialQueriesMap.traces),
|
||||
() => getListViewQuery(stagedQuery || initialQueryAIWithType),
|
||||
[stagedQuery],
|
||||
);
|
||||
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
|
||||
export const DEFAULT_PANEL_TYPE = PANEL_TYPES.TRACE;
|
||||
|
||||
export const TOOLBAR_VIEWS = {
|
||||
list: {
|
||||
name: 'list',
|
||||
|
||||
@@ -37,11 +37,13 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
|
||||
compositeQuery.queries?.forEach((q) => {
|
||||
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
|
||||
if (q.type === 'builder_query') {
|
||||
if (q.type === 'builder_query' || q.type === 'builder_ai_query') {
|
||||
if (spec.name) {
|
||||
builderQueries[spec.name] = convertBuilderQueryToIBuilderQuery(
|
||||
spec as BuilderQuery,
|
||||
);
|
||||
builderQueries[spec.name] = {
|
||||
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
|
||||
builderQueryType: q.type,
|
||||
};
|
||||
// Both share the builder bucket; the AI variant rides on the query itself.
|
||||
builderQueryTypes[spec.name] = 'builder_query';
|
||||
}
|
||||
} else if (q.type === 'builder_formula') {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
useMemo,
|
||||
} from 'react';
|
||||
import { Color } from '@signozhq/design-tokens';
|
||||
import { Atom, Terminal } from '@signozhq/icons';
|
||||
import { Atom, Sparkles, Terminal } from '@signozhq/icons';
|
||||
import { Tabs } from 'antd';
|
||||
import cx from 'classnames';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
@@ -21,15 +21,24 @@ import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interface
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getHiddenQueryBuilderFields,
|
||||
getSupportedQueryTypes,
|
||||
supportsAIQuery,
|
||||
} from '../../Panels/capabilities';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
type PanelKind,
|
||||
} from '../../Panels/types/panelKind';
|
||||
import {
|
||||
AI_QUERY_TAB,
|
||||
type QueryTabKey,
|
||||
resolveActiveQueryTab,
|
||||
toAIQuery,
|
||||
withAIQueryType,
|
||||
} from './utils';
|
||||
|
||||
import styles from './PanelEditorQueryBuilder.module.scss';
|
||||
|
||||
@@ -69,11 +78,20 @@ function PanelEditorQueryBuilder({
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
const isDarkMode = useIsDarkMode();
|
||||
|
||||
// The AI tab is not a query type — it stamps `builderQueryType` onto the builder
|
||||
// queries (and pins them to traces, the only signal AI queries support).
|
||||
const handleQueryCategoryChange = useCallback(
|
||||
(queryType: string): void => {
|
||||
(nextTab: string): void => {
|
||||
if (nextTab === AI_QUERY_TAB) {
|
||||
redirectWithQueryBuilderData({
|
||||
...toAIQuery(currentQuery),
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
});
|
||||
return;
|
||||
}
|
||||
redirectWithQueryBuilderData({
|
||||
...currentQuery,
|
||||
queryType: queryType as EQueryType,
|
||||
...withAIQueryType(currentQuery, false),
|
||||
queryType: nextTab as EQueryType,
|
||||
});
|
||||
},
|
||||
[currentQuery, redirectWithQueryBuilderData],
|
||||
@@ -101,9 +119,32 @@ function PanelEditorQueryBuilder({
|
||||
);
|
||||
|
||||
const items = useMemo(() => {
|
||||
const supportedQueryTypes = getSupportedQueryTypes(panelKind);
|
||||
const supportedQueryTypes: QueryTabKey[] = getSupportedQueryTypes(panelKind);
|
||||
const supportedTabs = supportsAIQuery(panelKind)
|
||||
? [...supportedQueryTypes, AI_QUERY_TAB]
|
||||
: supportedQueryTypes;
|
||||
|
||||
const queryTypeComponents = {
|
||||
[AI_QUERY_TAB]: {
|
||||
icon: <Sparkles size={14} />,
|
||||
label: 'AI Query Builder',
|
||||
component: (
|
||||
<div className="query-builder-v2-container">
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
filterConfigs={filterConfigs}
|
||||
config={{
|
||||
initialDataSource: DataSource.TRACES,
|
||||
queryVariant: 'static',
|
||||
}}
|
||||
version="v3"
|
||||
isListViewPanel={panelType === PANEL_TYPES.LIST}
|
||||
queryComponents={{}}
|
||||
savePreviousQuery
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
@@ -138,15 +179,15 @@ function PanelEditorQueryBuilder({
|
||||
},
|
||||
};
|
||||
|
||||
return supportedQueryTypes.map((queryType) => ({
|
||||
key: queryType,
|
||||
return supportedTabs.map((tabKey) => ({
|
||||
key: tabKey,
|
||||
label: (
|
||||
<div className={styles.queryTypeTab}>
|
||||
{queryTypeComponents[queryType].icon}
|
||||
<Typography>{queryTypeComponents[queryType].label}</Typography>
|
||||
{queryTypeComponents[tabKey].icon}
|
||||
<Typography>{queryTypeComponents[tabKey].label}</Typography>
|
||||
</div>
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
children: queryTypeComponents[tabKey].component,
|
||||
}));
|
||||
}, [panelKind, panelType, filterConfigs, isDarkMode]);
|
||||
|
||||
@@ -163,7 +204,7 @@ function PanelEditorQueryBuilder({
|
||||
className={cx(styles.tabsContainer, {
|
||||
[styles.stickyNav]: stickyHeader,
|
||||
})}
|
||||
activeKey={currentQuery.queryType}
|
||||
activeKey={resolveActiveQueryTab(currentQuery)}
|
||||
onChange={handleQueryCategoryChange}
|
||||
tabBarExtraContent={
|
||||
<span className={styles.runQueryBtnContainer}>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { OPERATORS } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
|
||||
|
||||
@@ -61,6 +62,7 @@ function lastQueryBuilderProps(): {
|
||||
panelType: string;
|
||||
isListViewPanel: boolean;
|
||||
filterConfigs: unknown;
|
||||
config?: unknown;
|
||||
} {
|
||||
const calls = mockQueryBuilderV2.mock.calls;
|
||||
return calls[calls.length - 1][0];
|
||||
@@ -70,15 +72,20 @@ describe('PanelEditorQueryBuilder query-type tabs (driven by the capabilities gu
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: { queryType: EQueryType.QUERY_BUILDER },
|
||||
currentQuery: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [] },
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
it('shows only the Query Builder tab for the List kind', () => {
|
||||
it('shows only the Query Builder tabs for the List kind', () => {
|
||||
renderBuilder('signoz/ListPanel', TelemetrytypesSignalDTO.logs);
|
||||
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.getByText('AI Query Builder')).toBeInTheDocument();
|
||||
expect(screen.queryByText('ClickHouse Query')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('PromQL')).not.toBeInTheDocument();
|
||||
});
|
||||
@@ -91,21 +98,62 @@ describe('PanelEditorQueryBuilder query-type tabs (driven by the capabilities gu
|
||||
expect(screen.queryByText('PromQL')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows all three tabs for the Time Series kind', () => {
|
||||
it('shows all four tabs for the Time Series kind', () => {
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
expect(screen.getByText('Query Builder')).toBeInTheDocument();
|
||||
expect(screen.getByText('AI Query Builder')).toBeInTheDocument();
|
||||
expect(screen.getByText('ClickHouse Query')).toBeInTheDocument();
|
||||
expect(screen.getByText('PromQL')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
// The AI tab is derived from `builderQueryType`, not from a stored tab key.
|
||||
it('activates the AI tab when the builder query carries the AI envelope tag', () => {
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ builderQueryType: 'builder_ai_query' }] },
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
});
|
||||
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
expect(
|
||||
screen.getByRole('tab', { name: 'AI Query Builder', selected: true }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('pins the AI tab builder to traces so the signal cannot be changed', () => {
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ builderQueryType: 'builder_ai_query' }] },
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
});
|
||||
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
expect(lastQueryBuilderProps().config).toStrictEqual({
|
||||
initialDataSource: DataSource.TRACES,
|
||||
queryVariant: 'static',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PanelEditorQueryBuilder field visibility (driven by the capabilities guard)', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockUseQueryBuilder.mockReturnValue({
|
||||
currentQuery: { queryType: EQueryType.QUERY_BUILDER },
|
||||
currentQuery: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [] },
|
||||
},
|
||||
redirectWithQueryBuilderData: jest.fn(),
|
||||
updateAllQueriesOperators: jest.fn(),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
AI_QUERY_TAB,
|
||||
isAIQuery,
|
||||
resolveActiveQueryTab,
|
||||
toAIQuery,
|
||||
withAIQueryType,
|
||||
} from '../utils';
|
||||
|
||||
function makeQuery(
|
||||
queryData: Record<string, unknown>[],
|
||||
queryType: EQueryType = EQueryType.QUERY_BUILDER,
|
||||
): Query {
|
||||
return {
|
||||
queryType,
|
||||
builder: { queryData, queryFormulas: [], queryTraceOperator: [] },
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
id: 'test',
|
||||
} as unknown as Query;
|
||||
}
|
||||
|
||||
describe('isAIQuery', () => {
|
||||
it('is true when any builder query carries the AI envelope tag', () => {
|
||||
expect(
|
||||
isAIQuery(
|
||||
makeQuery([{ queryName: 'A' }, { builderQueryType: 'builder_ai_query' }]),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is false for plain builder queries and for an empty builder', () => {
|
||||
expect(isAIQuery(makeQuery([{ queryName: 'A' }]))).toBe(false);
|
||||
expect(isAIQuery(makeQuery([]))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveActiveQueryTab', () => {
|
||||
it('selects the AI tab for a tagged builder query', () => {
|
||||
expect(
|
||||
resolveActiveQueryTab(makeQuery([{ builderQueryType: 'builder_ai_query' }])),
|
||||
).toBe(AI_QUERY_TAB);
|
||||
});
|
||||
|
||||
it('selects the query type for an untagged query', () => {
|
||||
expect(resolveActiveQueryTab(makeQuery([{ queryName: 'A' }]))).toBe(
|
||||
EQueryType.QUERY_BUILDER,
|
||||
);
|
||||
});
|
||||
|
||||
// A PromQL panel reads its queries from a different bucket, so a stale tag on the
|
||||
// builder bucket must not steal the active tab.
|
||||
it('keeps PromQL selected even if the builder bucket carries a tag', () => {
|
||||
expect(
|
||||
resolveActiveQueryTab(
|
||||
makeQuery([{ builderQueryType: 'builder_ai_query' }], EQueryType.PROM),
|
||||
),
|
||||
).toBe(EQueryType.PROM);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toAIQuery', () => {
|
||||
// The backend decodes a builder_ai_query spec as QueryBuilderQuery[TraceAggregation],
|
||||
// which has no `metricName` — a carried-over metrics aggregation fails the request.
|
||||
it('re-seeds a metrics query onto traces, dropping the metric aggregation', () => {
|
||||
const result = toAIQuery(
|
||||
makeQuery([
|
||||
{
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.METRICS,
|
||||
aggregations: [{ metricName: 'signoz_latency_bucket' }],
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
const [queryData] = result.builder.queryData;
|
||||
expect(queryData.dataSource).toBe(DataSource.TRACES);
|
||||
expect(queryData.aggregations).toStrictEqual([{ expression: 'count() ' }]);
|
||||
expect(queryData.builderQueryType).toBe('builder_ai_query');
|
||||
});
|
||||
|
||||
it('keeps the filter on a query already using traces', () => {
|
||||
const result = toAIQuery(
|
||||
makeQuery([
|
||||
{
|
||||
queryName: 'A',
|
||||
dataSource: DataSource.TRACES,
|
||||
filter: { expression: "service.name = 'checkout'" },
|
||||
},
|
||||
]),
|
||||
);
|
||||
|
||||
expect(result.builder.queryData[0].filter).toStrictEqual({
|
||||
expression: "service.name = 'checkout'",
|
||||
});
|
||||
expect(result.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
|
||||
});
|
||||
|
||||
it('preserves the query name when re-seeding', () => {
|
||||
const result = toAIQuery(
|
||||
makeQuery([{ queryName: 'B', dataSource: DataSource.LOGS }]),
|
||||
);
|
||||
|
||||
expect(result.builder.queryData[0].queryName).toBe('B');
|
||||
});
|
||||
});
|
||||
|
||||
describe('withAIQueryType', () => {
|
||||
it('stamps the tag onto every builder query', () => {
|
||||
const result = withAIQueryType(
|
||||
makeQuery([{ queryName: 'A' }, { queryName: 'B' }]),
|
||||
true,
|
||||
);
|
||||
|
||||
expect(
|
||||
result.builder.queryData.map((item) => item.builderQueryType),
|
||||
).toStrictEqual(['builder_ai_query', 'builder_ai_query']);
|
||||
});
|
||||
|
||||
it('deletes the key when clearing, rather than setting undefined', () => {
|
||||
const result = withAIQueryType(
|
||||
makeQuery([{ queryName: 'A', builderQueryType: 'builder_ai_query' }]),
|
||||
false,
|
||||
);
|
||||
|
||||
expect(result.builder.queryData[0]).not.toHaveProperty('builderQueryType');
|
||||
expect(result.builder.queryData[0]).toStrictEqual({ queryName: 'A' });
|
||||
});
|
||||
|
||||
it('returns the query untouched when it already matches', () => {
|
||||
const tagged = makeQuery([{ builderQueryType: 'builder_ai_query' }]);
|
||||
const plain = makeQuery([{ queryName: 'A' }]);
|
||||
|
||||
expect(withAIQueryType(tagged, true)).toBe(tagged);
|
||||
expect(withAIQueryType(plain, false)).toBe(plain);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,91 @@
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import type {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
/**
|
||||
* Tab key for the AI query builder. Deliberately not an `EQueryType`: an AI query is
|
||||
* a builder query carrying `builderQueryType: 'builder_ai_query'`, so the query type
|
||||
* on the wire stays `builder` and only the per-query envelope tag differs. Keeping the
|
||||
* tab out of the enum leaves that tag the single source of truth.
|
||||
*/
|
||||
export const AI_QUERY_TAB = 'ai_builder' as const;
|
||||
|
||||
export type QueryTabKey = EQueryType | typeof AI_QUERY_TAB;
|
||||
|
||||
export function isAIQuery(query: Query): boolean {
|
||||
return query.builder.queryData.some(
|
||||
(item) => item.builderQueryType === 'builder_ai_query',
|
||||
);
|
||||
}
|
||||
|
||||
/** The tab to highlight — derived from the queries, never stored separately. */
|
||||
export function resolveActiveQueryTab(query: Query): QueryTabKey {
|
||||
return query.queryType === EQueryType.QUERY_BUILDER && isAIQuery(query)
|
||||
? AI_QUERY_TAB
|
||||
: query.queryType;
|
||||
}
|
||||
|
||||
/** Carried across a signal switch, mirroring the builder's own datasource selector. */
|
||||
const PRESERVED_ON_SIGNAL_SWITCH = ['queryName', 'expression'];
|
||||
|
||||
/**
|
||||
* Re-seed a non-traces query with the traces defaults, the way `handleChangeDataSource`
|
||||
* does. AI queries are traces-only, and a leftover metrics aggregation carries
|
||||
* `metricName` — a field the backend rejects on a trace spec. A query already on traces
|
||||
* keeps its filters, so switching tabs on a trace query is non-destructive.
|
||||
*/
|
||||
function toTracesQueryData(item: IBuilderQuery): IBuilderQuery {
|
||||
if (item.dataSource === DataSource.TRACES) {
|
||||
return item;
|
||||
}
|
||||
|
||||
const tracesDefaults = Object.fromEntries(
|
||||
Object.entries(initialQueryBuilderFormValuesMap[DataSource.TRACES]).filter(
|
||||
([key]) => !PRESERVED_ON_SIGNAL_SWITCH.includes(key),
|
||||
),
|
||||
);
|
||||
return { ...item, ...tracesDefaults, dataSource: DataSource.TRACES };
|
||||
}
|
||||
|
||||
/** Move a query onto the AI builder: pin every query to traces and tag it. */
|
||||
export function toAIQuery(query: Query): Query {
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item) => ({
|
||||
...toTracesQueryData(item),
|
||||
builderQueryType: 'builder_ai_query' as const,
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Stamp or clear `builderQueryType` across every builder query. Returns the query
|
||||
* untouched when it already matches, and deletes the key rather than setting it to
|
||||
* `undefined` — the dirty checks compare by value, so a stray key reads as an edit.
|
||||
*/
|
||||
export function withAIQueryType(query: Query, enabled: boolean): Query {
|
||||
const needsUpdate = query.builder.queryData.some(
|
||||
(item) => (item.builderQueryType === 'builder_ai_query') !== enabled,
|
||||
);
|
||||
if (!needsUpdate) {
|
||||
return query;
|
||||
}
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: query.builder.queryData.map((item): IBuilderQuery => {
|
||||
const { builderQueryType: _dropped, ...rest } = item;
|
||||
return enabled ? { ...rest, builderQueryType: 'builder_ai_query' } : rest;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -5,7 +5,10 @@ import { handleQueryChange } from 'container/NewWidget/utils';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryType } from '../../../Panels/capabilities';
|
||||
import {
|
||||
resolveQueryType,
|
||||
supportsAIQuery,
|
||||
} from '../../../Panels/capabilities';
|
||||
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../../queryV5/persesQueryAdapters';
|
||||
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
|
||||
@@ -19,6 +22,7 @@ jest.mock('container/NewWidget/utils', () => ({
|
||||
}));
|
||||
jest.mock('../../../Panels/capabilities', () => ({
|
||||
resolveQueryType: jest.fn(),
|
||||
supportsAIQuery: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../queryV5/persesQueryAdapters', () => ({
|
||||
toPerses: jest.fn(),
|
||||
@@ -33,6 +37,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
|
||||
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
|
||||
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
|
||||
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
|
||||
const mockSupportsAIQuery = supportsAIQuery 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;
|
||||
@@ -96,7 +101,11 @@ describe('usePanelTypeSwitch', () => {
|
||||
|
||||
it('does nothing when switching to the current kind', () => {
|
||||
const setSpec = jest.fn();
|
||||
const state = builderState({ id: 'q', queryType: 'builder' } as Query);
|
||||
const state = builderState({
|
||||
id: 'q',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
@@ -114,7 +123,11 @@ describe('usePanelTypeSwitch', () => {
|
||||
|
||||
it('on first visit: transforms the query and resets the spec to the new kind', () => {
|
||||
const setSpec = jest.fn();
|
||||
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
|
||||
const tableQuery = {
|
||||
id: 'table-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query;
|
||||
const state = builderState(tableQuery);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
@@ -142,7 +155,11 @@ describe('usePanelTypeSwitch', () => {
|
||||
it('seeds timestamp-desc Order By on every query when switching to a List panel', () => {
|
||||
const setSpec = jest.fn();
|
||||
mockUseQueryBuilder.mockReturnValue(
|
||||
builderState({ id: 'ts-current', queryType: 'builder' } as Query),
|
||||
builderState({
|
||||
id: 'ts-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query),
|
||||
);
|
||||
mockHandleQueryChange.mockReturnValue({
|
||||
id: 'transformed',
|
||||
@@ -169,7 +186,11 @@ describe('usePanelTypeSwitch', () => {
|
||||
|
||||
it('coerces the query type when the new kind disallows it (promql → List)', () => {
|
||||
const setSpec = jest.fn();
|
||||
const promQuery = { id: 'prom', queryType: 'promql' } as Query;
|
||||
const promQuery = {
|
||||
id: 'prom',
|
||||
queryType: 'promql',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query;
|
||||
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
@@ -191,10 +212,88 @@ describe('usePanelTypeSwitch', () => {
|
||||
expect((queryArg as Query).queryType).toBe('builder');
|
||||
});
|
||||
|
||||
// `handleQueryChange` rebuilds from a field allow-list that omits `builderQueryType`,
|
||||
// so the tag has to be re-applied after the rebuild or the AI tab silently reverts.
|
||||
it('re-applies the AI envelope tag when the new kind supports AI queries', () => {
|
||||
const setSpec = jest.fn();
|
||||
mockSupportsAIQuery.mockReturnValue(true);
|
||||
mockHandleQueryChange.mockReturnValue({
|
||||
id: 'transformed',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [{ orderBy: [] }] },
|
||||
} as unknown as Query);
|
||||
const aiQuery = {
|
||||
id: 'ai-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [{ builderQueryType: 'builder_ai_query' }] },
|
||||
} as unknown as Query;
|
||||
const state = builderState(aiQuery);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelTypeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
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',
|
||||
);
|
||||
});
|
||||
|
||||
it('drops the AI envelope tag when the new kind has no AI tab', () => {
|
||||
const setSpec = jest.fn();
|
||||
mockSupportsAIQuery.mockReturnValue(false);
|
||||
mockHandleQueryChange.mockReturnValue({
|
||||
id: 'transformed',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [{ orderBy: [] }] },
|
||||
} as unknown as Query);
|
||||
const aiQuery = {
|
||||
id: 'ai-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [{ builderQueryType: 'builder_ai_query' }] },
|
||||
} as unknown as Query;
|
||||
const state = builderState(aiQuery);
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelTypeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
}),
|
||||
);
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// The rebuild receives an untagged query…
|
||||
const [, queryArg] = mockHandleQueryChange.mock.calls[0];
|
||||
expect((queryArg as Query).builder.queryData[0]).not.toHaveProperty(
|
||||
'builderQueryType',
|
||||
);
|
||||
// …and nothing re-applies it afterwards.
|
||||
const redirected = state.redirectWithQueryBuilderData.mock
|
||||
.calls[0][0] as Query;
|
||||
expect(redirected.builder.queryData[0].builderQueryType).toBeUndefined();
|
||||
});
|
||||
|
||||
it('restores the original kind verbatim on switch-back (reversibility)', () => {
|
||||
const setSpec = jest.fn();
|
||||
const tableQuery = { id: 'table-current', queryType: 'builder' } as Query;
|
||||
const listQuery = { id: 'list-current', queryType: 'builder' } as Query;
|
||||
const tableQuery = {
|
||||
id: 'table-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query;
|
||||
const listQuery = {
|
||||
id: 'list-current',
|
||||
queryType: 'builder',
|
||||
builder: { queryData: [] },
|
||||
} as unknown as Query;
|
||||
let state = builderState(tableQuery);
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ import type {
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryType } from '../../Panels/capabilities';
|
||||
import { resolveQueryType, supportsAIQuery } from '../../Panels/capabilities';
|
||||
import {
|
||||
PANEL_KIND_TO_PANEL_TYPE,
|
||||
type PanelKind,
|
||||
@@ -29,6 +29,7 @@ import {
|
||||
getSwitchedPluginSpec,
|
||||
type SwitchedPluginSpec,
|
||||
} from '../getSwitchedPluginSpec';
|
||||
import { isAIQuery, withAIQueryType } from '../PanelEditorQueryBuilder/utils';
|
||||
|
||||
// V1's handleQueryChange clears orderBy for lists; re-seed the fresh-list default (timestamp desc).
|
||||
const DEFAULT_LIST_ORDER_BY: OrderByPayload[] = [
|
||||
@@ -139,16 +140,24 @@ export function usePanelTypeSwitch({
|
||||
// First visit → coerce the query type if the new kind disallows it, then
|
||||
// rebuild the builder query for the new type.
|
||||
const queryType = resolveQueryType(newKind, query.queryType);
|
||||
// AI-ness rides on the query, not on `queryType`, so `resolveQueryType` can't
|
||||
// see it: carry it across only when the new kind has an AI tab to surface it.
|
||||
const keepAIQueryType = supportsAIQuery(newKind) && isAIQuery(query);
|
||||
const transformed = handleQueryChange(
|
||||
newPanelType as keyof PartialPanelTypes,
|
||||
{ ...query, queryType },
|
||||
{ ...withAIQueryType(query, false), queryType },
|
||||
panelTypeRef.current,
|
||||
);
|
||||
// Match a fresh list panel's default order so the builder's Order By isn't empty.
|
||||
const nextQuery =
|
||||
const reordered =
|
||||
newPanelType === PANEL_TYPES.LIST
|
||||
? withDefaultListOrder(transformed)
|
||||
: transformed;
|
||||
// `handleQueryChange` rebuilds each query from an allow-list of fields that
|
||||
// doesn't include `builderQueryType`, so re-stamp it after the rebuild.
|
||||
const nextQuery = keepAIQueryType
|
||||
? withAIQueryType(reordered, true)
|
||||
: reordered;
|
||||
const signal = getBuilderQueries(currentSpec.queries)[0]
|
||||
?.signal as TelemetrytypesSignalDTO;
|
||||
|
||||
|
||||
@@ -39,6 +39,15 @@ export function isQueryTypeSupportedByPanelKind(
|
||||
return getSupportedQueryTypes(kind).includes(queryType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether a kind offers the AI query builder. Separate from `supportedQueryTypes`
|
||||
* because an AI query is a builder query carrying `builderQueryType`, not its own
|
||||
* `EQueryType` — the tab is UI state, the wire type stays `builder`.
|
||||
*/
|
||||
export function supportsAIQuery(kind: PanelKind): boolean {
|
||||
return getPanelDefinition(kind).supportsAIQuery === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Master guard: is this panel kind renderable with this query type (and, in builder
|
||||
* mode, this signal)? ClickHouse/PromQL queries carry no signal, so the signal is
|
||||
|
||||
@@ -19,6 +19,7 @@ export const definition: PanelDefinition<'signoz/BarChartPanel'> = {
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const definition: PanelDefinition<'signoz/HistogramPanel'> = {
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -19,6 +19,7 @@ export const definition: PanelDefinition<'signoz/ListPanel'> = {
|
||||
// hide `limit` (the server paginates raw spans). Mirrors QueryBuilderV2's internal
|
||||
// list configs — the capabilities guard is the single source for both.
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {
|
||||
default: {
|
||||
stepInterval: { isHidden: true, isDisabled: true },
|
||||
|
||||
@@ -19,6 +19,7 @@ export const definition: PanelDefinition<'signoz/NumberPanel'> = {
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ export const definition: PanelDefinition<'signoz/PieChartPanel'> = {
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -15,6 +15,7 @@ export const definition: PanelDefinition<'signoz/TablePanel'> = {
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
supportedQueryTypes: [EQueryType.QUERY_BUILDER, EQueryType.CLICKHOUSE],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
// Tables carry tabular data worth exporting (V1 parity: download is table-only).
|
||||
actions: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export const definition: PanelDefinition<'signoz/TimeSeriesPanel'> = {
|
||||
EQueryType.CLICKHOUSE,
|
||||
EQueryType.PROM,
|
||||
],
|
||||
supportsAIQuery: true,
|
||||
queryBuilderFields: {},
|
||||
actions: {
|
||||
view: true,
|
||||
|
||||
@@ -48,6 +48,8 @@ export interface PanelDefinition<K extends PanelKind = PanelKind> {
|
||||
supportedSignals: TelemetrytypesSignalDTO[];
|
||||
/** Query languages this kind supports (Query Builder / ClickHouse / PromQL). */
|
||||
supportedQueryTypes: EQueryType[];
|
||||
/** Kind offers the AI query builder — a traces-only builder variant, not its own query language. */
|
||||
supportsAIQuery?: boolean;
|
||||
/** Query-builder fields this kind hides/disables, optionally per signal (`{}` hides none). */
|
||||
queryBuilderFields: QueryBuilderFieldRule;
|
||||
actions: PanelActionCapabilities;
|
||||
|
||||
@@ -5,8 +5,8 @@ import type {
|
||||
import type { BuilderQuery } from 'types/api/v5/queryRange';
|
||||
|
||||
/**
|
||||
* Flattens a panel's queries into its builder queries, unwrapping
|
||||
* `CompositeQuery` envelopes. Non-builder kinds (PromQL, ClickHouseSQL, Formula,
|
||||
* Flattens a panel's queries into its builder queries (`builder_query` and its AI
|
||||
* variant), unwrapping `CompositeQuery` envelopes. Non-builder kinds (PromQL, ClickHouseSQL, Formula,
|
||||
* TraceOperator) are dropped — they lack the legend/groupBy/aggregation context
|
||||
* downstream code needs. Returns the generated v5 `BuilderQuery` shape directly.
|
||||
*/
|
||||
@@ -22,7 +22,7 @@ export function getBuilderQueries(
|
||||
}
|
||||
if (plugin.kind === 'signoz/CompositeQuery') {
|
||||
(plugin.spec.queries || []).forEach((sub) => {
|
||||
if (sub.type === 'builder_query') {
|
||||
if (sub.type === 'builder_query' || sub.type === 'builder_ai_query') {
|
||||
flattened.push(sub.spec as BuilderQuery);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -2,7 +2,11 @@ import type {
|
||||
DashboardtypesQueryDTO,
|
||||
Querybuildertypesv5QueryEnvelopeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
@@ -171,6 +175,23 @@ describe('persesQueryAdapters', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves an AI builder query through toPerses → fromPerses', () => {
|
||||
const original: Query = initialQueryAIWithType;
|
||||
|
||||
const perses = toPerses(original, PANEL_TYPES.TIME_SERIES);
|
||||
const { queries } = perses[0].spec.plugin.spec as {
|
||||
queries: Querybuildertypesv5QueryEnvelopeDTO[];
|
||||
};
|
||||
expect(queries[0].type).toBe('builder_ai_query');
|
||||
|
||||
const restored = fromPerses(perses, PANEL_TYPES.TIME_SERIES);
|
||||
|
||||
expect(restored.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(restored.builder.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves a List builder query through toPerses → fromPerses', () => {
|
||||
const original: Query = initialQueriesMap[DataSource.LOGS];
|
||||
|
||||
|
||||
@@ -475,6 +475,7 @@ export function QueryBuilderProvider({
|
||||
const newQuery: IBuilderQuery = {
|
||||
...initialBuilderQuery,
|
||||
source: queries?.[0]?.source || '',
|
||||
builderQueryType: queries?.[0]?.builderQueryType,
|
||||
queryName: createNewBuilderItemName({ existNames, sourceNames: alphabet }),
|
||||
expression: createNewBuilderItemName({
|
||||
existNames,
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
describe('createNewBuilderQuery builderQueryType propagation', () => {
|
||||
it('carries builderQueryType from the first query onto an added query', () => {
|
||||
const { result } = renderQueryBuilder(initialQueryAIWithType);
|
||||
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[0].builderQueryType,
|
||||
).toBe('builder_ai_query');
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
|
||||
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[1].builderQueryType,
|
||||
).toBe('builder_ai_query');
|
||||
});
|
||||
|
||||
it('leaves builderQueryType unset when the first query has none', () => {
|
||||
const { result } = renderQueryBuilder(initialQueriesMap.traces);
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
|
||||
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
|
||||
expect(
|
||||
result.current.currentQuery.builder.queryData[1].builderQueryType,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
BuilderQueryType,
|
||||
Filter,
|
||||
Having as HavingV5,
|
||||
LogAggregation,
|
||||
@@ -90,6 +91,7 @@ export type IBuilderQuery = {
|
||||
offset?: number;
|
||||
selectColumns?: BaseAutocompleteData[] | TelemetryFieldKey[];
|
||||
source?: 'meter' | '';
|
||||
builderQueryType?: BuilderQueryType;
|
||||
};
|
||||
|
||||
export interface IClickHouseQuery {
|
||||
|
||||
@@ -16,6 +16,7 @@ export type RequestType =
|
||||
|
||||
export type QueryType =
|
||||
| 'builder_query'
|
||||
| 'builder_ai_query'
|
||||
| 'builder_trace_operator'
|
||||
| 'builder_formula'
|
||||
| 'builder_sub_query'
|
||||
@@ -23,6 +24,11 @@ export type QueryType =
|
||||
| 'clickhouse_sql'
|
||||
| 'promql';
|
||||
|
||||
export type BuilderQueryType = Extract<
|
||||
QueryType,
|
||||
'builder_query' | 'builder_ai_query'
|
||||
>;
|
||||
|
||||
export type OrderDirection = 'asc' | 'desc';
|
||||
|
||||
export type JoinType = 'inner' | 'left' | 'right' | 'full' | 'cross';
|
||||
|
||||
Reference in New Issue
Block a user