mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-27 03:20:41 +00:00
Compare commits
9 Commits
test/uplot
...
feat/ai-qu
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9909d8e222 | ||
|
|
31bde4d502 | ||
|
|
f9330ddf82 | ||
|
|
0ba63f0757 | ||
|
|
731a169962 | ||
|
|
7e66be7152 | ||
|
|
c61da39761 | ||
|
|
feda4133a1 | ||
|
|
7b0ce0b448 |
@@ -902,6 +902,40 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toStrictEqual({ expression: '' });
|
||||
});
|
||||
|
||||
it('sends the AI slot as builder_ai_query envelopes, ignoring builder', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
id: 'q-ai',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery({ queryName: 'M' })],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
builder_ai_query: {
|
||||
queryData: [
|
||||
baseBuilderQuery({ queryName: 'A', dataSource: DataSource.TRACES }),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const { queries } =
|
||||
prepareQueryRangePayloadV5(props).queryPayload.compositeQuery;
|
||||
expect(
|
||||
queries.map((q) => [q.type, (q.spec as { name: string }).name]),
|
||||
).toStrictEqual([['builder_ai_query', 'A']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertBuilderQueriesToV5 having normalization', () => {
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { GetQueryResultsProps } from 'lib/dashboard/getQueryResults';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import getStartEndRangeTime from 'lib/getStartEndRangeTime';
|
||||
import { mapQueryDataToApi } from 'lib/newQueryBuilder/queryBuilderMappers/mapQueryDataToApi';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
@@ -557,9 +558,15 @@ export const prepareQueryRangePayloadV5 = (
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
switch (query.queryType) {
|
||||
const wireQuery = toWireQuery(query);
|
||||
|
||||
switch (wireQuery.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const {
|
||||
queryData: data,
|
||||
queryFormulas,
|
||||
queryTraceOperator,
|
||||
} = wireQuery.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
@@ -621,13 +628,13 @@ export const prepareQueryRangePayloadV5 = (
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertPromQueriesToV5(promQueries.queries);
|
||||
legendMap = promQueries.legends;
|
||||
break;
|
||||
}
|
||||
case EQueryType.CLICKHOUSE: {
|
||||
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||
legendMap = chQueries.legends;
|
||||
break;
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getBuilderVariant } from 'lib/query/queryMode';
|
||||
import {
|
||||
BuilderVariant,
|
||||
IBuilderTraceOperator,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { IBuilderTraceOperator } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderField } from './queryBuilderFields.types';
|
||||
@@ -54,6 +50,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
handleRunQuery,
|
||||
} = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -64,20 +61,8 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
[config],
|
||||
);
|
||||
|
||||
const isAIQuery =
|
||||
getBuilderVariant(currentQuery.builder) === BuilderVariant.AI;
|
||||
|
||||
useEffect(() => {
|
||||
if (currentDataSource !== initialDataSource || newPanelType !== panelType) {
|
||||
// AI queries are traces-only; BAR must not force metrics on them.
|
||||
if (isAIQuery) {
|
||||
handleSetConfig(newPanelType, DataSource.TRACES);
|
||||
return;
|
||||
}
|
||||
if (newPanelType === PANEL_TYPES.BAR) {
|
||||
handleSetConfig(PANEL_TYPES.BAR, DataSource.METRICS);
|
||||
return;
|
||||
}
|
||||
handleSetConfig(newPanelType, currentDataSource);
|
||||
}
|
||||
}, [
|
||||
@@ -86,7 +71,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
currentDataSource,
|
||||
newPanelType,
|
||||
isAIQuery,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -139,28 +123,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
const renderedQueries = useMemo(
|
||||
() =>
|
||||
isMultiQueryAllowed
|
||||
? currentQuery.builder.queryData
|
||||
: currentQuery.builder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, currentQuery.builder.queryData],
|
||||
? activeBuilder.queryData
|
||||
: activeBuilder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
return activeBuilder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
}, [activeBuilder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData.some(
|
||||
activeBuilder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[currentQuery.builder.queryData],
|
||||
[activeBuilder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
@@ -234,9 +218,9 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
key={activeBuilder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
query={activeBuilder.queryData[0]}
|
||||
fieldsConfig={fieldsConfig}
|
||||
allowedDataSources={queryDataSources}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
@@ -247,7 +231,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isRawQuery={isRawQuery}
|
||||
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
|
||||
signalSource={activeBuilder.queryData[0].source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
@@ -273,28 +257,26 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
signalSource={query.source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
queriesCount={activeBuilder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause &&
|
||||
currentQuery.builder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{currentQuery.builder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
currentQuery.builder.queryData[index] ||
|
||||
currentQuery.builder.queryData[0];
|
||||
{!showOnlyWhereClause && activeBuilder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{activeBuilder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
activeBuilder.queryData[index] || activeBuilder.queryData[0];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -328,7 +310,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
{activeBuilder.queryFormulas?.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,67 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import type {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({
|
||||
QueryV2: ({ query }: { query: IBuilderQuery }): JSX.Element => (
|
||||
<div data-testid="query-v2">{query.queryName}</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
function renderWith(currentQuery: Query): void {
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: null,
|
||||
handleSetConfig: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TIME_SERIES} version="v3" />);
|
||||
}
|
||||
|
||||
const builderWithQuery = (
|
||||
builder: Query['builder'],
|
||||
queryName: string,
|
||||
): Query['builder'] => ({
|
||||
...builder,
|
||||
queryData: [{ ...builder.queryData[0], queryName }],
|
||||
});
|
||||
|
||||
const query: Query = {
|
||||
...initialQueriesMap.metrics,
|
||||
builder: builderWithQuery(initialQueriesMap.metrics.builder, 'MAIN'),
|
||||
builder_ai_query: builderWithQuery(initialQueryAIWithType.builder, 'AI'),
|
||||
};
|
||||
|
||||
describe('QueryBuilderV2 builder slot', () => {
|
||||
it('renders the AI slot when AI is the query type', () => {
|
||||
renderWith({ ...query, queryType: EQueryType.AI_QUERY_BUILDER });
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('AI');
|
||||
});
|
||||
|
||||
it('renders builder when Query Builder is the query type', () => {
|
||||
renderWith(query);
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('MAIN');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,109 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({ QueryV2: (): null => null }));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
builder: {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
function mockContext({
|
||||
panelType = null,
|
||||
initialDataSource = null,
|
||||
currentQuery = initialQueriesMap.metrics,
|
||||
}: {
|
||||
panelType?: PANEL_TYPES | null;
|
||||
initialDataSource?: DataSource | null;
|
||||
currentQuery?: Query;
|
||||
}): jest.Mock {
|
||||
const handleSetConfig = jest.fn();
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType,
|
||||
initialDataSource,
|
||||
handleSetConfig,
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
return handleSetConfig;
|
||||
}
|
||||
|
||||
describe('QueryBuilderV2 provider config sync', () => {
|
||||
it('leaves the data source unset for a bar panel without a static config', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.BAR} version="v3" />);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(PANEL_TYPES.BAR, null);
|
||||
});
|
||||
|
||||
it('syncs the static data source', () => {
|
||||
const handleSetConfig = mockContext({});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.LOGS, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.LOGS,
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps an AI query on traces for a bar panel', () => {
|
||||
const handleSetConfig = mockContext({ currentQuery: aiQuery });
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.BAR}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).toHaveBeenCalledWith(
|
||||
PANEL_TYPES.BAR,
|
||||
DataSource.TRACES,
|
||||
);
|
||||
});
|
||||
|
||||
it('does not reset the config when the provider already matches', () => {
|
||||
const handleSetConfig = mockContext({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: DataSource.TRACES,
|
||||
});
|
||||
|
||||
render(
|
||||
<QueryBuilderV2
|
||||
version="v3"
|
||||
panelType={PANEL_TYPES.TIME_SERIES}
|
||||
config={{ initialDataSource: DataSource.TRACES, queryVariant: 'static' }}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(handleSetConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,13 +1,16 @@
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
adjustQueryForV5,
|
||||
convertAggregationToExpression,
|
||||
convertExpressionToFilters,
|
||||
convertFiltersToExpression,
|
||||
@@ -1632,3 +1635,31 @@ describe('formatValueForExpression', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('adjustQueryForV5', () => {
|
||||
it('adjusts the AI builder slot for AI query builder queries', () => {
|
||||
const emptyBuilder = {
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
};
|
||||
const query: Query = {
|
||||
id: 'q',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: emptyBuilder,
|
||||
builder_ai_query: {
|
||||
...emptyBuilder,
|
||||
queryData: [initialQueryBuilderFormValuesMap.logs],
|
||||
},
|
||||
};
|
||||
|
||||
const result = adjustQueryForV5(query);
|
||||
|
||||
expect(result.builder).toBe(emptyBuilder);
|
||||
expect(result.builder_ai_query?.queryData[0]).not.toHaveProperty(
|
||||
'aggregateOperator',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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';
|
||||
@@ -1013,8 +1017,9 @@ export function getQueryLabelWithAggregation(
|
||||
}
|
||||
|
||||
export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const newQueryData = currentQuery.builder.queryData.map((query) => {
|
||||
if (isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const newQueryData = activeBuilder.queryData.map((query) => {
|
||||
const aggregations = query.aggregations?.map((aggregation) => {
|
||||
if (query.dataSource === DataSource.METRICS) {
|
||||
const metricAggregation = aggregation as MetricAggregation;
|
||||
@@ -1055,8 +1060,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
[getBuilderSlotKey(currentQuery.queryType)]: {
|
||||
...activeBuilder,
|
||||
queryData: newQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -38,6 +38,7 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -220,6 +220,7 @@ function FormAlertRules({
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { Select, Spin } from 'antd';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { Pill } from '@signozhq/ui/pill';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
@@ -857,14 +858,10 @@ function QueryBuilderSearchV2(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
const isLastQuery = useMemo(() => {
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
return isEqual(queryData[queryData.length - 1], query);
|
||||
}, [currentQuery, query]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -4,6 +4,7 @@ import cx from 'classnames';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import Download from 'container/Download/Download';
|
||||
import { IServiceName } from 'container/MetricsApplication/Tabs/types';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
@@ -34,7 +35,7 @@ export function QueryTable({
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
const isQueryTypeBuilder = query.queryType === 'builder';
|
||||
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
|
||||
|
||||
const { servicename: encodedServiceName } = useParams<IServiceName>();
|
||||
const servicename = decodeURIComponent(encodedServiceName);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import WidgetHeader from 'container/WidgetCard/Header';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryTable } from '../QueryTable';
|
||||
import { QueryTableProps, WidgetHeaderProps } from './mocks';
|
||||
@@ -33,6 +34,21 @@ describe('QueryTable -', () => {
|
||||
const tableRows = container.querySelectorAll('tr.ant-table-row');
|
||||
expect(tableRows).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should enable drilldown cells for AI query builder queries', () => {
|
||||
const { container } = render(
|
||||
<QueryTable
|
||||
{...QueryTableProps}
|
||||
query={{
|
||||
...QueryTableProps.query,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: QueryTableProps.query.builder,
|
||||
}}
|
||||
enableDrillDown
|
||||
/>,
|
||||
);
|
||||
expect(container.querySelector('.clickable-cell')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
const setSearchTerm = jest.fn();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -28,12 +28,15 @@ export const useGetCompositeQueryParam = (): Query | null => {
|
||||
);
|
||||
|
||||
// Add default values for optional fields if empty
|
||||
if (parsedCompositeQuery?.builder) {
|
||||
parsedCompositeQuery.builder.queryFormulas =
|
||||
parsedCompositeQuery.builder.queryFormulas ?? [];
|
||||
parsedCompositeQuery.builder.queryTraceOperator =
|
||||
parsedCompositeQuery.builder.queryTraceOperator ?? [];
|
||||
}
|
||||
[
|
||||
parsedCompositeQuery?.builder,
|
||||
parsedCompositeQuery?.builder_ai_query,
|
||||
].forEach((builder) => {
|
||||
if (builder) {
|
||||
builder.queryFormulas = builder.queryFormulas ?? [];
|
||||
builder.queryTraceOperator = builder.queryTraceOperator ?? [];
|
||||
}
|
||||
});
|
||||
|
||||
// Convert old format to new format for each query in builder.queryData
|
||||
if (parsedCompositeQuery?.builder?.queryData) {
|
||||
|
||||
@@ -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,
|
||||
@@ -87,8 +88,16 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const builder = transformQueryBuilderDataModel(
|
||||
builderQueries,
|
||||
builderQueryTypes,
|
||||
);
|
||||
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
|
||||
|
||||
return {
|
||||
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
|
||||
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
|
||||
...(isAIQuery && { builder_ai_query: builder }),
|
||||
promql: promQueries,
|
||||
clickhouse_sql: clickhouseQueries,
|
||||
queryType: compositeQuery.queryType,
|
||||
|
||||
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 = initialQueryAIWithType.builder;
|
||||
|
||||
describe('isBuilderQueryType', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, true],
|
||||
[EQueryType.AI_QUERY_BUILDER, true],
|
||||
[EQueryType.CLICKHOUSE, false],
|
||||
[EQueryType.PROM, false],
|
||||
])('%s → %s', (queryType, expected) => {
|
||||
expect(isBuilderQueryType(queryType)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuilderSlotKey', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, 'builder'],
|
||||
[EQueryType.CLICKHOUSE, 'builder'],
|
||||
[EQueryType.PROM, 'builder'],
|
||||
[EQueryType.AI_QUERY_BUILDER, 'builder_ai_query'],
|
||||
])('maps %s to %s', (queryType, slot) => {
|
||||
expect(getBuilderSlotKey(queryType)).toBe(slot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveBuilder', () => {
|
||||
it('reads builder for the Query Builder type', () => {
|
||||
const query: Query = { ...baseQuery, builder_ai_query: aiBuilder };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
|
||||
it('reads the AI slot for the AI type', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: aiBuilder,
|
||||
};
|
||||
expect(getActiveBuilder(query)).toBe(aiBuilder);
|
||||
});
|
||||
|
||||
it('reads an unused AI slot as holding no queries', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.AI_QUERY_BUILDER };
|
||||
expect(getActiveBuilder(query)).toStrictEqual({
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps builder for ClickHouse and PromQL', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.PROM };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toWireQuery', () => {
|
||||
it('passes non-AI queries through untouched', () => {
|
||||
expect(toWireQuery(baseQuery)).toBe(baseQuery);
|
||||
});
|
||||
|
||||
it('sends AI as a builder query holding the AI slot, every query tagged', () => {
|
||||
const untagged = {
|
||||
...aiBuilder.queryData[0],
|
||||
builderQueryType: undefined,
|
||||
};
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: { ...aiBuilder, queryData: [untagged] },
|
||||
};
|
||||
|
||||
const wire = toWireQuery(query);
|
||||
|
||||
expect(wire.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(wire.builder.queryData).toStrictEqual([
|
||||
{ ...untagged, builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
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> = {
|
||||
|
||||
@@ -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,
|
||||
@@ -173,7 +175,7 @@ function PanelEditorQueryBuilder({
|
||||
),
|
||||
children: queryTypeComponents[queryType].component,
|
||||
// QB wants a null data source and AI pins traces: both mounted, they overwrite each other's provider config in a loop.
|
||||
destroyInactiveTabPane: isBuilderMode(queryType),
|
||||
destroyInactiveTabPane: isBuilderQueryType(queryType),
|
||||
}));
|
||||
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
|
||||
|
||||
@@ -190,8 +192,8 @@ function PanelEditorQueryBuilder({
|
||||
className={cx(styles.tabsContainer, {
|
||||
[styles.stickyNav]: stickyHeader,
|
||||
})}
|
||||
activeKey={getQueryMode(currentQuery)}
|
||||
onChange={onChangeQueryMode}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={onChangeQueryType}
|
||||
tabBarExtraContent={
|
||||
<span className={styles.runQueryBtnContainer}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
|
||||
@@ -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,11 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
'traces',
|
||||
);
|
||||
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
|
||||
expect(seeded.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(seeded.builder_ai_query.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
|
||||
});
|
||||
|
||||
it('keeps the builder query when returning from ClickHouse', () => {
|
||||
@@ -203,7 +194,7 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
expect(onClickHouse.builder).toBe(logsQuery.builder);
|
||||
});
|
||||
|
||||
it('shows a fresh default query when leaving AI', () => {
|
||||
it('seeds a default query for the kind when leaving AI for an unused Query Builder', () => {
|
||||
mockBuilder(AI_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
@@ -215,6 +206,19 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
expect(next.builder.queryData[0].dataSource).toBe('metrics');
|
||||
});
|
||||
|
||||
it("reseeds a Query Builder query this kind can't author when leaving AI", () => {
|
||||
mockBuilder({
|
||||
...AI_QUERY,
|
||||
builder: { queryData: [{ dataSource: 'metrics' }] },
|
||||
});
|
||||
renderBuilder('signoz/ListPanel');
|
||||
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
|
||||
const [next] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(next.builder.queryData[0].dataSource).toBe('logs');
|
||||
});
|
||||
|
||||
it("keeps each tab's own query across Query Builder ↔ AI round trips", () => {
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
@@ -232,17 +236,20 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
const editedAIBuilder = {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
};
|
||||
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
|
||||
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
const [backToQB] = redirectWithQueryBuilderData.mock.calls[1];
|
||||
expect(backToQB.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(backToQB.builder).toBe(logsQuery.builder);
|
||||
expect(backToQB.builder_ai_query).toBe(editedAIBuilder);
|
||||
|
||||
mockBuilder(backToQB);
|
||||
rerender(host());
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
|
||||
expect(backToAI.builder).toBe(editedAIBuilder);
|
||||
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -71,8 +71,6 @@ interface QueryEditorBodyProps {
|
||||
/** Kind switch, owned by the shell (its cache must survive the fork swap). */
|
||||
onChangePanelKind: (kind: PanelKind) => void;
|
||||
originalPanelKind?: PanelKind;
|
||||
/** Tab switch, owned by the shell alongside the kind switch it shares memory with. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,7 +92,6 @@ function QueryEditorBody({
|
||||
panelDefinition,
|
||||
onChangePanelKind,
|
||||
originalPanelKind,
|
||||
onChangeQueryMode,
|
||||
}: QueryEditorBodyProps): JSX.Element {
|
||||
// Read here rather than taken as props: this renders inside a loaded dashboard
|
||||
// subtree, so it resolves the same context every other consumer does.
|
||||
@@ -310,7 +307,6 @@ function QueryEditorBody({
|
||||
panelDefinition={panelDefinition}
|
||||
signal={listSignal}
|
||||
isLoadingQueries={isFetching}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={runQuery}
|
||||
onCancelQuery={cancelQuery}
|
||||
spec={spec}
|
||||
|
||||
@@ -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';
|
||||
@@ -20,8 +20,6 @@ import { usePanelEditorQuerySync } from '../hooks/usePanelEditorQuerySync';
|
||||
import { requireQueryPanelDefinition } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
|
||||
import PanelEditorQueryBuilder from '../PanelEditorQueryBuilder/PanelEditorQueryBuilder';
|
||||
import { useQueryModeChange } from '../hooks/useQueryModeChange';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
// jest.config maps the real hook to a no-op mock; this suite needs real navigation.
|
||||
jest.mock('hooks/useSafeNavigate', () => {
|
||||
@@ -84,17 +82,10 @@ function EditorRoute(): JSX.Element {
|
||||
savedQueries: panel.spec.queries,
|
||||
});
|
||||
const panelDefinition = requireQueryPanelDefinition('signoz/TimeSeriesPanel');
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={noop}
|
||||
onCancelQuery={noop}
|
||||
|
||||
@@ -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 } },
|
||||
);
|
||||
|
||||
@@ -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,58 +25,44 @@ const { logs, traces, metrics } = TelemetrytypesSignalDTO;
|
||||
const { time_series, scalar, raw, heatmap } = Querybuildertypesv5RequestTypeDTO;
|
||||
|
||||
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
|
||||
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/AreaChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/ListPanel': [QUERY_BUILDER],
|
||||
// Static kind: no query surface at all.
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
|
||||
const EXPECTED_QUERY_MODES: Record<PanelKind, QueryMode[]> = {
|
||||
'signoz/TimeSeriesPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/BarChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/AreaChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/NumberPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HistogramPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
|
||||
@@ -270,10 +254,10 @@ describe('panel capabilities guard', () => {
|
||||
|
||||
it('reads a specific mode when one is given', () => {
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toStrictEqual([traces]);
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.CLICKHOUSE),
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.CLICKHOUSE),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -324,36 +308,21 @@ describe('panel capabilities guard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSupportedQueryModes', () => {
|
||||
it.each(Object.entries(EXPECTED_QUERY_MODES))(
|
||||
'%s supports %j',
|
||||
(kind, modes) => {
|
||||
expect(getSupportedQueryModes(kind as PanelKind)).toStrictEqual(modes);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps AI out of the query types', () => {
|
||||
expect(getSupportedQueryTypes('signoz/TimeSeriesPanel')).not.toContain(
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveQueryMode', () => {
|
||||
describe('resolveQueryType', () => {
|
||||
it('keeps a supported mode', () => {
|
||||
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(
|
||||
resolveQueryMode('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.AI_QUERY_BUILDER);
|
||||
resolveQueryType('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(
|
||||
resolveQueryMode('signoz/ListPanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.AI_QUERY_BUILDER);
|
||||
resolveQueryType('signoz/ListPanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
});
|
||||
|
||||
it('coerces an unsupported mode to the first supported one', () => {
|
||||
// PromQL → List has no PromQL, falls back to its first (and only) type.
|
||||
expect(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryMode('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,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 : [];
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ import type { SyncedQueryBuilderField } from 'components/QueryBuilderV2/queryBui
|
||||
import type {
|
||||
PanelQueryCapabilities,
|
||||
QueryBuilderFieldsConfig,
|
||||
SupportedQueryModesKindMap,
|
||||
SupportedQueryTypesKindMap,
|
||||
} from './panelCapabilities';
|
||||
import type {
|
||||
BaseRendererProps,
|
||||
@@ -95,8 +95,6 @@ export interface QueryEditorPaneProps {
|
||||
panelDefinition: RenderableQueryPanelDefinition;
|
||||
signal: TelemetrytypesSignalDTO;
|
||||
isLoadingQueries: boolean;
|
||||
/** Switch authoring tab (Query Builder / AI / ClickHouse / PromQL), owned by the host. */
|
||||
onChangeQueryMode: (key: string) => void;
|
||||
onStageRunQuery: () => void;
|
||||
onCancelQuery: () => void;
|
||||
/** Pin the tabs row to the pane top; the View modal opts out. */
|
||||
@@ -118,7 +116,7 @@ export interface QueryPanelDefinition<
|
||||
/** Lower editor pane — the shared query-builder pane, or a kind wrapper of it. */
|
||||
EditorPane: ComponentType<QueryEditorPaneProps>;
|
||||
/** Authoring modes (Query Builder / ClickHouse / PromQL / AI) and the signals each accepts. */
|
||||
supportedQueryModesKindMap: SupportedQueryModesKindMap;
|
||||
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
|
||||
queryBuilderFields: QueryBuilderFieldsConfig;
|
||||
/** How this kind's query-range request is shaped (request type, paging, result formatting). */
|
||||
queryCapabilities: PanelQueryCapabilities;
|
||||
|
||||
@@ -5,14 +5,14 @@ import {
|
||||
type PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
DataSource,
|
||||
type QueryBuilderContextType,
|
||||
} from 'types/common/queryBuilder';
|
||||
|
||||
interface SeedBuilderForModeArgs {
|
||||
mode: QueryMode;
|
||||
mode: EQueryType;
|
||||
/** The kind's first Query Builder signal; what a fresh panel of this kind starts on. */
|
||||
defaultSignal?: TelemetrytypesSignalDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
@@ -26,7 +26,7 @@ export function seedBuilderForMode({
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
}: SeedBuilderForModeArgs): Query['builder'] {
|
||||
if (mode === QueryMode.AI_QUERY_BUILDER) {
|
||||
if (mode === EQueryType.AI_QUERY_BUILDER) {
|
||||
return updateAllQueriesOperators(
|
||||
initialQueryAIWithType,
|
||||
panelType,
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -12,6 +12,7 @@ import { toPanelType } from 'pages/DashboardPage/DashboardContainer/Panels/types
|
||||
import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getPanelQueryType';
|
||||
import { useDashboardEventMeta } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardEventMeta';
|
||||
import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/queryV5/buildQueryRangeRequest';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
|
||||
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
|
||||
@@ -81,9 +82,8 @@ export function useCreateAlertFromPanel(): (
|
||||
{ data: request },
|
||||
{
|
||||
onSuccess: (response) => {
|
||||
const query = envelopesToQuery(
|
||||
response.data.compositeQuery?.queries ?? [],
|
||||
panelType,
|
||||
const query = toWireQuery(
|
||||
envelopesToQuery(response.data.compositeQuery?.queries ?? [], panelType),
|
||||
);
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
const url = buildAlertUrl(
|
||||
|
||||
@@ -22,7 +22,7 @@ import { getPanelQueryType } from 'pages/DashboardPage/DashboardContainer/Panels
|
||||
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { dynamicSignalFromQuerySignal } from 'pages/DashboardPage/DashboardContainer/Panels/utils/drilldown/signal';
|
||||
import { DashboardDetailEvents } from 'pages/DashboardPage/constants/events';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { isBuilderQueryType, toWireQuery } from 'lib/query/builderSlot';
|
||||
|
||||
import DrilldownAggregateMenu from '../DrilldownMenu/DrilldownAggregateMenu';
|
||||
import DrilldownBreakoutMenu from '../DrilldownMenu/DrilldownBreakoutMenu';
|
||||
@@ -87,13 +87,13 @@ export function useDrilldown(
|
||||
const enableDrillDown = useMemo(
|
||||
() =>
|
||||
getPanelDefinition(kind).actions.drilldown &&
|
||||
getPanelQueryType(panel) === EQueryType.QUERY_BUILDER &&
|
||||
isBuilderQueryType(getPanelQueryType(panel)) &&
|
||||
getBuilderQueries(queries).length > 0,
|
||||
[kind, panel, queries],
|
||||
);
|
||||
|
||||
const v1Query = useMemo(
|
||||
() => fromPerses(queries, panelType),
|
||||
() => toWireQuery(fromPerses(queries, panelType)),
|
||||
[queries, panelType],
|
||||
);
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useSelector } from 'react-redux';
|
||||
import { useReplaceVariables } from 'api/generated/services/querier';
|
||||
import type { DashboardtypesQueryDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { buildQueryRangeRequest } from 'pages/DashboardPage/DashboardContainer/queryV5/buildQueryRangeRequest';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import { envelopesToQuery } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import { selectResolvedVariables } from 'pages/DashboardPage/DashboardContainer/store/slices/variableSelectionSlice';
|
||||
import { useDashboardStore } from 'pages/DashboardPage/DashboardContainer/store/useDashboardStore';
|
||||
@@ -90,9 +91,11 @@ export function useResolvedDrilldownQuery({
|
||||
}
|
||||
// View-in-X navigates to a V1 explorer, so the resolved query crosses back into the
|
||||
// V1 `Query` shape — the one place this hook still needs a legacy panel type.
|
||||
return envelopesToQuery(
|
||||
data.data.compositeQuery?.queries ?? [],
|
||||
toPanelType(panelKind),
|
||||
return toWireQuery(
|
||||
envelopesToQuery(
|
||||
data.data.compositeQuery?.queries ?? [],
|
||||
toPanelType(panelKind),
|
||||
),
|
||||
);
|
||||
}, [hasVariables, data, v1Query, panelKind]);
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
type PanelFormattingSlice,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/sections';
|
||||
import { getSectionControls } from 'pages/DashboardPage/DashboardContainer/Panels/utils/getSectionControls';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import { fromPerses } from 'pages/DashboardPage/DashboardContainer/queryV5/persesQueryAdapters';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -74,7 +75,7 @@ export function buildAlertUrl(
|
||||
*/
|
||||
export function buildCreateAlertUrl(panel: DashboardtypesPanelDTO): string {
|
||||
const panelType = toPanelType(panel.spec.plugin.kind);
|
||||
const query = fromPerses(panel.spec.queries, panelType);
|
||||
const query = toWireQuery(fromPerses(panel.spec.queries, panelType));
|
||||
const unit = readPanelUnit(panel.spec.plugin);
|
||||
return buildAlertUrl(
|
||||
query,
|
||||
|
||||
@@ -87,6 +87,19 @@ describe('persesQueryAdapters', () => {
|
||||
EQueryType.CLICKHOUSE,
|
||||
);
|
||||
});
|
||||
|
||||
it('opens an AI panel on the AI query type with its queries in the AI slot', () => {
|
||||
const restored = fromPerses(
|
||||
toPerses(initialQueryAIWithType, PANEL_TYPES.LIST),
|
||||
PANEL_TYPES.LIST,
|
||||
);
|
||||
|
||||
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(restored.builder.queryData).toStrictEqual([]);
|
||||
expect(restored.builder_ai_query?.queryData[0].dataSource).toBe(
|
||||
DataSource.TRACES,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('envelopesToQuery', () => {
|
||||
@@ -232,13 +245,41 @@ describe('persesQueryAdapters', () => {
|
||||
expect(result[0].spec.plugin.kind).toBe('signoz/AIBuilderQuery');
|
||||
});
|
||||
|
||||
it('emits signoz/AIBuilderQuery for an AI-typed List query, from its own slot', () => {
|
||||
const query: Query = {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: initialQueryAIWithType.builder,
|
||||
};
|
||||
|
||||
const result = toPerses(query, PANEL_TYPES.LIST);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].spec.plugin.kind).toBe('signoz/AIBuilderQuery');
|
||||
});
|
||||
|
||||
it('emits only the active slot, never a parked AI query', () => {
|
||||
const query: Query = {
|
||||
...initialQueriesMap[DataSource.METRICS],
|
||||
builder_ai_query: initialQueryAIWithType.builder,
|
||||
};
|
||||
|
||||
const result = toPerses(query, PANEL_TYPES.TIME_SERIES);
|
||||
const envelopes = (
|
||||
result[0].spec.plugin.spec as { queries?: { type?: string }[] }
|
||||
).queries;
|
||||
|
||||
expect(envelopes?.map((e) => e.type)).toStrictEqual(['builder_query']);
|
||||
});
|
||||
|
||||
it('preserves an AI List query through toPerses → fromPerses', () => {
|
||||
const restored = fromPerses(
|
||||
toPerses(initialQueryAIWithType, PANEL_TYPES.LIST),
|
||||
PANEL_TYPES.LIST,
|
||||
);
|
||||
|
||||
expect(restored.builder.queryData[0].builderQueryType).toBe(
|
||||
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
@@ -251,7 +292,8 @@ describe('persesQueryAdapters', () => {
|
||||
savedAgain[0].spec.plugin.spec as { queries?: { type?: string }[] }
|
||||
).queries;
|
||||
|
||||
expect(restored.builder.queryData[0].builderQueryType).toBe(
|
||||
expect(restored.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(restored.builder_ai_query?.queryData[0].builderQueryType).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
expect(envelopes?.[0]?.type).toBe('builder_ai_query');
|
||||
|
||||
@@ -85,6 +85,9 @@ export function deriveQueryType(
|
||||
) {
|
||||
return EQueryType.CLICKHOUSE;
|
||||
}
|
||||
if (envelopes.some(isAIBuilderEnvelope)) {
|
||||
return EQueryType.AI_QUERY_BUILDER;
|
||||
}
|
||||
return EQueryType.QUERY_BUILDER;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
initialClickHouseData,
|
||||
initialFormulaBuilderFormValues,
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
initialQueryBuilderFormTraceOperatorValues,
|
||||
initialQueryBuilderFormValuesMap,
|
||||
initialQueryPromQLData,
|
||||
@@ -35,11 +36,15 @@ import {
|
||||
} from 'lib/query/panelTypeDataSourceFormValuesMap';
|
||||
import { OptionsQuery } from 'container/OptionsMenu/types';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { updateStepInterval } from 'hooks/queryBuilder/useStepInterval';
|
||||
import { useSafeNavigate } from 'hooks/useSafeNavigate';
|
||||
import { createIdFromObjectFields } from 'lib/createIdFromObjectFields';
|
||||
import { createNewBuilderItemName } from 'lib/newQueryBuilder/createNewBuilderItemName';
|
||||
import { getOperatorsBySourceAndPanelType } from 'lib/newQueryBuilder/getOperatorsBySourceAndPanelType';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { saveRecentQuery } from 'lib/recentQueries/saveRecentQuery';
|
||||
import { replaceIncorrectObjectFields } from 'lib/replaceIncorrectObjectFields';
|
||||
import { cloneDeep, get, isEqual, set } from 'lodash-es';
|
||||
@@ -136,6 +141,7 @@ export function QueryBuilderProvider({
|
||||
const [stagedQuery, setStagedQuery] = useState<Query | null>(null);
|
||||
|
||||
const [queryType, setQueryType] = useState<EQueryType>(queryTypeParam);
|
||||
const builderSlot = getBuilderSlotKey(queryType);
|
||||
|
||||
const getElementWithActualOperator = useCallback(
|
||||
(
|
||||
@@ -163,62 +169,73 @@ export function QueryBuilderProvider({
|
||||
|
||||
const prepareQueryBuilderData = useCallback(
|
||||
(query: Query): Query => {
|
||||
const builder: QueryBuilderData = {
|
||||
queryData:
|
||||
query.builder.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryFormulas:
|
||||
query.builder.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryTraceOperator:
|
||||
query.builder.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
const urlPanelType = readPanelTypeFromUrl(PANEL_TYPES.LIST);
|
||||
|
||||
const isRawPanel =
|
||||
urlPanelType === PANEL_TYPES.LIST || urlPanelType === PANEL_TYPES.TRACE;
|
||||
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
const currentElement: IBuilderQuery = {
|
||||
...item,
|
||||
groupBy: item.groupBy.map(({ id: _, ...item }) => ({
|
||||
...item,
|
||||
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||
})),
|
||||
aggregateAttribute: {
|
||||
...item.aggregateAttribute,
|
||||
id: createIdFromObjectFields(
|
||||
item.aggregateAttribute as BaseAutocompleteData,
|
||||
baseAutoCompleteIdKeysOrder,
|
||||
),
|
||||
} as BaseAutocompleteData,
|
||||
const prepareBuilder = (source: QueryBuilderData): QueryBuilderData => {
|
||||
const builder: QueryBuilderData = {
|
||||
queryData:
|
||||
source.queryData?.map((item) => ({
|
||||
...initialQueryBuilderFormValuesMap[
|
||||
initialDataSource || DataSource.METRICS
|
||||
],
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryFormulas:
|
||||
source.queryFormulas?.map((item) => ({
|
||||
...initialFormulaBuilderFormValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
queryTraceOperator:
|
||||
source.queryTraceOperator?.map((item) => ({
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
...item,
|
||||
})) ?? [],
|
||||
};
|
||||
|
||||
// Explorer pages: sanitize stale orderBy before first query
|
||||
const isExplorer =
|
||||
(location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER ||
|
||||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER) &&
|
||||
!isRawPanel;
|
||||
if (isExplorer) {
|
||||
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
|
||||
return calledFromHandleRunQuery
|
||||
? currentElement
|
||||
: { ...currentElement, orderBy: sanitizedOrderBy };
|
||||
}
|
||||
const setupedQueryData = builder.queryData.map((item) => {
|
||||
const currentElement: IBuilderQuery = {
|
||||
...item,
|
||||
groupBy: item.groupBy.map(({ id: _, ...item }) => ({
|
||||
...item,
|
||||
id: createIdFromObjectFields(item, baseAutoCompleteIdKeysOrder),
|
||||
})),
|
||||
aggregateAttribute: {
|
||||
...item.aggregateAttribute,
|
||||
id: createIdFromObjectFields(
|
||||
item.aggregateAttribute as BaseAutocompleteData,
|
||||
baseAutoCompleteIdKeysOrder,
|
||||
),
|
||||
} as BaseAutocompleteData,
|
||||
};
|
||||
|
||||
return currentElement;
|
||||
});
|
||||
// Explorer pages: sanitize stale orderBy before first query
|
||||
const isExplorer =
|
||||
(location.pathname === ROUTES.LOGS_EXPLORER ||
|
||||
location.pathname === ROUTES.TRACES_EXPLORER ||
|
||||
location.pathname === ROUTES.AI_OBSERVABILITY_EXPLORER) &&
|
||||
!isRawPanel;
|
||||
if (isExplorer) {
|
||||
const sanitizedOrderBy = sanitizeOrderByForExplorer(currentElement);
|
||||
return calledFromHandleRunQuery
|
||||
? currentElement
|
||||
: { ...currentElement, orderBy: sanitizedOrderBy };
|
||||
}
|
||||
|
||||
return currentElement;
|
||||
});
|
||||
|
||||
return { ...builder, queryData: setupedQueryData };
|
||||
};
|
||||
|
||||
// Filled the first time AI is the active type, then kept so a tab switch can return to it.
|
||||
const aiBuilder =
|
||||
query.builder_ai_query ??
|
||||
(query.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
? initialQueryAIWithType.builder
|
||||
: undefined);
|
||||
|
||||
const promql: IPromQLQuery[] =
|
||||
query.promql?.map((item) => ({
|
||||
@@ -235,10 +252,8 @@ export function QueryBuilderProvider({
|
||||
const newQueryState: QueryState = {
|
||||
clickhouse_sql: clickHouse,
|
||||
promql,
|
||||
builder: {
|
||||
...builder,
|
||||
queryData: setupedQueryData,
|
||||
},
|
||||
builder: prepareBuilder(query.builder),
|
||||
...(aiBuilder && { builder_ai_query: prepareBuilder(aiBuilder) }),
|
||||
id: query.id,
|
||||
unit: query.unit,
|
||||
};
|
||||
@@ -283,20 +298,19 @@ export function QueryBuilderProvider({
|
||||
dataSource: DataSource,
|
||||
signalSource?: 'meter' | '',
|
||||
): Query => {
|
||||
const queryData = query.builder.queryData?.map((item) =>
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
const queryData = builder.queryData?.map((item) =>
|
||||
getElementWithActualOperator(item, dataSource, panelType),
|
||||
);
|
||||
|
||||
return {
|
||||
...query,
|
||||
builder: {
|
||||
...query.builder,
|
||||
queryData: queryData.map((item) => ({
|
||||
...item,
|
||||
source: signalSource,
|
||||
})),
|
||||
},
|
||||
};
|
||||
return withBuilderAt(query, slot, {
|
||||
...builder,
|
||||
queryData: queryData.map((item) => ({
|
||||
...item,
|
||||
source: signalSource,
|
||||
})),
|
||||
});
|
||||
},
|
||||
|
||||
[getElementWithActualOperator],
|
||||
@@ -393,9 +407,11 @@ export function QueryBuilderProvider({
|
||||
index: number,
|
||||
) => QueryBuilderData[T][number],
|
||||
): Query => {
|
||||
const result = query.builder[type].map(updateCallback);
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
const result = builder[type].map(updateCallback);
|
||||
|
||||
return { ...query, builder: { ...query.builder, [type]: result } };
|
||||
return withBuilderAt(query, slot, { ...builder, [type]: result });
|
||||
},
|
||||
[],
|
||||
);
|
||||
@@ -407,17 +423,14 @@ export function QueryBuilderProvider({
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IBuilderTraceOperator
|
||||
)[] = prevState.builder[type];
|
||||
)[] = getBuilderAt(prevState, builderSlot)[type];
|
||||
|
||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
[type]: filteredArray,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
[type]: filteredArray,
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
@@ -425,34 +438,35 @@ export function QueryBuilderProvider({
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
| IBuilderTraceOperator
|
||||
)[] = prevState.builder[type];
|
||||
)[] = getBuilderAt(prevState, builderSlot)[type];
|
||||
|
||||
const filteredArray = currentArray.filter((_, i) => index !== i);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
[type]: filteredArray,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
[type]: filteredArray,
|
||||
});
|
||||
});
|
||||
},
|
||||
[],
|
||||
[builderSlot],
|
||||
);
|
||||
|
||||
const removeAllQueryBuilderEntities = useCallback(
|
||||
(type: keyof QueryBuilderData) => {
|
||||
setCurrentQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: { ...prevState.builder, [type]: [] },
|
||||
}));
|
||||
setSupersetQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: { ...prevState.builder, [type]: [] },
|
||||
}));
|
||||
setCurrentQuery((prevState) =>
|
||||
withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
[type]: [],
|
||||
}),
|
||||
);
|
||||
setSupersetQuery((prevState) =>
|
||||
withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
[type]: [],
|
||||
}),
|
||||
);
|
||||
},
|
||||
[setCurrentQuery, setSupersetQuery],
|
||||
[builderSlot, setCurrentQuery, setSupersetQuery],
|
||||
);
|
||||
|
||||
const removeQueryTypeItemByIndex = useCallback(
|
||||
@@ -582,177 +596,180 @@ export function QueryBuilderProvider({
|
||||
|
||||
const addNewBuilderQuery = useCallback(() => {
|
||||
setCurrentQuery((prevState) => {
|
||||
if (prevState.builder.queryData.length >= MAX_QUERIES) {
|
||||
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const newQuery = createNewBuilderQuery(prevState.builder.queryData);
|
||||
const newQuery = createNewBuilderQuery(
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: [...prevState.builder.queryData, newQuery],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: [...getBuilderAt(prevState, builderSlot).queryData, newQuery],
|
||||
});
|
||||
});
|
||||
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
if (prevState.builder.queryData.length >= MAX_QUERIES) {
|
||||
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const newQuery = createNewBuilderQuery(prevState.builder.queryData);
|
||||
const newQuery = createNewBuilderQuery(
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: [...prevState.builder.queryData, newQuery],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: [...getBuilderAt(prevState, builderSlot).queryData, newQuery],
|
||||
});
|
||||
});
|
||||
}, [createNewBuilderQuery]);
|
||||
}, [builderSlot, createNewBuilderQuery]);
|
||||
|
||||
const cloneQuery = useCallback(
|
||||
(type: string, query: IBuilderQuery): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
if (prevState.builder.queryData.length >= MAX_QUERIES) {
|
||||
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const clonedQuery = cloneNewBuilderQuery(
|
||||
prevState.builder.queryData,
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
query,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: [...prevState.builder.queryData, clonedQuery],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: [
|
||||
...getBuilderAt(prevState, builderSlot).queryData,
|
||||
clonedQuery,
|
||||
],
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
if (prevState.builder.queryData.length >= MAX_QUERIES) {
|
||||
if (getBuilderAt(prevState, builderSlot).queryData.length >= MAX_QUERIES) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const clonedQuery = cloneNewBuilderQuery(
|
||||
prevState.builder.queryData,
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
query,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: [...prevState.builder.queryData, clonedQuery],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: [
|
||||
...getBuilderAt(prevState, builderSlot).queryData,
|
||||
clonedQuery,
|
||||
],
|
||||
});
|
||||
});
|
||||
},
|
||||
[cloneNewBuilderQuery],
|
||||
[builderSlot, cloneNewBuilderQuery],
|
||||
);
|
||||
|
||||
const addNewFormula = useCallback(() => {
|
||||
setCurrentQuery((prevState) => {
|
||||
if (prevState.builder.queryFormulas.length >= MAX_FORMULAS) {
|
||||
if (
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas.length >= MAX_FORMULAS
|
||||
) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const newFormula = createNewBuilderFormula(prevState.builder.queryFormulas);
|
||||
const newFormula = createNewBuilderFormula(
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryFormulas: [...prevState.builder.queryFormulas, newFormula],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryFormulas: [
|
||||
...getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
newFormula,
|
||||
],
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
if (prevState.builder.queryFormulas.length >= MAX_FORMULAS) {
|
||||
if (
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas.length >= MAX_FORMULAS
|
||||
) {
|
||||
return prevState;
|
||||
}
|
||||
|
||||
const newFormula = createNewBuilderFormula(prevState.builder.queryFormulas);
|
||||
const newFormula = createNewBuilderFormula(
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryFormulas: [...prevState.builder.queryFormulas, newFormula],
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryFormulas: [
|
||||
...getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
newFormula,
|
||||
],
|
||||
});
|
||||
});
|
||||
}, [createNewBuilderFormula]);
|
||||
}, [builderSlot, createNewBuilderFormula]);
|
||||
|
||||
const addTraceOperator = useCallback((expression = '') => {
|
||||
const trimmed = (expression || '').trim();
|
||||
const addTraceOperator = useCallback(
|
||||
(expression = '') => {
|
||||
const trimmed = (expression || '').trim();
|
||||
|
||||
setCurrentQuery((prevState) => {
|
||||
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
setCurrentQuery((prevState) => {
|
||||
const existing =
|
||||
getBuilderAt(prevState, builderSlot).queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
// enforce single trace operator and replace only expression
|
||||
queryTraceOperator: [updated],
|
||||
},
|
||||
};
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const existing = prevState.builder.queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const existing =
|
||||
getBuilderAt(prevState, builderSlot).queryTraceOperator?.[0] || null;
|
||||
const updated: IBuilderTraceOperator = existing
|
||||
? { ...existing, expression: trimmed }
|
||||
: {
|
||||
...initialQueryBuilderFormTraceOperatorValues,
|
||||
queryName: TRACE_OPERATOR_QUERY_NAME,
|
||||
expression: trimmed,
|
||||
};
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
// enforce single trace operator and replace only expression
|
||||
queryTraceOperator: [updated],
|
||||
},
|
||||
};
|
||||
});
|
||||
}, []);
|
||||
});
|
||||
});
|
||||
},
|
||||
[builderSlot],
|
||||
);
|
||||
|
||||
const removeTraceOperator = useCallback(() => {
|
||||
setCurrentQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
setCurrentQuery((prevState) =>
|
||||
withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
}));
|
||||
}),
|
||||
);
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => ({
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
setSupersetQuery((prevState) =>
|
||||
withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
}));
|
||||
}, []);
|
||||
}),
|
||||
);
|
||||
}, [builderSlot]);
|
||||
|
||||
const updateQueryBuilderData: <T>(
|
||||
arr: T[],
|
||||
@@ -832,110 +849,92 @@ export function QueryBuilderProvider({
|
||||
(index: number, newQueryData: IBuilderQuery): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
const updatedQueryBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryData,
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
index,
|
||||
newQueryData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: updatedQueryBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: updatedQueryBuilderData,
|
||||
});
|
||||
});
|
||||
setSupersetQuery((prevState) => {
|
||||
const updatedQueryBuilderData = updateSuperSetQueryBuilderData(
|
||||
prevState.builder.queryData,
|
||||
getBuilderAt(prevState, builderSlot).queryData,
|
||||
index,
|
||||
newQueryData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryData: updatedQueryBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryData: updatedQueryBuilderData,
|
||||
});
|
||||
});
|
||||
},
|
||||
[updateQueryBuilderData, updateSuperSetQueryBuilderData],
|
||||
[builderSlot, updateQueryBuilderData, updateSuperSetQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleSetTraceOperatorData = useCallback(
|
||||
(index: number, traceOperatorData: IBuilderTraceOperator): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryTraceOperator,
|
||||
getBuilderAt(prevState, builderSlot).queryTraceOperator,
|
||||
index,
|
||||
traceOperatorData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const updatedTraceOperatorBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryTraceOperator,
|
||||
getBuilderAt(prevState, builderSlot).queryTraceOperator,
|
||||
index,
|
||||
traceOperatorData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryTraceOperator: updatedTraceOperatorBuilderData,
|
||||
});
|
||||
});
|
||||
},
|
||||
[updateQueryBuilderData],
|
||||
[builderSlot, updateQueryBuilderData],
|
||||
);
|
||||
|
||||
const handleSetFormulaData = useCallback(
|
||||
(index: number, formulaData: IBuilderFormula): void => {
|
||||
setCurrentQuery((prevState) => {
|
||||
const updatedFormulasBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryFormulas,
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
index,
|
||||
formulaData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryFormulas: updatedFormulasBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryFormulas: updatedFormulasBuilderData,
|
||||
});
|
||||
});
|
||||
// eslint-disable-next-line sonarjs/no-identical-functions
|
||||
setSupersetQuery((prevState) => {
|
||||
const updatedFormulasBuilderData = updateQueryBuilderData(
|
||||
prevState.builder.queryFormulas,
|
||||
getBuilderAt(prevState, builderSlot).queryFormulas,
|
||||
index,
|
||||
formulaData,
|
||||
);
|
||||
|
||||
return {
|
||||
...prevState,
|
||||
builder: {
|
||||
...prevState.builder,
|
||||
queryFormulas: updatedFormulasBuilderData,
|
||||
},
|
||||
};
|
||||
return withBuilderAt(prevState, builderSlot, {
|
||||
...getBuilderAt(prevState, builderSlot),
|
||||
queryFormulas: updatedFormulasBuilderData,
|
||||
});
|
||||
});
|
||||
},
|
||||
[updateQueryBuilderData],
|
||||
[builderSlot, updateQueryBuilderData],
|
||||
);
|
||||
|
||||
const isStagedQueryUpdated = useCallback(
|
||||
@@ -989,6 +988,9 @@ export function QueryBuilderProvider({
|
||||
const currentGeneratedQuery: Query = {
|
||||
queryType,
|
||||
builder,
|
||||
...(query.builder_ai_query && {
|
||||
builder_ai_query: query.builder_ai_query,
|
||||
}),
|
||||
promql,
|
||||
clickhouse_sql: clickhouseSql,
|
||||
id: uuid(),
|
||||
@@ -1055,42 +1057,34 @@ export function QueryBuilderProvider({
|
||||
}
|
||||
saveRecentQuery(currentQuery);
|
||||
|
||||
const currentQueryData = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression:
|
||||
item.filter?.expression.trim() === ''
|
||||
? ''
|
||||
: (item.filter?.expression ?? ''),
|
||||
},
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
redirectWithQueryBuilderData({
|
||||
...{
|
||||
...currentQueryData,
|
||||
...updateStepInterval({
|
||||
builder: currentQueryData.builder,
|
||||
clickhouse_sql: currentQueryData.clickhouse_sql,
|
||||
promql: currentQueryData.promql,
|
||||
id: currentQueryData.id,
|
||||
queryType,
|
||||
unit: currentQueryData.unit,
|
||||
}),
|
||||
},
|
||||
queryType,
|
||||
const builder = getBuilderAt(currentQuery, builderSlot);
|
||||
const currentQueryData = withBuilderAt(currentQuery, builderSlot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item) => ({
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression:
|
||||
item.filter?.expression.trim() === ''
|
||||
? ''
|
||||
: (item.filter?.expression ?? ''),
|
||||
},
|
||||
filters: {
|
||||
items: [],
|
||||
op: 'AND',
|
||||
},
|
||||
stepInterval: item.stepInterval ?? null,
|
||||
})),
|
||||
});
|
||||
}, [currentQuery, location.pathname, queryType, redirectWithQueryBuilderData]);
|
||||
|
||||
redirectWithQueryBuilderData({ ...currentQueryData, queryType });
|
||||
}, [
|
||||
builderSlot,
|
||||
currentQuery,
|
||||
location.pathname,
|
||||
queryType,
|
||||
redirectWithQueryBuilderData,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
if (location.pathname !== currentPathnameRef.current) {
|
||||
@@ -1121,7 +1115,10 @@ export function QueryBuilderProvider({
|
||||
);
|
||||
|
||||
if (!isValid) {
|
||||
redirectWithQueryBuilderData(validData);
|
||||
redirectWithQueryBuilderData({
|
||||
...validData,
|
||||
builder_ai_query: compositeQueryParam.builder_ai_query,
|
||||
});
|
||||
} else {
|
||||
initQueryBuilderData(compositeQueryParam);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { act, AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
const renderQueryBuilder = (
|
||||
initialQuery: Query,
|
||||
): ReturnType<
|
||||
typeof renderHook<ReturnType<typeof useQueryBuilder>, unknown>
|
||||
> => {
|
||||
const hook = renderHook(() => useQueryBuilder(), {
|
||||
wrapper: AllTheProviders,
|
||||
});
|
||||
|
||||
act(() => {
|
||||
hook.result.current.initQueryBuilderData(initialQuery);
|
||||
});
|
||||
|
||||
return hook;
|
||||
};
|
||||
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.metrics,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: initialQueryAIWithType.builder,
|
||||
};
|
||||
|
||||
describe('QueryBuilder AI slot', () => {
|
||||
it('leaves the AI slot absent for a query that never used it', () => {
|
||||
const { result } = renderQueryBuilder(initialQueriesMap.metrics);
|
||||
|
||||
expect(result.current.currentQuery).not.toHaveProperty('builder_ai_query');
|
||||
});
|
||||
|
||||
it('fills an unused AI slot when AI becomes the query type', () => {
|
||||
const { result } = renderQueryBuilder({
|
||||
...initialQueriesMap.metrics,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
});
|
||||
|
||||
expect(
|
||||
result.current.currentQuery.builder_ai_query?.queryData[0],
|
||||
).toMatchObject({
|
||||
dataSource: 'traces',
|
||||
builderQueryType: 'builder_ai_query',
|
||||
});
|
||||
});
|
||||
|
||||
it('edits the AI slot and leaves builder alone while AI is active', () => {
|
||||
const { result } = renderQueryBuilder(aiQuery);
|
||||
const { builder } = result.current.currentQuery;
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
act(() => {
|
||||
result.current.handleSetQueryData(0, {
|
||||
...initialQueryAIWithType.builder.queryData[0],
|
||||
legend: 'ai legend',
|
||||
});
|
||||
});
|
||||
|
||||
const { currentQuery } = result.current;
|
||||
expect(currentQuery.builder).toStrictEqual(builder);
|
||||
expect(currentQuery.builder_ai_query?.queryData).toHaveLength(2);
|
||||
expect(currentQuery.builder_ai_query?.queryData[0].legend).toBe('ai legend');
|
||||
});
|
||||
|
||||
it('edits builder and keeps the AI slot while Query Builder is active', () => {
|
||||
const { result } = renderQueryBuilder({
|
||||
...aiQuery,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
});
|
||||
const aiBuilder = result.current.currentQuery.builder_ai_query;
|
||||
|
||||
act(() => {
|
||||
result.current.addNewBuilderQuery();
|
||||
});
|
||||
|
||||
expect(result.current.currentQuery.builder.queryData).toHaveLength(2);
|
||||
expect(result.current.currentQuery.builder_ai_query).toStrictEqual(aiBuilder);
|
||||
});
|
||||
});
|
||||
@@ -234,4 +234,40 @@ describe('mapQueryDataFromApi', (): void => {
|
||||
expect(result.builder.queryData[0].queryName).toBe('A');
|
||||
expect(result.builder.queryData[0].dataSource).toBe(DataSource.TRACES);
|
||||
});
|
||||
|
||||
it('puts an AI-typed composite in the AI slot, leaving builder empty', () => {
|
||||
const compositeQuery: ICompositeMetricQuery = {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
unit: undefined,
|
||||
queries: [
|
||||
{
|
||||
type: 'builder_ai_query',
|
||||
spec: {
|
||||
name: 'A',
|
||||
signal: 'traces',
|
||||
aggregations: [{ expression: 'count()' }],
|
||||
},
|
||||
} as QueryEnvelope,
|
||||
{
|
||||
type: 'builder_formula',
|
||||
spec: { name: 'F1', expression: 'A * 2' },
|
||||
} as QueryEnvelope,
|
||||
],
|
||||
};
|
||||
|
||||
const result = mapQueryDataFromApi(compositeQuery);
|
||||
|
||||
expect(result.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(result.builder).toStrictEqual({
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
});
|
||||
expect(result.builder_ai_query?.queryData[0]).toMatchObject({
|
||||
queryName: 'A',
|
||||
builderQueryType: 'builder_ai_query',
|
||||
});
|
||||
expect(result.builder_ai_query?.queryFormulas[0].queryName).toBe('F1');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import {
|
||||
Querybuildertypesv5BucketOptionsDTO,
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { Querybuildertypesv5BucketOptionsDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { TelemetryFieldKey } from 'api/v5/v5';
|
||||
import { Format } from 'constants/formats/types';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -23,14 +20,6 @@ import {
|
||||
} from '../v5/queryRange';
|
||||
import { BaseAutocompleteData } from './queryAutocompleteResponse';
|
||||
|
||||
export const QueryMode = {
|
||||
...EQueryType,
|
||||
/** Not an `EQueryType`: AI queries are builder queries tagged `builderQueryType: builder_ai_query`. */
|
||||
AI_QUERY_BUILDER: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
|
||||
} as const;
|
||||
|
||||
export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode];
|
||||
|
||||
// Type for Formula
|
||||
export interface IBuilderFormula {
|
||||
expression: string;
|
||||
@@ -128,6 +117,8 @@ export interface Query {
|
||||
queryType: EQueryType;
|
||||
promql: IPromQLQuery[];
|
||||
builder: QueryBuilderData;
|
||||
/** AI Query Builder queries; absent until the AI tab is used. */
|
||||
builder_ai_query?: QueryBuilderData;
|
||||
clickhouse_sql: IClickHouseQuery[];
|
||||
id: string;
|
||||
unit?: Format['id'];
|
||||
@@ -153,17 +144,3 @@ export type MapQueryDataToApiResult<T> = {
|
||||
data: T;
|
||||
newLegendMap: Record<string, string>;
|
||||
};
|
||||
|
||||
/** Query Builder and AI share `Query.builder`; the variant says which one it holds. */
|
||||
export const BuilderVariant = {
|
||||
MAIN: 'main',
|
||||
AI: 'ai',
|
||||
} as const;
|
||||
|
||||
export type BuilderVariant =
|
||||
(typeof BuilderVariant)[keyof typeof BuilderVariant];
|
||||
|
||||
/** Builder queries per variant; `Query` has one `builder` slot, so the off-screen variant lives here. */
|
||||
export type BuildersByVariant = Partial<
|
||||
Record<BuilderVariant, Query['builder']>
|
||||
>;
|
||||
|
||||
@@ -2,4 +2,5 @@ export enum EQueryType {
|
||||
QUERY_BUILDER = 'builder',
|
||||
CLICKHOUSE = 'clickhouse_sql',
|
||||
PROM = 'promql',
|
||||
AI_QUERY_BUILDER = 'builder_ai_query',
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user