mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-08 09:21:01 +00:00
Compare commits
35 Commits
fix/genera
...
feat/ai-qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e0269e2bc | ||
|
|
baa277d434 | ||
|
|
54a6bc216d | ||
|
|
c78bd4366c | ||
|
|
5b269e384b | ||
|
|
54ab158634 | ||
|
|
f78711e42b | ||
|
|
03d0d3a9fb | ||
|
|
084e17f527 | ||
|
|
c19ddba548 | ||
|
|
ccbb3b89a6 | ||
|
|
d6d4ff3df7 | ||
|
|
377e31b014 | ||
|
|
36fb1fc2d0 | ||
|
|
4e4db33bdf | ||
|
|
c92ef6079a | ||
|
|
9b976c81b1 | ||
|
|
cb48993c8d | ||
|
|
07a59a2560 | ||
|
|
c3e0de9eb0 | ||
|
|
c31cb99273 | ||
|
|
5497146f98 | ||
|
|
13625708a0 | ||
|
|
a415902357 | ||
|
|
8d97b8ef74 | ||
|
|
9dda0825bd | ||
|
|
99f9e634eb | ||
|
|
7f1b6bd594 | ||
|
|
98e6975358 | ||
|
|
5b646396a9 | ||
|
|
4ced3b0c13 | ||
|
|
3e20fb09f7 | ||
|
|
dffb844621 | ||
|
|
117183b172 | ||
|
|
6c490be536 |
@@ -903,6 +903,40 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toStrictEqual({ expression: '' });
|
||||
});
|
||||
|
||||
it('sends the AI slot as builder_ai_query envelopes, ignoring builder', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
id: 'q-ai',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery({ queryName: 'M' })],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
builder_ai_query: {
|
||||
queryData: [
|
||||
baseBuilderQuery({ queryName: 'A', dataSource: DataSource.TRACES }),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const { queries } =
|
||||
prepareQueryRangePayloadV5(props).queryPayload.compositeQuery;
|
||||
expect(
|
||||
queries.map((q) => [q.type, (q.spec as { name: string }).name]),
|
||||
).toStrictEqual([['builder_ai_query', 'A']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapPanelTypeToRequestType', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -559,9 +560,15 @@ export const prepareQueryRangePayloadV5 = (
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
switch (query.queryType) {
|
||||
const wireQuery = toWireQuery(query);
|
||||
|
||||
switch (wireQuery.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const {
|
||||
queryData: data,
|
||||
queryFormulas,
|
||||
queryTraceOperator,
|
||||
} = wireQuery.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
@@ -623,13 +630,13 @@ export const prepareQueryRangePayloadV5 = (
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertPromQueriesToV5(promQueries.queries);
|
||||
legendMap = promQueries.legends;
|
||||
break;
|
||||
}
|
||||
case EQueryType.CLICKHOUSE: {
|
||||
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||
legendMap = chQueries.legends;
|
||||
break;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
@@ -49,6 +50,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
handleRunQuery,
|
||||
} = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -129,28 +131,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
const renderedQueries = useMemo(
|
||||
() =>
|
||||
isMultiQueryAllowed
|
||||
? currentQuery.builder.queryData
|
||||
: currentQuery.builder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, currentQuery.builder.queryData],
|
||||
? activeBuilder.queryData
|
||||
: activeBuilder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
return activeBuilder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
}, [activeBuilder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.some(
|
||||
activeBuilder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[currentQuery.builder.queryData],
|
||||
[activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
@@ -225,9 +227,9 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
key={activeBuilder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
query={activeBuilder.queryData[0]}
|
||||
fieldsConfig={fieldsConfig}
|
||||
allowedDataSources={queryDataSources}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
@@ -238,7 +240,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isRawQuery={isRawQuery}
|
||||
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
|
||||
signalSource={activeBuilder.queryData[0].source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
@@ -264,28 +266,26 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
signalSource={query.source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
queriesCount={activeBuilder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
currentQuery.builder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
{!showOnlyWhereClause && activeBuilder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{activeBuilder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
activeBuilder.queryData[index] || activeBuilder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -319,7 +319,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
{activeBuilder.queryFormulas?.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -132,6 +133,7 @@ function QuerySearch({
|
||||
metricNamespace,
|
||||
valueSuggestionsOverride,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const builderQueryType = useBuilderQueryType(queryData);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
const [activeKey, setActiveKey] = useState<string>('');
|
||||
@@ -330,7 +332,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricNamespace,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
);
|
||||
|
||||
if (response.data.keys) {
|
||||
@@ -368,7 +370,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -510,7 +512,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
},
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
).then((response) => {
|
||||
const responseData = response.data;
|
||||
const responseDataValues = responseData.values;
|
||||
@@ -613,7 +615,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
queryData.builderQueryType,
|
||||
builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
|
||||
import { QueryProps } from 'container/QueryBuilder/type';
|
||||
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Copy, Ellipsis, Trash } from '@signozhq/icons';
|
||||
@@ -62,7 +63,8 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const hasQueryFunctions = query?.functions?.length > 0;
|
||||
const { dataSource, builderQueryType } = query;
|
||||
const { dataSource } = query;
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import type {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({
|
||||
QueryV2: ({ query }: { query: IBuilderQuery }): JSX.Element => (
|
||||
<div data-testid="query-v2">{query.queryName}</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
function renderWith(currentQuery: Query): void {
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: null,
|
||||
handleSetConfig: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TIME_SERIES} version="v3" />);
|
||||
}
|
||||
|
||||
const builderWithQuery = (
|
||||
builder: Query['builder'],
|
||||
queryName: string,
|
||||
): Query['builder'] => ({
|
||||
...builder,
|
||||
queryData: [{ ...builder.queryData[0], queryName }],
|
||||
});
|
||||
|
||||
const query: Query = {
|
||||
...initialQueriesMap.metrics,
|
||||
builder: builderWithQuery(initialQueriesMap.metrics.builder, 'MAIN'),
|
||||
builder_ai_query: builderWithQuery(
|
||||
getActiveBuilder(initialQueryAIWithType),
|
||||
'AI',
|
||||
),
|
||||
};
|
||||
|
||||
describe('QueryBuilderV2 builder slot', () => {
|
||||
it('renders the AI slot when AI is the query type', () => {
|
||||
renderWith({ ...query, queryType: EQueryType.AI_QUERY_BUILDER });
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('AI');
|
||||
});
|
||||
|
||||
it('renders builder when Query Builder is the query type', () => {
|
||||
renderWith(query);
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('MAIN');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
@@ -19,7 +19,7 @@ const aiQuery: Query = {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
adjustQueryForV5,
|
||||
convertAggregationToExpression,
|
||||
convertExpressionToFilters,
|
||||
convertFiltersToExpression,
|
||||
@@ -1684,3 +1687,31 @@ describe('formatValueForExpression', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustQueryForV5', () => {
|
||||
it('adjusts the AI builder slot for AI query builder queries', () => {
|
||||
const emptyBuilder = {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
const query: Query = {
|
||||
id: 'q',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: emptyBuilder,
|
||||
builder_ai_query: {
|
||||
...emptyBuilder,
|
||||
queryData: [initialQueryBuilderFormValuesMap.logs],
|
||||
},
|
||||
};
|
||||
|
||||
const result = adjustQueryForV5(query);
|
||||
|
||||
expect(result.builder).toBe(emptyBuilder);
|
||||
expect(result.builder_ai_query?.queryData[0]).not.toHaveProperty(
|
||||
'aggregateOperator',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -9,6 +9,11 @@ import {
|
||||
QUERY_BUILDER_FUNCTIONS,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderSlotKey,
|
||||
isBuilderQueryType,
|
||||
} from 'lib/query/builderSlot';
|
||||
import FilterQueryLexer from 'parser/FilterQueryLexer';
|
||||
import FilterQueryParser, {
|
||||
AndExpressionContext,
|
||||
@@ -33,7 +38,6 @@ import {
|
||||
MetricAggregation,
|
||||
TraceAggregation,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
import { isQuoted, unquote } from 'utils/stringUtils';
|
||||
@@ -1030,8 +1034,9 @@ export function getQueryLabelWithAggregation(
|
||||
}
|
||||
|
||||
export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const newQueryData = currentQuery.builder.queryData.map((query) => {
|
||||
if (isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const newQueryData = activeBuilder.queryData.map((query) => {
|
||||
const aggregations = query.aggregations?.map((aggregation) => {
|
||||
if (query.dataSource === DataSource.METRICS) {
|
||||
const metricAggregation = aggregation as MetricAggregation;
|
||||
@@ -1072,8 +1077,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
[getBuilderSlotKey(currentQuery.queryType)]: {
|
||||
...activeBuilder,
|
||||
queryData: newQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
@@ -556,3 +557,49 @@ describe('clearFilterFromQuery', () => {
|
||||
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quick filters on an AI query', () => {
|
||||
const builderQuery = buildQuery([], '').builder;
|
||||
const aiQuery = {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: builderQuery,
|
||||
builder_ai_query: buildQuery(
|
||||
[{ key: KEY, op: 'in', value: ['a'] }],
|
||||
`${KEY} IN ['a']`,
|
||||
).builder,
|
||||
} as unknown as Query;
|
||||
|
||||
it('a checkbox toggle writes into the AI slot and leaves builder alone', () => {
|
||||
const result = applyCheckboxToggle({
|
||||
currentQuery: aiQuery,
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
attributeValues: ['a', 'b', 'c'],
|
||||
value: 'b',
|
||||
checked: true,
|
||||
isOnlyOrAllClicked: false,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(
|
||||
result.builder_ai_query?.queryData[0].filters?.items.map(
|
||||
(item) => item.value,
|
||||
),
|
||||
).toStrictEqual([['a', 'b']]);
|
||||
});
|
||||
|
||||
it('clearing a filter clears it from the AI slot', () => {
|
||||
const result = clearFilterFromQuery({
|
||||
currentQuery: aiQuery,
|
||||
filterKey: KEY,
|
||||
activeQueryIndex: 0,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(result.builder_ai_query?.queryData[0].filters?.items).toStrictEqual(
|
||||
[],
|
||||
);
|
||||
expect(result.builder_ai_query?.queryData[0].filter?.expression).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,11 @@ import {
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -157,8 +162,9 @@ export function applyCheckboxToggle({
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
}): Query {
|
||||
const activeItems =
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const activeItems = builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
@@ -173,7 +179,7 @@ export function applyCheckboxToggle({
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
const query = cloneDeep(builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
@@ -430,18 +436,15 @@ export function applyCheckboxToggle({
|
||||
query.filters = synced.filters;
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: [
|
||||
...builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from './constants';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -37,12 +38,13 @@ function useCheckboxDisclosure({
|
||||
DEFAULT_VISIBLE_ITEMS_COUNT,
|
||||
);
|
||||
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
!!queryData?.[activeQueryIndex]?.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
[queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual, isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -77,7 +78,7 @@ function useCheckboxFilterActions({
|
||||
dispatch(updatedQuery);
|
||||
|
||||
if (onQuickFilterChange) {
|
||||
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
|
||||
const queryData = getActiveBuilder(updatedQuery).queryData[activeQueryIndex];
|
||||
const expression = queryData?.filter?.expression || '';
|
||||
const filterItemKeys = (queryData?.filters?.items || [])
|
||||
.map((item) => item.key?.key)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { deriveCheckboxState } from './checkboxFilterQuery';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -31,33 +32,28 @@ function useCheckboxFilterState({
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
// derive the state of each filter key here and keep it in sync with current query
|
||||
const currentFilterState = useMemo(
|
||||
() =>
|
||||
deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems:
|
||||
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterItems: activeQueryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterKey: filter.attributeKey.key,
|
||||
}),
|
||||
[
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
],
|
||||
[attributeValues, activeQueryData, filter.attributeKey, activeQueryIndex],
|
||||
);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
(activeQueryData?.[activeQueryIndex]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
[activeQueryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// whether the current filter has multiple values to its name in the key op value section
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -128,18 +129,15 @@ export default function CheckboxFilterV2(
|
||||
onToggleOpen();
|
||||
};
|
||||
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
const currentFilterOp = useMemo(() => {
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items.find((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
const filterSync = activeQueryData?.[activeQueryIndex]?.filters?.items.find(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
return filterSync?.op;
|
||||
}, [
|
||||
currentQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}, [activeQueryData, activeQueryIndex, filter.attributeKey.key]);
|
||||
|
||||
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
|
||||
|
||||
@@ -18,6 +19,9 @@ export function useExistingQuery({
|
||||
activeQueryIndex,
|
||||
}: UseExistingQueryParams): UseExistingQueryResult {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const stagedQueryData = stagedQuery
|
||||
? getActiveBuilder(stagedQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const existingQuery = useMemo(() => {
|
||||
if (useFieldApis.existingQuery === null) {
|
||||
@@ -28,7 +32,7 @@ export function useExistingQuery({
|
||||
return useFieldApis.existingQuery;
|
||||
}
|
||||
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
|
||||
// Prefer V5 filter.expression
|
||||
if (queryData?.filter?.expression) {
|
||||
@@ -41,21 +45,17 @@ export function useExistingQuery({
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [
|
||||
useFieldApis.existingQuery,
|
||||
stagedQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
]);
|
||||
}, [useFieldApis.existingQuery, stagedQueryData, activeQueryIndex]);
|
||||
|
||||
// Check if ANY filters exist in query (V3 items or V5 expression)
|
||||
// This is separate from existingQuery because existingQuery can be explicitly
|
||||
// disabled (null) while filters still exist in the query for UI purposes
|
||||
const hasExistingQuery = useMemo(() => {
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
|
||||
const hasV5Expression = !!queryData?.filter?.expression;
|
||||
return hasV3Items || hasV5Expression || !!existingQuery;
|
||||
}, [stagedQuery?.builder.queryData, activeQueryIndex, existingQuery]);
|
||||
}, [stagedQueryData, activeQueryIndex, existingQuery]);
|
||||
|
||||
return { existingQuery, hasExistingQuery };
|
||||
}
|
||||
|
||||
@@ -9,6 +9,12 @@ import {
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
@@ -57,7 +63,8 @@ function Duration({
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
const filters =
|
||||
compositeQuery?.builder.queryData?.[activeQueryIndex]?.filters;
|
||||
compositeQuery &&
|
||||
getActiveBuilder(compositeQuery).queryData?.[activeQueryIndex]?.filters;
|
||||
if (!filters) {
|
||||
return {} as FilterType;
|
||||
}
|
||||
@@ -183,49 +190,51 @@ function Duration({
|
||||
};
|
||||
|
||||
const removeFilterItemIds = (query: Query): Query => {
|
||||
const clonedQuery = cloneDeep(query);
|
||||
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
}));
|
||||
return clonedQuery;
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
return withBuilderAt(cloneDeep(query), slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
});
|
||||
};
|
||||
|
||||
const handleRun = useCallback(
|
||||
(props?: HandleRunProps): void => {
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
|
||||
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getKeySpellings, isKeyMatch } from '../Checkbox/utils';
|
||||
@@ -20,34 +25,33 @@ export function clearFilterFromQuery({
|
||||
activeQueryIndex: number;
|
||||
operators?: string[];
|
||||
}): Query {
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -32,6 +32,12 @@ import {
|
||||
QuickFilterReadPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/quick-filter.permissions';
|
||||
import { isFunction } from 'lodash-es';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
@@ -87,15 +93,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeBuilder = currentQuery
|
||||
? getActiveBuilder(currentQuery)
|
||||
: undefined;
|
||||
|
||||
// Sync lastUsedQuery when queries change (e.g., after deletion)
|
||||
const validQueryIndex = useMemo(
|
||||
() =>
|
||||
Math.min(
|
||||
lastUsedQuery || 0,
|
||||
(currentQuery?.builder?.queryData?.length || 1) - 1,
|
||||
),
|
||||
[lastUsedQuery, currentQuery?.builder?.queryData?.length],
|
||||
Math.min(lastUsedQuery || 0, (activeBuilder?.queryData?.length || 1) - 1),
|
||||
[lastUsedQuery, activeBuilder?.queryData?.length],
|
||||
);
|
||||
|
||||
// Determine if we're in ListView mode
|
||||
@@ -103,15 +109,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Generate query options based on available queries
|
||||
const queryOptions = useMemo(() => {
|
||||
if (!currentQuery?.builder?.queryData) {
|
||||
if (!activeBuilder?.queryData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return currentQuery.builder.queryData.map((query, index) => ({
|
||||
return activeBuilder.queryData.map((query, index) => ({
|
||||
label: query.queryName || String.fromCharCode(65 + index),
|
||||
value: index,
|
||||
}));
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
}, [activeBuilder?.queryData]);
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
@@ -127,36 +133,35 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
const activeQuery = currentQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const activeQuery = activeBuilder?.queryData?.[activeQueryIndex];
|
||||
|
||||
if (!activeQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(preparedQuery);
|
||||
@@ -166,13 +171,12 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
};
|
||||
|
||||
const lastQueryName =
|
||||
showQueryName &&
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
showQueryName && activeBuilder?.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
// In ListView, always show the 0th query's name; otherwise use the active query's name
|
||||
const displayedQueryName =
|
||||
isListView || isAIObservabilityRowView
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
? showQueryName && activeBuilder?.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
|
||||
const handleQueryChange = (value: number): void => {
|
||||
|
||||
@@ -311,14 +311,10 @@ export const initialQueryMeterWithType: Query = {
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
builder: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
queryData: [initialQueryBuilderFormValuesMap.traces],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,6 +13,11 @@ import {
|
||||
Query,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/** Param shape emitted by the AI for every filter-touching action. */
|
||||
@@ -56,12 +61,11 @@ export function replaceFirstQueryData(
|
||||
query: Query,
|
||||
updated: IBuilderQuery,
|
||||
): Query {
|
||||
const queryData = [...query.builder.queryData];
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
const queryData = [...builder.queryData];
|
||||
queryData[0] = updated;
|
||||
return {
|
||||
...query,
|
||||
builder: { ...query.builder, queryData },
|
||||
};
|
||||
return withBuilderAt(query, slot, { ...builder, queryData });
|
||||
}
|
||||
|
||||
/** Page-state deps required by the runQuery / addFilter actions. */
|
||||
|
||||
@@ -40,6 +40,7 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -218,6 +218,7 @@ function FormAlertRules({
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
@@ -177,7 +178,9 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
const result = (
|
||||
stagedQuery ? getActiveBuilder(stagedQuery) : undefined
|
||||
)?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -35,7 +36,7 @@ function TimeSeriesViewContainer({
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
currentQuery.builder.queryData.forEach(
|
||||
getActiveBuilder(currentQuery).queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
FilterDeps,
|
||||
replaceFirstQueryData,
|
||||
} from 'container/AIAssistant/pageActions/builderQueryHelpers';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
ActionResult,
|
||||
PageAction,
|
||||
@@ -93,7 +94,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ filters }): Promise<ActionResult> => {
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
@@ -117,7 +118,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
getContext: (): Record<string, unknown> => ({
|
||||
filters:
|
||||
deps.currentQuery.builder.queryData[0]?.filters?.items?.map(
|
||||
getActiveBuilder(deps.currentQuery).queryData[0]?.filters?.items?.map(
|
||||
(f: TagFilterItem) => ({
|
||||
key: f.key?.key,
|
||||
op: f.op,
|
||||
@@ -158,7 +159,7 @@ export function tracesAddFilterAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ key, op, value }): Promise<ActionResult> => {
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseListOrderBy } from 'utils/explorerUtils';
|
||||
@@ -15,8 +16,9 @@ export const getListViewQuery = (
|
||||
? [parseListOrderBy(orderBy)]
|
||||
: [];
|
||||
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
const { queryData: builderQueries } = getActiveBuilder(query);
|
||||
for (let i = 0; i < builderQueries.length; i++) {
|
||||
const queryData = builderQueries[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
|
||||
@@ -29,6 +29,7 @@ import { getMetricNameForQuery } from '../utils';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
import { GroupByFilterProps } from './GroupByFilter.interfaces';
|
||||
import { toAttributeKey } from './utils';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export const GroupByFilter = memo(function GroupByFilter({
|
||||
query,
|
||||
@@ -45,6 +46,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -52,7 +54,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedValue,
|
||||
},
|
||||
query.builderQueryType,
|
||||
builderQueryType,
|
||||
{ enabled: !disabled && isFocused, keepPreviousData: true },
|
||||
);
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import { getMetricNameForQuery } from '../utils';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
import { useOrderByFilter } from './useOrderByFilter';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export function OrderByFilter({
|
||||
query,
|
||||
@@ -34,6 +35,7 @@ export function OrderByFilter({
|
||||
handleSearchKeys,
|
||||
} = useOrderByFilter({ query, onChange, entityVersion });
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -41,7 +43,7 @@ export function OrderByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedSearchText,
|
||||
},
|
||||
query.builderQueryType,
|
||||
builderQueryType,
|
||||
{
|
||||
enabled: !!query.aggregateAttribute?.key || isRawQuery,
|
||||
keepPreviousData: true,
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type RefSelectProps, Select, Spin } from 'antd';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { Pill } from '@signozhq/ui/pill';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
@@ -856,14 +857,10 @@ function QueryBuilderSearchV2(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
const isLastQuery = useMemo(() => {
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
return isEqual(queryData[queryData.length - 1], query);
|
||||
}, [currentQuery, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useAppParams } from 'lib/router/useAppParams';
|
||||
import cx from 'classnames';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import Download from 'container/Download/Download';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
@@ -33,7 +34,7 @@ export function QueryTable({
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
const isQueryTypeBuilder = query.queryType === 'builder';
|
||||
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
|
||||
|
||||
const { servicename: encodedServiceName } = useAppParams<'servicename'>();
|
||||
const servicename = decodeURIComponent(encodedServiceName || '');
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import WidgetHeader from 'container/WidgetCard/Header';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryTable } from '../QueryTable';
|
||||
import { QueryTableProps, WidgetHeaderProps } from './mocks';
|
||||
@@ -29,6 +30,21 @@ describe('QueryTable -', () => {
|
||||
const tableRows = container.querySelectorAll('tr.ant-table-row');
|
||||
expect(tableRows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should enable drilldown cells for AI query builder queries', () => {
|
||||
const { container } = render(
|
||||
<QueryTable
|
||||
{...QueryTableProps}
|
||||
query={{
|
||||
...QueryTableProps.query,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: QueryTableProps.query.builder,
|
||||
}}
|
||||
enableDrillDown
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.clickable-cell')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const setSearchTerm = jest.fn();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 (
|
||||
<>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { useBuilderQueryType } from '../useBuilderQueryType';
|
||||
import { useQueryBuilder } from '../useQueryBuilder';
|
||||
|
||||
jest.mock('../useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
const withQueryType = (queryType: EQueryType): void => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: { queryType },
|
||||
});
|
||||
};
|
||||
|
||||
describe('useBuilderQueryType', () => {
|
||||
it('is AI for every query while AI is the query type', () => {
|
||||
withQueryType(EQueryType.AI_QUERY_BUILDER);
|
||||
|
||||
expect(renderHook(() => useBuilderQueryType({})).result.current).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it("follows the query's own tag otherwise", () => {
|
||||
withQueryType(EQueryType.QUERY_BUILDER);
|
||||
|
||||
expect(
|
||||
renderHook(() =>
|
||||
useBuilderQueryType({ builderQueryType: 'builder_ai_query' }),
|
||||
).result.current,
|
||||
).toBe('builder_ai_query');
|
||||
expect(
|
||||
renderHook(() => useBuilderQueryType({})).result.current,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap, initialQueryState } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -105,18 +106,25 @@ describe('useGetCompositeQueryParam', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { QUERY_BUILDER, AI_QUERY_BUILDER } = EQueryType;
|
||||
[
|
||||
[ROUTES.LOGS_EXPLORER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, DataSource.METRICS, ''],
|
||||
[ROUTES.METER_EXPLORER, DataSource.METRICS, TelemetrytypesSourceDTO.meter],
|
||||
].forEach(([route, dataSource, source]) => {
|
||||
[ROUTES.LOGS_EXPLORER, QUERY_BUILDER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, AI_QUERY_BUILDER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, QUERY_BUILDER, DataSource.METRICS, ''],
|
||||
[
|
||||
ROUTES.METER_EXPLORER,
|
||||
QUERY_BUILDER,
|
||||
DataSource.METRICS,
|
||||
TelemetrytypesSourceDTO.meter,
|
||||
],
|
||||
].forEach(([route, queryType, dataSource, source]) => {
|
||||
const query = renderOn(clickhouseWithOperator, route);
|
||||
const builder = query && getActiveBuilder(query);
|
||||
|
||||
expect(query?.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(query?.builder.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(query?.builder.queryData[0].source ?? '').toBe(source);
|
||||
expect(query?.builder.queryTraceOperator).toStrictEqual([]);
|
||||
expect(query?.queryType).toBe(queryType);
|
||||
expect(builder?.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(builder?.queryData[0].source ?? '').toBe(source);
|
||||
expect(builder?.queryTraceOperator).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,16 +1,14 @@
|
||||
import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { BuilderQuerySource, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type ExplorerQueryRules = {
|
||||
dataSource: DataSource;
|
||||
/** `meter` on the meter explorer, which shares the metrics signal; '' elsewhere. */
|
||||
source: BuilderQuerySource;
|
||||
builderQueryType: BuilderQueryType;
|
||||
/** AI_QUERY_BUILDER on the AI explorer, whose queries live in the AI slot. */
|
||||
queryType: EQueryType.QUERY_BUILDER | EQueryType.AI_QUERY_BUILDER;
|
||||
allowsTraceOperator: boolean;
|
||||
};
|
||||
|
||||
@@ -18,31 +16,31 @@ const EXPLORER_QUERY_RULES: Partial<Record<string, ExplorerQueryRules>> = {
|
||||
[ROUTES.LOGS_EXPLORER]: {
|
||||
dataSource: DataSource.LOGS,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.TRACES_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: true,
|
||||
},
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: '',
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: TelemetrytypesSourceDTO.meter,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,11 +8,8 @@ import {
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -44,8 +41,9 @@ function firstBuilderQuery(
|
||||
queryData: unknown[],
|
||||
pathname: string,
|
||||
): IBuilderQuery {
|
||||
return normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname)
|
||||
.query.builder.queryData[0];
|
||||
return getActiveBuilder(
|
||||
normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname).query,
|
||||
).queryData[0];
|
||||
}
|
||||
|
||||
function without(builderQuery: object, key: keyof IBuilderQuery): object {
|
||||
@@ -62,9 +60,11 @@ describe('normalizeCompositeQuery', () => {
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
extra: 'kept',
|
||||
};
|
||||
const aiQuery = withBuilderQueries(initialQueriesMap.traces, [
|
||||
{ ...tracesQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER },
|
||||
]);
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
};
|
||||
|
||||
[NOT_AN_EXPLORER, ROUTES.LOGS_EXPLORER].forEach((pathname) =>
|
||||
expect(
|
||||
@@ -86,6 +86,33 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('moves the AI queries of an older link into the AI slot', () => {
|
||||
const aiQueryData = [
|
||||
{ ...tracesQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER },
|
||||
];
|
||||
const { query, changed } = normalizeCompositeQuery(
|
||||
withBuilderQueries(initialQueriesMap.traces, aiQueryData),
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
);
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(query.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(query.builder_ai_query?.queryData).toStrictEqual([tracesQuery]);
|
||||
expect(query.builder).toStrictEqual(initialQueryState.builder);
|
||||
});
|
||||
|
||||
it('drops the AI slot outside the AI explorer', () => {
|
||||
const { query } = normalizeCompositeQuery(
|
||||
{
|
||||
...initialQueriesMap.traces,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
|
||||
expect(query.builder_ai_query).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds missing blocks and a stable id without changing its inputs', () => {
|
||||
const { builder: _, ...builderless } = {
|
||||
...initialQueriesMap.logs,
|
||||
@@ -209,7 +236,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
logs,
|
||||
[{ ...logsQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER }],
|
||||
[{ ...logsQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER }],
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
).builderQueryType,
|
||||
).toBeUndefined();
|
||||
@@ -220,7 +247,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
{
|
||||
...tracesQuery,
|
||||
queryName: 'D',
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
},
|
||||
],
|
||||
@@ -243,10 +270,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(TelemetrytypesSourceDTO.meter);
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...tracesQuery, ...llmFilter }], ai),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
});
|
||||
).toStrictEqual({ ...tracesQuery, ...llmFilter });
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
traces,
|
||||
@@ -260,17 +284,13 @@ describe('normalizeCompositeQuery', () => {
|
||||
],
|
||||
ai,
|
||||
),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
queryName: 'B',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
).toStrictEqual({ ...tracesQuery, queryName: 'B', ...llmFilter });
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...logsQuery, queryName: 'C' }], ai),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'C',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,8 +7,10 @@ import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import type { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
import {
|
||||
@@ -78,6 +80,14 @@ function addMissingBlocks(query: Query, urlQuery: object): void {
|
||||
if (!query.builder.queryTraceOperator) {
|
||||
query.builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
if (isPlainObject(query.builder_ai_query)) {
|
||||
const aiBuilder = query.builder_ai_query as QueryBuilderData;
|
||||
aiBuilder.queryFormulas = aiBuilder.queryFormulas ?? [];
|
||||
aiBuilder.queryTraceOperator = aiBuilder.queryTraceOperator ?? [];
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,8 +103,21 @@ function isListOfObjects(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
// Explorers only have the query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = EQueryType.QUERY_BUILDER;
|
||||
// Explorers only have a query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = rules.queryType;
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
// Older links carry the AI queries inside `builder`.
|
||||
if (!query.builder_ai_query) {
|
||||
query.builder_ai_query = query.builder;
|
||||
query.builder = cloneDeep(initialQueryState.builder);
|
||||
}
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
const builder = getActiveBuilder(query);
|
||||
if (!isListOfObjects(builder.queryData) || builder.queryData.length === 0) {
|
||||
builder.queryData = cloneDeep(initialQueryState.builder.queryData);
|
||||
}
|
||||
|
||||
if (query.promql.some((promql) => promql.query !== '')) {
|
||||
query.promql = cloneDeep(initialQueryState.promql);
|
||||
@@ -105,10 +128,10 @@ function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
}
|
||||
|
||||
if (!rules.allowsTraceOperator) {
|
||||
query.builder.queryTraceOperator = [];
|
||||
builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
query.builder.queryData = query.builder.queryData.map((builderQuery) =>
|
||||
builder.queryData = builder.queryData.map((builderQuery) =>
|
||||
fitBuilderQueryToExplorer(builderQuery, rules),
|
||||
);
|
||||
}
|
||||
@@ -148,12 +171,9 @@ function fillMissingSignal(
|
||||
builderQuery.source = rules.source;
|
||||
}
|
||||
|
||||
// A missing `builderQueryType` already means builder_query, so only AI needs it written.
|
||||
if (
|
||||
builderQuery.builderQueryType === undefined &&
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
// The AI slot marks its queries as AI; a tag from an older link is redundant.
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
delete builderQuery.builderQueryType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,15 +189,11 @@ function belongsToExplorer(
|
||||
return false;
|
||||
}
|
||||
|
||||
const builderQueryType =
|
||||
builderQuery.builderQueryType ??
|
||||
Querybuildertypesv5QueryTypeDTO.builder_query;
|
||||
|
||||
if (builderQueryType !== rules.builderQueryType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
// Only the AI explorer runs AI queries.
|
||||
return (
|
||||
builderQuery.builderQueryType !==
|
||||
Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
);
|
||||
}
|
||||
|
||||
function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
@@ -186,11 +202,5 @@ function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
);
|
||||
builderQuery.source = rules.source;
|
||||
|
||||
if (
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
}
|
||||
|
||||
return builderQuery;
|
||||
}
|
||||
|
||||
18
frontend/src/hooks/queryBuilder/useBuilderQueryType.ts
Normal file
18
frontend/src/hooks/queryBuilder/useBuilderQueryType.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
/**
|
||||
* The query's builder type, for picking the field suggestion endpoints. Alert editors
|
||||
* keep AI queries in `builder`, so a query's own tag still counts.
|
||||
*/
|
||||
export function useBuilderQueryType(
|
||||
query: Pick<IBuilderQuery, 'builderQueryType'>,
|
||||
): BuilderQueryType | undefined {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
return currentQuery?.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
? 'builder_ai_query'
|
||||
: query.builderQueryType;
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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']]);
|
||||
});
|
||||
});
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
PromQuery,
|
||||
QueryBuilderFormula,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
convertBuilderQueryToIBuilderQuery,
|
||||
convertQueryBuilderFormulaToIBuilderFormula,
|
||||
@@ -37,6 +38,7 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
> = {};
|
||||
const promQueries: IPromQLQuery[] = [];
|
||||
const clickhouseQueries: IClickHouseQuery[] = [];
|
||||
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
|
||||
|
||||
compositeQuery.queries?.forEach((q) => {
|
||||
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
|
||||
@@ -49,10 +51,13 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
}
|
||||
} else if (q.type === 'builder_ai_query') {
|
||||
if (spec.name) {
|
||||
builderQueries[spec.name] = {
|
||||
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
|
||||
builderQueryType: 'builder_ai_query',
|
||||
};
|
||||
const builderQuery = convertBuilderQueryToIBuilderQuery(
|
||||
spec as BuilderQuery,
|
||||
);
|
||||
// Only AI queries kept in `builder` (alerts) need the tag; the AI slot marks its own.
|
||||
builderQueries[spec.name] = isAIQuery
|
||||
? builderQuery
|
||||
: { ...builderQuery, builderQueryType: 'builder_ai_query' };
|
||||
builderQueryTypes[spec.name] = 'builder_ai_query';
|
||||
}
|
||||
} else if (q.type === 'builder_formula') {
|
||||
@@ -87,8 +92,14 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const builder = transformQueryBuilderDataModel(
|
||||
builderQueries,
|
||||
builderQueryTypes,
|
||||
);
|
||||
return {
|
||||
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
|
||||
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
|
||||
...(isAIQuery && { builder_ai_query: builder }),
|
||||
promql: promQueries,
|
||||
clickhouse_sql: clickhouseQueries,
|
||||
queryType: compositeQuery.queryType,
|
||||
|
||||
93
frontend/src/lib/query/__tests__/builderSlot.test.ts
Normal file
93
frontend/src/lib/query/__tests__/builderSlot.test.ts
Normal file
@@ -0,0 +1,93 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderSlotKey,
|
||||
isBuilderQueryType,
|
||||
toWireQuery,
|
||||
} from 'lib/query/builderSlot';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
const baseQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const aiBuilder = getActiveBuilder(initialQueryAIWithType);
|
||||
|
||||
describe('isBuilderQueryType', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, true],
|
||||
[EQueryType.AI_QUERY_BUILDER, true],
|
||||
[EQueryType.CLICKHOUSE, false],
|
||||
[EQueryType.PROM, false],
|
||||
])('%s → %s', (queryType, expected) => {
|
||||
expect(isBuilderQueryType(queryType)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuilderSlotKey', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, 'builder'],
|
||||
[EQueryType.CLICKHOUSE, 'builder'],
|
||||
[EQueryType.PROM, 'builder'],
|
||||
[EQueryType.AI_QUERY_BUILDER, 'builder_ai_query'],
|
||||
])('maps %s to %s', (queryType, slot) => {
|
||||
expect(getBuilderSlotKey(queryType)).toBe(slot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveBuilder', () => {
|
||||
it('reads builder for the Query Builder type', () => {
|
||||
const query: Query = { ...baseQuery, builder_ai_query: aiBuilder };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
|
||||
it('reads the AI slot for the AI type', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: aiBuilder,
|
||||
};
|
||||
expect(getActiveBuilder(query)).toBe(aiBuilder);
|
||||
});
|
||||
|
||||
it('reads an unused AI slot as holding no queries', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.AI_QUERY_BUILDER };
|
||||
expect(getActiveBuilder(query)).toStrictEqual({
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps builder for ClickHouse and PromQL', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.PROM };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toWireQuery', () => {
|
||||
it('passes non-AI queries through untouched', () => {
|
||||
expect(toWireQuery(baseQuery)).toBe(baseQuery);
|
||||
});
|
||||
|
||||
it('sends AI as a builder query holding the AI slot, every query tagged', () => {
|
||||
const untagged = {
|
||||
...aiBuilder.queryData[0],
|
||||
builderQueryType: undefined,
|
||||
};
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: { ...aiBuilder, queryData: [untagged] },
|
||||
};
|
||||
|
||||
const wire = toWireQuery(query);
|
||||
|
||||
expect(wire.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(wire.builder.queryData).toStrictEqual([
|
||||
{ ...untagged, builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
73
frontend/src/lib/query/builderSlot.ts
Normal file
73
frontend/src/lib/query/builderSlot.ts
Normal 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));
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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> = {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { useDashboardVariableNames } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardVariableNames';
|
||||
|
||||
import type { VariableItem } from './types';
|
||||
@@ -20,13 +21,16 @@ const GLOBAL_TIMESTAMP_VARIABLES: VariableItem[] = [
|
||||
*/
|
||||
export function useContextLinkVariables(): VariableItem[] {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const dashboardVariableNames = useDashboardVariableNames();
|
||||
|
||||
// `_`-prefixed to match V1 and avoid colliding with dashboard-variable names.
|
||||
const fieldVariableNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
(currentQuery?.builder?.queryData ?? []).forEach((query) => {
|
||||
(activeQueryData ?? []).forEach((query) => {
|
||||
(query.groupBy ?? []).forEach((field) => {
|
||||
if (field.key) {
|
||||
names.add(`_${field.key}`);
|
||||
@@ -34,7 +38,7 @@ export function useContextLinkVariables(): VariableItem[] {
|
||||
});
|
||||
});
|
||||
return Array.from(names);
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
}, [activeQueryData]);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
|
||||
@@ -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' },
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
@@ -23,22 +24,20 @@ import { useSyncQueryBuilderFields } from 'components/QueryBuilderV2/useSyncQuer
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getQueryModeSignals,
|
||||
getQueryTypeSignals,
|
||||
isRawRequest,
|
||||
} from '../../Panels/types/panelCapabilities';
|
||||
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
|
||||
import { toPanelType } from '../../Panels/types/panelKind';
|
||||
import { getQueryMode, isBuilderMode } from 'lib/query/queryMode';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
|
||||
import { useQueryTypeChange } from './useQueryTypeChange';
|
||||
import styles from './PanelEditorQueryBuilder.module.scss';
|
||||
|
||||
interface PanelEditorQueryBuilderProps {
|
||||
/** The edited kind's definition — drives supported query types, the signals the
|
||||
* builder may be pointed at, and how it narrows the builder's fields. */
|
||||
panelDefinition: RenderableQueryPanelDefinition;
|
||||
/** Switch authoring tab; owned by the host, which remembers the Query Builder query. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
/** Preview fetch in flight — drives the Stage & Run button's loading/cancel state. */
|
||||
isLoadingQueries: boolean;
|
||||
/** Run the current query (Stage & Run button / ⌘↵). Always re-runs. */
|
||||
@@ -58,7 +57,6 @@ interface PanelEditorQueryBuilderProps {
|
||||
*/
|
||||
function PanelEditorQueryBuilder({
|
||||
panelDefinition,
|
||||
onChangeQueryMode,
|
||||
isLoadingQueries,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
@@ -68,6 +66,10 @@ function PanelEditorQueryBuilder({
|
||||
// The shared QueryBuilderV2 provider still speaks the legacy PANEL_TYPES; what the
|
||||
// builder offers for this kind comes from the kind's own declaration.
|
||||
const panelType = toPanelType(panelDefinition.kind);
|
||||
const onChangeQueryType = useQueryTypeChange({
|
||||
panelType,
|
||||
supportedQueryTypesKindMap: panelDefinition.supportedQueryTypesKindMap,
|
||||
});
|
||||
// Raw rows: the builder drops its aggregation controls, and with them the trace
|
||||
// operator that combines aggregated trace queries (V1 parity).
|
||||
const isRawQuery = isRawRequest(panelDefinition.queryCapabilities);
|
||||
@@ -99,10 +101,10 @@ function PanelEditorQueryBuilder({
|
||||
|
||||
const items = useMemo(() => {
|
||||
const queryTypeComponents: Record<
|
||||
QueryMode,
|
||||
EQueryType,
|
||||
{ icon: ReactNode; label: string; component: ReactNode }
|
||||
> = {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
@@ -110,9 +112,9 @@ function PanelEditorQueryBuilder({
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
fieldsConfig={panelDefinition.queryBuilderFields}
|
||||
allowedDataSources={getQueryModeSignals(
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
QueryMode.QUERY_BUILDER,
|
||||
allowedDataSources={getQueryTypeSignals(
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
EQueryType.QUERY_BUILDER,
|
||||
)}
|
||||
showTraceOperator={!isRawQuery}
|
||||
version="v3"
|
||||
@@ -123,12 +125,12 @@ function PanelEditorQueryBuilder({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[QueryMode.CLICKHOUSE]: {
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
icon: <Terminal size={14} />,
|
||||
label: 'ClickHouse Query',
|
||||
component: <ClickHouseQueryContainer />,
|
||||
},
|
||||
[QueryMode.PROM]: {
|
||||
[EQueryType.PROM]: {
|
||||
icon: (
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
@@ -137,7 +139,7 @@ function PanelEditorQueryBuilder({
|
||||
label: 'PromQL',
|
||||
component: <PromQLQueryContainer />,
|
||||
},
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
icon: <Brain size={14} />,
|
||||
label: 'AI Query Builder',
|
||||
component: (
|
||||
@@ -160,8 +162,8 @@ function PanelEditorQueryBuilder({
|
||||
};
|
||||
|
||||
const modes = Object.keys(
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
) as QueryMode[];
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
) as EQueryType[];
|
||||
|
||||
return modes.map((queryType) => ({
|
||||
key: queryType,
|
||||
@@ -176,7 +178,7 @@ function PanelEditorQueryBuilder({
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
// QB wants a null data source and AI pins traces: both mounted, they overwrite each other's provider config in a loop.
|
||||
destroyInactiveTabPane: isBuilderMode(queryType),
|
||||
destroyInactiveTabPane: isBuilderQueryType(queryType),
|
||||
}));
|
||||
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
|
||||
|
||||
@@ -193,8 +195,8 @@ function PanelEditorQueryBuilder({
|
||||
className={cx(styles.tabsContainer, {
|
||||
[styles.stickyNav]: stickyHeader,
|
||||
})}
|
||||
activeKey={getQueryMode(currentQuery)}
|
||||
onChange={onChangeQueryMode}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={onChangeQueryType}
|
||||
tabBarExtraContent={
|
||||
<span className={styles.runQueryBtnContainer}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -1,17 +1,11 @@
|
||||
import { useRef } from 'react';
|
||||
import { fireEvent, render, screen } from '@testing-library/react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import {
|
||||
toPanelType,
|
||||
type PanelKind,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder';
|
||||
import { useQueryModeChange } from '../../hooks/useQueryModeChange';
|
||||
|
||||
// Capture the props the (real-guard-fed) QueryBuilderV2 receives without rendering it.
|
||||
const mockQueryBuilderV2 = jest.fn();
|
||||
@@ -57,25 +51,19 @@ const BUILDER_QUERY = {
|
||||
builder: { queryData: [{ dataSource: 'traces' }] },
|
||||
};
|
||||
const AI_QUERY = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: { queryData: [] },
|
||||
builder_ai_query: {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
},
|
||||
};
|
||||
|
||||
/** Stands in for the editor shell: owns the tab switch and its parked Query Builder query. */
|
||||
/** Stands in for the editor shell. */
|
||||
function BuilderHost({ panelKind }: { panelKind: PanelKind }): JSX.Element {
|
||||
const panelDefinition = requireQueryPanelDefinition(panelKind);
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: toPanelType(panelKind),
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={jest.fn()}
|
||||
onCancelQuery={jest.fn()}
|
||||
@@ -184,8 +172,12 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
'traces',
|
||||
);
|
||||
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
|
||||
expect(seeded.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(seeded.builder_ai_query.queryData[0]).toMatchObject({
|
||||
dataSource: 'traces',
|
||||
});
|
||||
expect(seeded.builder_ai_query.queryData[0].builderQueryType).toBeUndefined();
|
||||
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
|
||||
});
|
||||
|
||||
it('keeps the builder query when returning from ClickHouse', () => {
|
||||
@@ -203,7 +195,7 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
expect(onClickHouse.builder).toBe(logsQuery.builder);
|
||||
});
|
||||
|
||||
it('shows a fresh default query when leaving AI', () => {
|
||||
it('seeds a default query for the kind when leaving AI for an unused Query Builder', () => {
|
||||
mockBuilder(AI_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
@@ -215,6 +207,19 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
expect(next.builder.queryData[0].dataSource).toBe('metrics');
|
||||
});
|
||||
|
||||
it("reseeds a Query Builder query this kind can't author when leaving AI", () => {
|
||||
mockBuilder({
|
||||
...AI_QUERY,
|
||||
builder: { queryData: [{ dataSource: 'metrics' }] },
|
||||
});
|
||||
renderBuilder('signoz/ListPanel');
|
||||
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
|
||||
const [next] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(next.builder.queryData[0].dataSource).toBe('logs');
|
||||
});
|
||||
|
||||
it("keeps each tab's own query across Query Builder ↔ AI round trips", () => {
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
@@ -232,17 +237,20 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
const editedAIBuilder = {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
};
|
||||
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
|
||||
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
const [backToQB] = redirectWithQueryBuilderData.mock.calls[1];
|
||||
expect(backToQB.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(backToQB.builder).toBe(logsQuery.builder);
|
||||
expect(backToQB.builder_ai_query).toBe(editedAIBuilder);
|
||||
|
||||
mockBuilder(backToQB);
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
|
||||
expect(backToAI.builder).toBe(editedAIBuilder);
|
||||
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -72,8 +72,6 @@ interface QueryEditorBodyProps {
|
||||
/** Kind switch, owned by the shell (its cache must survive the fork swap). */
|
||||
onChangePanelKind: (kind: PanelKind) => void;
|
||||
originalPanelKind?: PanelKind;
|
||||
/** Tab switch, owned by the shell alongside the kind switch it shares memory with. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -95,7 +93,6 @@ function QueryEditorBody({
|
||||
panelDefinition,
|
||||
onChangePanelKind,
|
||||
originalPanelKind,
|
||||
onChangeQueryMode,
|
||||
}: QueryEditorBodyProps): JSX.Element {
|
||||
// Read here rather than taken as props: this renders inside a loaded dashboard
|
||||
// subtree, so it resolves the same context every other consumer does.
|
||||
@@ -312,7 +309,6 @@ function QueryEditorBody({
|
||||
panelDefinition={panelDefinition}
|
||||
signal={listSignal}
|
||||
isLoadingQueries={isFetching}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={runQuery}
|
||||
onCancelQuery={cancelQuery}
|
||||
spec={spec}
|
||||
|
||||
@@ -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', () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { useRef, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
@@ -22,8 +22,6 @@ import { usePanelEditorQuerySync } from '../hooks/usePanelEditorQuerySync';
|
||||
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
|
||||
import { useQueryModeChange } from '../hooks/useQueryModeChange';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
@@ -81,17 +79,10 @@ function EditorRoute({ panelId }: { panelId: string }): JSX.Element {
|
||||
savedQueries: panel.spec.queries,
|
||||
});
|
||||
const panelDefinition = requireQueryPanelDefinition('signoz/TimeSeriesPanel');
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={noop}
|
||||
onCancelQuery={noop}
|
||||
|
||||
@@ -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 } },
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getIsQueryModified } from 'lib/query/panelQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -111,9 +112,10 @@ export function usePanelEditorQuerySync({
|
||||
|
||||
// Re-commit on a query-type/datasource switch so the preview refetches. Skip
|
||||
// mount: the initial query is synced into the draft by the staged-query effect below.
|
||||
const activeQueryData = getActiveBuilder(currentQuery).queryData;
|
||||
const dataSources = useMemo(
|
||||
() => (currentQuery.builder?.queryData ?? []).map((q) => q.dataSource),
|
||||
[currentQuery.builder],
|
||||
() => (activeQueryData ?? []).map((q) => q.dataSource),
|
||||
[activeQueryData],
|
||||
);
|
||||
const dataSourceSignature = dataSources.join(',');
|
||||
const prevDataSourcesRef = useRef(dataSources);
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { UNSUPPORTED_PANEL } from '../kinds/UnsupportedPanel/definition';
|
||||
import { getPanelDefinition, isPanelKindSupported } from '../registry';
|
||||
@@ -12,13 +11,12 @@ import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
|
||||
import {
|
||||
getQueryPanelDefinition,
|
||||
requireQueryPanelDefinition,
|
||||
getSupportedQueryModes,
|
||||
getSupportedQueryTypes,
|
||||
getSupportedSignals,
|
||||
isPanelCombinationValid,
|
||||
isQueryTypeSupportedByPanelKind,
|
||||
isSignalSupported,
|
||||
resolveQueryMode,
|
||||
resolveQueryType,
|
||||
} from '../capabilities';
|
||||
import type { PanelKind } from '../types/panelKind';
|
||||
|
||||
@@ -27,59 +25,44 @@ const { logs, traces, metrics } = TelemetrytypesSignalDTO;
|
||||
const { time_series, scalar, raw, heatmap } = Querybuildertypesv5RequestTypeDTO;
|
||||
|
||||
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
|
||||
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/AreaChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/ListPanel': [QUERY_BUILDER],
|
||||
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
// Static kind: no query surface at all.
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
|
||||
const EXPECTED_QUERY_MODES: Record<PanelKind, QueryMode[]> = {
|
||||
'signoz/TimeSeriesPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/BarChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/AreaChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/NumberPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HistogramPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
@@ -281,10 +264,10 @@ describe('panel capabilities guard', () => {
|
||||
|
||||
it('reads a specific mode when one is given', () => {
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toStrictEqual([traces]);
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.CLICKHOUSE),
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.CLICKHOUSE),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -335,36 +318,21 @@ describe('panel capabilities guard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedQueryModes', () => {
|
||||
it.each(Object.entries(EXPECTED_QUERY_MODES))(
|
||||
'%s supports %j',
|
||||
(kind, modes) => {
|
||||
expect(getSupportedQueryModes(kind as PanelKind)).toStrictEqual(modes);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps AI out of the query types', () => {
|
||||
expect(getSupportedQueryTypes('signoz/TimeSeriesPanel')).not.toContain(
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveQueryMode', () => {
|
||||
describe('resolveQueryType', () => {
|
||||
it('keeps a supported mode', () => {
|
||||
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(
|
||||
resolveQueryMode('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.AI_QUERY_BUILDER);
|
||||
resolveQueryType('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(
|
||||
resolveQueryMode('signoz/ListPanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.AI_QUERY_BUILDER);
|
||||
resolveQueryType('signoz/ListPanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
});
|
||||
|
||||
it('coerces an unsupported mode to the first supported one', () => {
|
||||
// PromQL → List has no PromQL, falls back to its first (and only) type.
|
||||
expect(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryMode('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ChartScatter } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { MAX_PLOTTED_GROUPS } from './utils/prepareData';
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
Querybuildertypesv5RequestTypeDTO,
|
||||
TelemetrytypesSignalDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { QueryBuilderField } from 'components/QueryBuilderV2/queryBuilderFields.types';
|
||||
|
||||
export const definition: PanelDefinition<'signoz/ScatterPlotPanel'> = {
|
||||
@@ -23,16 +23,16 @@ export const definition: PanelDefinition<'signoz/ScatterPlotPanel'> = {
|
||||
Renderer,
|
||||
EditorPane: QueryBuilderEditorPane,
|
||||
sections,
|
||||
supportedQueryModesKindMap: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
supportedQueryTypesKindMap: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
signals: [
|
||||
TelemetrytypesSignalDTO.metrics,
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
},
|
||||
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
},
|
||||
queryBuilderFields: {
|
||||
// Pinned so it opens empty: the group-by template a click would seed names nothing here.
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -25,7 +25,7 @@ export const UNSUPPORTED_PANEL: RenderablePanelDefinition = {
|
||||
Renderer,
|
||||
EditorPane: QueryBuilderEditorPane,
|
||||
sections: [],
|
||||
supportedQueryModesKindMap: {},
|
||||
supportedQueryTypesKindMap: {},
|
||||
queryBuilderFields: {},
|
||||
queryCapabilities: {
|
||||
requestType: Querybuildertypesv5RequestTypeDTO.time_series,
|
||||
|
||||
@@ -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 : [];
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ import type { SyncedQueryBuilderField } from 'components/QueryBuilderV2/queryBui
|
||||
import type {
|
||||
PanelQueryCapabilities,
|
||||
QueryBuilderFieldsConfig,
|
||||
SupportedQueryModesKindMap,
|
||||
SupportedQueryTypesKindMap,
|
||||
} from './panelCapabilities';
|
||||
import type {
|
||||
BaseRendererProps,
|
||||
@@ -99,8 +99,6 @@ export interface QueryEditorPaneProps {
|
||||
panelDefinition: RenderableQueryPanelDefinition;
|
||||
signal: TelemetrytypesSignalDTO;
|
||||
isLoadingQueries: boolean;
|
||||
/** Switch authoring tab (Query Builder / AI / ClickHouse / PromQL), owned by the host. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
onStageRunQuery: () => void;
|
||||
onCancelQuery: () => void;
|
||||
/** Pin the tabs row to the pane top; the View modal opts out. */
|
||||
@@ -122,7 +120,7 @@ export interface QueryPanelDefinition<
|
||||
/** Lower editor pane — the shared query-builder pane, or a kind wrapper of it. */
|
||||
EditorPane: ComponentType<QueryEditorPaneProps>;
|
||||
/** Authoring modes (Query Builder / ClickHouse / PromQL / AI) and the signals each accepts. */
|
||||
supportedQueryModesKindMap: SupportedQueryModesKindMap;
|
||||
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
|
||||
queryBuilderFields: QueryBuilderFieldsConfig;
|
||||
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
|
||||
queryCapabilities: PanelQueryCapabilities;
|
||||
|
||||
@@ -4,15 +4,16 @@ import {
|
||||
initialQueryAIWithType,
|
||||
type PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
DataSource,
|
||||
type QueryBuilderContextType,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
interface SeedBuilderForModeArgs {
|
||||
mode: QueryMode;
|
||||
mode: EQueryType;
|
||||
/** The kind's first Query Builder signal; what a fresh panel of this kind starts on. */
|
||||
defaultSignal?: TelemetrytypesSignalDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
@@ -26,12 +27,14 @@ export function seedBuilderForMode({
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
}: SeedBuilderForModeArgs): Query['builder'] {
|
||||
if (mode === QueryMode.AI_QUERY_BUILDER) {
|
||||
return updateAllQueriesOperators(
|
||||
initialQueryAIWithType,
|
||||
panelType,
|
||||
DataSource.TRACES,
|
||||
).builder;
|
||||
if (mode === EQueryType.AI_QUERY_BUILDER) {
|
||||
return getActiveBuilder(
|
||||
updateAllQueriesOperators(
|
||||
initialQueryAIWithType,
|
||||
panelType,
|
||||
DataSource.TRACES,
|
||||
),
|
||||
);
|
||||
}
|
||||
const dataSource = (defaultSignal ??
|
||||
TelemetrytypesSignalDTO.metrics) as unknown as DataSource;
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
}),
|
||||
|
||||
@@ -157,6 +157,12 @@ describe('useDrilldown', () => {
|
||||
expect(result.current.enableDrillDown).toBe(true);
|
||||
});
|
||||
|
||||
it('is true for an AI query', () => {
|
||||
mockGetPanelQueryType.mockReturnValue(EQueryType.AI_QUERY_BUILDER);
|
||||
const { result } = renderHook(() => useDrilldown(tsPanel, 'p1'));
|
||||
expect(result.current.enableDrillDown).toBe(true);
|
||||
});
|
||||
|
||||
it('is false when there is no builder query', () => {
|
||||
mockGetBuilderQueries.mockReturnValue([]);
|
||||
const { result } = renderHook(() => useDrilldown(tsPanel, 'p1'));
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user