mirror of
https://github.com/SigNoz/signoz.git
synced 2026-11-09 09:50:41 +00:00
Compare commits
1 Commits
refactor/a
...
fix/genera
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d7ef3b0a67 |
@@ -11,6 +11,8 @@ import axios, { AxiosError, AxiosRequestConfig } from 'axios';
|
||||
// generated API Instance
|
||||
const generatedAPIAxiosInstance = axios.create({
|
||||
baseURL: ENVIRONMENT.baseURL,
|
||||
// The backend binds repeated keys (`states=a&states=b`); axios' default sends `states[]=`.
|
||||
paramsSerializer: { indexes: null },
|
||||
});
|
||||
|
||||
let generatedAPIQueryKeyHeaderContext: Record<string, unknown> | undefined;
|
||||
|
||||
@@ -903,40 +903,6 @@ describe('prepareQueryRangePayloadV5', () => {
|
||||
const logSpec = builderQuery.spec as LogBuilderQuery;
|
||||
expect(logSpec.filter).toStrictEqual({ expression: '' });
|
||||
});
|
||||
|
||||
it('sends the AI slot as builder_ai_query envelopes, ignoring builder', () => {
|
||||
const props: GetQueryResultsProps = {
|
||||
query: {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
id: 'q-ai',
|
||||
unit: undefined,
|
||||
promql: [],
|
||||
clickhouse_sql: [],
|
||||
builder: {
|
||||
queryData: [baseBuilderQuery({ queryName: 'M' })],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
builder_ai_query: {
|
||||
queryData: [
|
||||
baseBuilderQuery({ queryName: 'A', dataSource: DataSource.TRACES }),
|
||||
],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
},
|
||||
},
|
||||
graphType: PANEL_TYPES.TIME_SERIES,
|
||||
selectedTime: 'GLOBAL_TIME',
|
||||
start,
|
||||
end,
|
||||
};
|
||||
|
||||
const { queries } =
|
||||
prepareQueryRangePayloadV5(props).queryPayload.compositeQuery;
|
||||
expect(
|
||||
queries.map((q) => [q.type, (q.spec as { name: string }).name]),
|
||||
).toStrictEqual([['builder_ai_query', 'A']]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapPanelTypeToRequestType', () => {
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
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';
|
||||
@@ -560,15 +559,9 @@ export const prepareQueryRangePayloadV5 = (
|
||||
const requestType = mapPanelTypeToRequestType(graphType);
|
||||
let queries: QueryEnvelope[] = [];
|
||||
|
||||
const wireQuery = toWireQuery(query);
|
||||
|
||||
switch (wireQuery.queryType) {
|
||||
switch (query.queryType) {
|
||||
case EQueryType.QUERY_BUILDER: {
|
||||
const {
|
||||
queryData: data,
|
||||
queryFormulas,
|
||||
queryTraceOperator,
|
||||
} = wireQuery.builder;
|
||||
const { queryData: data, queryFormulas, queryTraceOperator } = query.builder;
|
||||
const currentQueryData = mapQueryDataToApi(data, 'queryName', tableParams);
|
||||
const currentFormulas = mapQueryDataToApi(queryFormulas, 'queryName');
|
||||
|
||||
@@ -630,13 +623,13 @@ export const prepareQueryRangePayloadV5 = (
|
||||
break;
|
||||
}
|
||||
case EQueryType.PROM: {
|
||||
const promQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
const promQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
queries = convertPromQueriesToV5(promQueries.queries);
|
||||
legendMap = promQueries.legends;
|
||||
break;
|
||||
}
|
||||
case EQueryType.CLICKHOUSE: {
|
||||
const chQueries = reduceQueriesToObject(wireQuery[wireQuery.queryType]);
|
||||
const chQueries = reduceQueriesToObject(query[query.queryType]);
|
||||
queries = convertClickHouseQueriesToV5(chQueries.queries);
|
||||
legendMap = chQueries.legends;
|
||||
break;
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useMemo, useRef } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { Formula } from 'container/QueryBuilder/components/Formula';
|
||||
import { QueryBuilderProps } from 'container/QueryBuilder/QueryBuilder.interfaces';
|
||||
@@ -50,7 +49,6 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
initialDataSource,
|
||||
handleRunQuery,
|
||||
} = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const containerRef = useRef(null);
|
||||
|
||||
@@ -131,28 +129,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
const renderedQueries = useMemo(
|
||||
() =>
|
||||
isMultiQueryAllowed
|
||||
? activeBuilder.queryData
|
||||
: activeBuilder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, activeBuilder.queryData],
|
||||
? currentQuery.builder.queryData
|
||||
: currentQuery.builder.queryData.slice(0, 1),
|
||||
[isMultiQueryAllowed, currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const traceOperator = useMemo((): IBuilderTraceOperator | undefined => {
|
||||
if (
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0
|
||||
) {
|
||||
return activeBuilder.queryTraceOperator[0];
|
||||
return currentQuery.builder.queryTraceOperator[0];
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [activeBuilder.queryTraceOperator]);
|
||||
}, [currentQuery.builder.queryTraceOperator]);
|
||||
|
||||
const hasAtLeastOneTraceQuery = useMemo(
|
||||
() =>
|
||||
activeBuilder.queryData.some(
|
||||
currentQuery.builder.queryData.some(
|
||||
(query) => query.dataSource === DataSource.TRACES,
|
||||
),
|
||||
[activeBuilder.queryData],
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const hasTraceOperator = useMemo(
|
||||
@@ -227,9 +225,9 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
{!isMultiQueryAllowed ? (
|
||||
<QueryV2
|
||||
ref={containerRef}
|
||||
key={activeBuilder.queryData[0].queryName}
|
||||
key={currentQuery.builder.queryData[0].queryName}
|
||||
index={0}
|
||||
query={activeBuilder.queryData[0]}
|
||||
query={currentQuery.builder.queryData[0]}
|
||||
fieldsConfig={fieldsConfig}
|
||||
allowedDataSources={queryDataSources}
|
||||
isMultiQueryAllowed={isMultiQueryAllowed}
|
||||
@@ -240,7 +238,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
queryVariant={config?.queryVariant || 'dropdown'}
|
||||
showOnlyWhereClause={showOnlyWhereClause}
|
||||
isRawQuery={isRawQuery}
|
||||
signalSource={activeBuilder.queryData[0].source as 'meter' | ''}
|
||||
signalSource={currentQuery.builder.queryData[0].source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={1}
|
||||
@@ -266,26 +264,28 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
signalSource={query.source as 'meter' | ''}
|
||||
onSignalSourceChange={onSignalSourceChange || ((): void => {})}
|
||||
signalSourceChangeEnabled={signalSourceChangeEnabled}
|
||||
queriesCount={activeBuilder.queryData.length}
|
||||
queriesCount={currentQuery.builder.queryData.length}
|
||||
savePreviousQuery={savePreviousQuery}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
||||
{!showOnlyWhereClause && activeBuilder.queryFormulas?.length > 0 && (
|
||||
<div className="qb-formulas-container">
|
||||
{activeBuilder.queryFormulas.map((formula, index) => {
|
||||
const query =
|
||||
activeBuilder.queryData[index] || activeBuilder.queryData[0];
|
||||
{!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];
|
||||
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
return (
|
||||
<div key={formula.queryName} className="qb-formula">
|
||||
<Formula query={query} formula={formula} index={index} isQBV2 />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{shouldShowFooter && (
|
||||
<QueryFooter
|
||||
@@ -319,7 +319,7 @@ export const QueryBuilderV2 = memo(function QueryBuilderV2({
|
||||
</div>
|
||||
))}
|
||||
|
||||
{activeBuilder.queryFormulas?.map((formula) => (
|
||||
{currentQuery.builder.queryFormulas?.map((formula) => (
|
||||
<div key={formula.queryName} className="formula-name">
|
||||
{formula.queryName}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
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';
|
||||
@@ -16,25 +15,28 @@ 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 = activeBuilder.queryData.length > 1;
|
||||
const hasMultipleQueries = currentQuery.builder.queryData.length > 1;
|
||||
const hasTraceOperator =
|
||||
activeBuilder.queryTraceOperator &&
|
||||
activeBuilder.queryTraceOperator.length > 0;
|
||||
currentQuery.builder.queryTraceOperator &&
|
||||
currentQuery.builder.queryTraceOperator.length > 0;
|
||||
return isRawQueryPanel && hasMultipleQueries && !hasTraceOperator;
|
||||
}, [activeBuilder.queryData, activeBuilder.queryTraceOperator, panelType]);
|
||||
}, [
|
||||
currentQuery?.builder?.queryData,
|
||||
currentQuery?.builder?.queryTraceOperator,
|
||||
panelType,
|
||||
]);
|
||||
|
||||
const traceOperatorWarning = useMemo(() => {
|
||||
if (activeBuilder.queryData.length === 0) {
|
||||
if (currentQuery.builder.queryData.length === 0) {
|
||||
return '';
|
||||
}
|
||||
const firstQuery = activeBuilder.queryData[0];
|
||||
const firstQuery = currentQuery.builder.queryData[0];
|
||||
return `Currently, you are only seeing results from query ${firstQuery.queryName}. Add a trace operator to combine results of multiple queries.`;
|
||||
}, [activeBuilder.queryData]);
|
||||
}, [currentQuery]);
|
||||
return (
|
||||
<div className="qb-trace-operator-button-container">
|
||||
<Tooltip
|
||||
|
||||
@@ -26,7 +26,6 @@ import {
|
||||
queryOperatorSuggestions,
|
||||
} from 'constants/antlrQueryConstants';
|
||||
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import useDebounce from 'hooks/useDebounce';
|
||||
import { debounce, isNull } from 'lodash-es';
|
||||
@@ -133,7 +132,6 @@ function QuerySearch({
|
||||
metricNamespace,
|
||||
valueSuggestionsOverride,
|
||||
}: QuerySearchProps): JSX.Element {
|
||||
const builderQueryType = useBuilderQueryType(queryData);
|
||||
const isDarkMode = useIsDarkMode();
|
||||
const [valueSuggestions, setValueSuggestions] = useState<any[]>([]);
|
||||
const [activeKey, setActiveKey] = useState<string>('');
|
||||
@@ -332,7 +330,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricNamespace,
|
||||
},
|
||||
builderQueryType,
|
||||
queryData.builderQueryType,
|
||||
);
|
||||
|
||||
if (response.data.keys) {
|
||||
@@ -370,7 +368,7 @@ function QuerySearch({
|
||||
hardcodedAttributeKeys,
|
||||
showFilterSuggestionsWithoutMetric,
|
||||
metricNamespace,
|
||||
builderQueryType,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -512,7 +510,7 @@ function QuerySearch({
|
||||
source: signalSource as TelemetrytypesSourceDTO,
|
||||
metricName: debouncedMetricName ?? undefined,
|
||||
},
|
||||
builderQueryType,
|
||||
queryData.builderQueryType,
|
||||
).then((response) => {
|
||||
const responseData = response.data;
|
||||
const responseDataValues = responseData.values;
|
||||
@@ -615,7 +613,7 @@ function QuerySearch({
|
||||
signalSource,
|
||||
toggleSuggestions,
|
||||
valueSuggestionsOverride,
|
||||
builderQueryType,
|
||||
queryData.builderQueryType,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ import { ENTITY_VERSION_V4, ENTITY_VERSION_V5 } from 'constants/app';
|
||||
import QBEntityOptions from 'container/QueryBuilder/components/QBEntityOptions/QBEntityOptions';
|
||||
import { QueryProps } from 'container/QueryBuilder/type';
|
||||
import SpanScopeSelector from 'container/QueryBuilder/filters/QueryBuilderSearchV2/SpanScopeSelector';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useQueryOperations } from 'hooks/queryBuilder/useQueryBuilderOperations';
|
||||
import { Copy, Ellipsis, Trash } from '@signozhq/icons';
|
||||
@@ -63,8 +62,7 @@ export const QueryV2 = forwardRef(function QueryV2(
|
||||
const { cloneQuery, panelType } = useQueryBuilder();
|
||||
|
||||
const hasQueryFunctions = query?.functions?.length > 0;
|
||||
const { dataSource } = query;
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { dataSource, builderQueryType } = query;
|
||||
|
||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
autocompletion,
|
||||
closeCompletion,
|
||||
@@ -88,18 +87,17 @@ function TraceOperatorEditor({
|
||||
const [isExternalQueryChange, setIsExternalQueryChange] = useState(false);
|
||||
const [lastExternalValue, setLastExternalValue] = useState<string>('');
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
|
||||
const queryOptions = useMemo(
|
||||
() =>
|
||||
activeBuilder.queryData
|
||||
currentQuery.builder.queryData
|
||||
.filter((query) => query.dataSource === DataSource.TRACES) // Only show trace queries
|
||||
.map((query) => ({
|
||||
label: query.queryName,
|
||||
type: 'atom',
|
||||
apply: query.queryName,
|
||||
})),
|
||||
[activeBuilder.queryData],
|
||||
[currentQuery.builder.queryData],
|
||||
);
|
||||
|
||||
const toggleSuggestions = useCallback(
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { render, screen } from 'tests/test-utils';
|
||||
import type {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder');
|
||||
jest.mock('../QueryV2/QueryV2', () => ({
|
||||
QueryV2: ({ query }: { query: IBuilderQuery }): JSX.Element => (
|
||||
<div data-testid="query-v2">{query.queryName}</div>
|
||||
),
|
||||
}));
|
||||
jest.mock('../QueryV2/QueryFooter/QueryFooter', () => (): null => null);
|
||||
|
||||
const mockedUseQueryBuilder = jest.mocked(useQueryBuilder);
|
||||
|
||||
function renderWith(currentQuery: Query): void {
|
||||
mockedUseQueryBuilder.mockReturnValue({
|
||||
currentQuery,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
initialDataSource: null,
|
||||
handleSetConfig: jest.fn(),
|
||||
addNewBuilderQuery: jest.fn(),
|
||||
addNewFormula: jest.fn(),
|
||||
addTraceOperator: jest.fn(),
|
||||
handleRunQuery: jest.fn(),
|
||||
} as unknown as ReturnType<typeof useQueryBuilder>);
|
||||
|
||||
render(<QueryBuilderV2 panelType={PANEL_TYPES.TIME_SERIES} version="v3" />);
|
||||
}
|
||||
|
||||
const builderWithQuery = (
|
||||
builder: Query['builder'],
|
||||
queryName: string,
|
||||
): Query['builder'] => ({
|
||||
...builder,
|
||||
queryData: [{ ...builder.queryData[0], queryName }],
|
||||
});
|
||||
|
||||
const query: Query = {
|
||||
...initialQueriesMap.metrics,
|
||||
builder: builderWithQuery(initialQueriesMap.metrics.builder, 'MAIN'),
|
||||
builder_ai_query: builderWithQuery(
|
||||
getActiveBuilder(initialQueryAIWithType),
|
||||
'AI',
|
||||
),
|
||||
};
|
||||
|
||||
describe('QueryBuilderV2 builder slot', () => {
|
||||
it('renders the AI slot when AI is the query type', () => {
|
||||
renderWith({ ...query, queryType: EQueryType.AI_QUERY_BUILDER });
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('AI');
|
||||
});
|
||||
|
||||
it('renders builder when Query Builder is the query type', () => {
|
||||
renderWith(query);
|
||||
|
||||
expect(screen.getByTestId('query-v2')).toHaveTextContent('MAIN');
|
||||
});
|
||||
});
|
||||
@@ -1,8 +1,8 @@
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { render } from 'tests/test-utils';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import { QueryBuilderV2 } from '../QueryBuilderV2';
|
||||
@@ -19,7 +19,7 @@ const aiQuery: Query = {
|
||||
...initialQueriesMap.traces.builder,
|
||||
queryData: initialQueriesMap.traces.builder.queryData.map((query) => ({
|
||||
...query,
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,16 +1,13 @@
|
||||
import { negateOperator, OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { initialQueryBuilderFormValuesMap } from 'constants/queryBuilder';
|
||||
import {
|
||||
BaseAutocompleteData,
|
||||
DataTypes,
|
||||
} from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query, TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource, ReduceOperators } from 'types/common/queryBuilder';
|
||||
import { extractQueryPairs } from 'utils/queryContextUtils';
|
||||
|
||||
import {
|
||||
adjustQueryForV5,
|
||||
convertAggregationToExpression,
|
||||
convertExpressionToFilters,
|
||||
convertFiltersToExpression,
|
||||
@@ -1687,31 +1684,3 @@ 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,5 +1,4 @@
|
||||
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';
|
||||
|
||||
@@ -21,8 +20,7 @@ export function useSyncQueryBuilderFields(
|
||||
fields: SyncedQueryBuilderField[] = NO_FIELDS,
|
||||
): void {
|
||||
const { currentQuery, handleSetQueryData } = useQueryBuilder();
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const queries = activeBuilder.queryData;
|
||||
const queries = currentQuery.builder.queryData;
|
||||
const previousRef = useRef<Map<string, IBuilderQuery>>();
|
||||
const enabled = fields.length > 0;
|
||||
|
||||
|
||||
@@ -9,11 +9,6 @@ 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,
|
||||
@@ -38,6 +33,7 @@ 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';
|
||||
@@ -1034,9 +1030,8 @@ export function getQueryLabelWithAggregation(
|
||||
}
|
||||
|
||||
export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
if (isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
const newQueryData = activeBuilder.queryData.map((query) => {
|
||||
if (currentQuery.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const newQueryData = currentQuery.builder.queryData.map((query) => {
|
||||
const aggregations = query.aggregations?.map((aggregation) => {
|
||||
if (query.dataSource === DataSource.METRICS) {
|
||||
const metricAggregation = aggregation as MetricAggregation;
|
||||
@@ -1077,8 +1072,8 @@ export const adjustQueryForV5 = (currentQuery: Query): Query => {
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
[getBuilderSlotKey(currentQuery.queryType)]: {
|
||||
...activeBuilder,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: newQueryData,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -4,8 +4,6 @@ function QueryTypeTag({ queryType }: IQueryTypeTagProps): JSX.Element {
|
||||
switch (queryType) {
|
||||
case EQueryType.QUERY_BUILDER:
|
||||
return <span>Query Builder</span>;
|
||||
case EQueryType.AI_QUERY_BUILDER:
|
||||
return <span>AI Query Builder</span>;
|
||||
|
||||
case EQueryType.CLICKHOUSE:
|
||||
return <span>ClickHouse Query</span>;
|
||||
|
||||
@@ -3,7 +3,6 @@ import {
|
||||
convertFiltersToExpressionWithExistingQuery,
|
||||
} from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
Query,
|
||||
TagFilter,
|
||||
@@ -557,49 +556,3 @@ describe('clearFilterFromQuery', () => {
|
||||
expect(active.filter?.expression).toBe(`http.method = 'GET'`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('quick filters on an AI query', () => {
|
||||
const builderQuery = buildQuery([], '').builder;
|
||||
const aiQuery = {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: builderQuery,
|
||||
builder_ai_query: buildQuery(
|
||||
[{ key: KEY, op: 'in', value: ['a'] }],
|
||||
`${KEY} IN ['a']`,
|
||||
).builder,
|
||||
} as unknown as Query;
|
||||
|
||||
it('a checkbox toggle writes into the AI slot and leaves builder alone', () => {
|
||||
const result = applyCheckboxToggle({
|
||||
currentQuery: aiQuery,
|
||||
activeQueryIndex: 0,
|
||||
filter: { attributeKey: { key: KEY, type: 'tag' } } as never,
|
||||
quickFilterSource: QuickFiltersSource.AI_OBSERVABILITY,
|
||||
attributeValues: ['a', 'b', 'c'],
|
||||
value: 'b',
|
||||
checked: true,
|
||||
isOnlyOrAllClicked: false,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(
|
||||
result.builder_ai_query?.queryData[0].filters?.items.map(
|
||||
(item) => item.value,
|
||||
),
|
||||
).toStrictEqual([['a', 'b']]);
|
||||
});
|
||||
|
||||
it('clearing a filter clears it from the AI slot', () => {
|
||||
const result = clearFilterFromQuery({
|
||||
currentQuery: aiQuery,
|
||||
filterKey: KEY,
|
||||
activeQueryIndex: 0,
|
||||
});
|
||||
|
||||
expect(result.builder).toBe(builderQuery);
|
||||
expect(result.builder_ai_query?.queryData[0].filters?.items).toStrictEqual(
|
||||
[],
|
||||
);
|
||||
expect(result.builder_ai_query?.queryData[0].filter?.expression).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,11 +10,6 @@ import {
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearchV2/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
@@ -162,9 +157,8 @@ export function applyCheckboxToggle({
|
||||
previousState?: CheckedState;
|
||||
sectionType?: SectionType;
|
||||
}): Query {
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const activeItems = builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
const activeItems =
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
@@ -179,7 +173,7 @@ export function applyCheckboxToggle({
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
const query = cloneDeep(builder.queryData?.[activeQueryIndex]);
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
@@ -436,15 +430,18 @@ export function applyCheckboxToggle({
|
||||
query.filters = synced.filters;
|
||||
}
|
||||
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: [
|
||||
...builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
});
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { DEFAULT_VISIBLE_ITEMS_COUNT } from './constants';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -38,13 +37,12 @@ function useCheckboxDisclosure({
|
||||
DEFAULT_VISIBLE_ITEMS_COUNT,
|
||||
);
|
||||
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
!!queryData?.[activeQueryIndex]?.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => {
|
||||
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual, isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -78,7 +77,7 @@ function useCheckboxFilterActions({
|
||||
dispatch(updatedQuery);
|
||||
|
||||
if (onQuickFilterChange) {
|
||||
const queryData = getActiveBuilder(updatedQuery).queryData[activeQueryIndex];
|
||||
const queryData = updatedQuery.builder.queryData[activeQueryIndex];
|
||||
const expression = queryData?.filter?.expression || '';
|
||||
const filterItemKeys = (queryData?.filters?.items || [])
|
||||
.map((item) => item.key?.key)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
|
||||
import { deriveCheckboxState } from './checkboxFilterQuery';
|
||||
import { isKeyMatch } from './utils';
|
||||
@@ -32,28 +31,33 @@ function useCheckboxFilterState({
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
// derive the state of each filter key here and keep it in sync with current query
|
||||
const currentFilterState = useMemo(
|
||||
() =>
|
||||
deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems: activeQueryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterItems:
|
||||
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterKey: filter.attributeKey.key,
|
||||
}),
|
||||
[attributeValues, activeQueryData, filter.attributeKey, activeQueryIndex],
|
||||
[
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
],
|
||||
);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(activeQueryData?.[activeQueryIndex]?.filters?.items?.filter((item) =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
[activeQueryData, activeQueryIndex, filter.attributeKey],
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// whether the current filter has multiple values to its name in the key op value section
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -129,15 +128,18 @@ export default function CheckboxFilterV2(
|
||||
onToggleOpen();
|
||||
};
|
||||
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
const currentFilterOp = useMemo(() => {
|
||||
const filterSync = activeQueryData?.[activeQueryIndex]?.filters?.items.find(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items.find((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
return filterSync?.op;
|
||||
}, [activeQueryData, activeQueryIndex, filter.attributeKey.key]);
|
||||
}, [
|
||||
currentQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
|
||||
const isNotInOperator = NON_SELECTED_OPERATORS.includes(currentFilterOp || '');
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { convertFiltersToExpression } from 'components/QueryBuilderV2/utils';
|
||||
import { QuickFilterCheckboxUseFieldApis } from 'components/QuickFilters/types';
|
||||
|
||||
@@ -19,9 +18,6 @@ export function useExistingQuery({
|
||||
activeQueryIndex,
|
||||
}: UseExistingQueryParams): UseExistingQueryResult {
|
||||
const { stagedQuery } = useQueryBuilder();
|
||||
const stagedQueryData = stagedQuery
|
||||
? getActiveBuilder(stagedQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const existingQuery = useMemo(() => {
|
||||
if (useFieldApis.existingQuery === null) {
|
||||
@@ -32,7 +28,7 @@ export function useExistingQuery({
|
||||
return useFieldApis.existingQuery;
|
||||
}
|
||||
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
|
||||
// Prefer V5 filter.expression
|
||||
if (queryData?.filter?.expression) {
|
||||
@@ -45,17 +41,21 @@ export function useExistingQuery({
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [useFieldApis.existingQuery, stagedQueryData, activeQueryIndex]);
|
||||
}, [
|
||||
useFieldApis.existingQuery,
|
||||
stagedQuery?.builder.queryData,
|
||||
activeQueryIndex,
|
||||
]);
|
||||
|
||||
// Check if ANY filters exist in query (V3 items or V5 expression)
|
||||
// This is separate from existingQuery because existingQuery can be explicitly
|
||||
// disabled (null) while filters still exist in the query for UI purposes
|
||||
const hasExistingQuery = useMemo(() => {
|
||||
const queryData = stagedQueryData?.[activeQueryIndex];
|
||||
const queryData = stagedQuery?.builder.queryData?.[activeQueryIndex];
|
||||
const hasV3Items = (queryData?.filters?.items?.length ?? 0) > 0;
|
||||
const hasV5Expression = !!queryData?.filter?.expression;
|
||||
return hasV3Items || hasV5Expression || !!existingQuery;
|
||||
}, [stagedQueryData, activeQueryIndex, existingQuery]);
|
||||
}, [stagedQuery?.builder.queryData, activeQueryIndex, existingQuery]);
|
||||
|
||||
return { existingQuery, hasExistingQuery };
|
||||
}
|
||||
|
||||
@@ -9,12 +9,6 @@ import {
|
||||
import { getMs } from 'utils/timeUtils';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { cloneDeep, isArray, isEqual, isFunction } from 'lodash-es';
|
||||
import {
|
||||
AllTraceFilterKeys,
|
||||
@@ -63,8 +57,7 @@ function Duration({
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const syncSelectedFilters = useMemo((): FilterType => {
|
||||
const filters =
|
||||
compositeQuery &&
|
||||
getActiveBuilder(compositeQuery).queryData?.[activeQueryIndex]?.filters;
|
||||
compositeQuery?.builder.queryData?.[activeQueryIndex]?.filters;
|
||||
if (!filters) {
|
||||
return {} as FilterType;
|
||||
}
|
||||
@@ -190,51 +183,49 @@ function Duration({
|
||||
};
|
||||
|
||||
const removeFilterItemIds = (query: Query): Query => {
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
return withBuilderAt(cloneDeep(query), slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
});
|
||||
const clonedQuery = cloneDeep(query);
|
||||
clonedQuery.builder.queryData = clonedQuery.builder.queryData.map((data) => ({
|
||||
...data,
|
||||
filters: {
|
||||
...data.filters,
|
||||
items:
|
||||
data.filters?.items?.map((item) => ({
|
||||
...item,
|
||||
id: '',
|
||||
})) || [],
|
||||
op: data.filters?.op || 'AND',
|
||||
},
|
||||
}));
|
||||
return clonedQuery;
|
||||
};
|
||||
|
||||
const handleRun = useCallback(
|
||||
(props?: HandleRunProps): void => {
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: props?.resetAll
|
||||
? []
|
||||
: (unionTagFilterItems(item.filters?.items || [], preparePostData())
|
||||
.map((item) =>
|
||||
item.key?.key === props?.clearByType ? undefined : item,
|
||||
)
|
||||
.filter((i) => i) as TagFilterItem[]),
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
const currentQueryWithoutIds = removeFilterItemIds(currentQuery);
|
||||
const preparedQueryWithoutIds = removeFilterItemIds(preparedQuery);
|
||||
|
||||
@@ -1,9 +1,4 @@
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { getKeySpellings, isKeyMatch } from '../Checkbox/utils';
|
||||
@@ -25,33 +20,34 @@ export function clearFilterFromQuery({
|
||||
activeQueryIndex: number;
|
||||
operators?: string[];
|
||||
}): Query {
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
return withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(
|
||||
item.filter?.expression ?? '',
|
||||
getKeySpellings(filterKey),
|
||||
false,
|
||||
operators,
|
||||
),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filterKey),
|
||||
) || [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,12 +32,6 @@ import {
|
||||
QuickFilterReadPermission,
|
||||
} from 'lib/authz/hooks/useAuthZ/permissions/quick-filter.permissions';
|
||||
import { isFunction } from 'lodash-es';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import useActiveQueryIndex from './hooks/useActiveQueryIndex';
|
||||
@@ -93,15 +87,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
const [open, setOpen] = useState(false);
|
||||
const activeBuilder = currentQuery
|
||||
? getActiveBuilder(currentQuery)
|
||||
: undefined;
|
||||
|
||||
// Sync lastUsedQuery when queries change (e.g., after deletion)
|
||||
const validQueryIndex = useMemo(
|
||||
() =>
|
||||
Math.min(lastUsedQuery || 0, (activeBuilder?.queryData?.length || 1) - 1),
|
||||
[lastUsedQuery, activeBuilder?.queryData?.length],
|
||||
Math.min(
|
||||
lastUsedQuery || 0,
|
||||
(currentQuery?.builder?.queryData?.length || 1) - 1,
|
||||
),
|
||||
[lastUsedQuery, currentQuery?.builder?.queryData?.length],
|
||||
);
|
||||
|
||||
// Determine if we're in ListView mode
|
||||
@@ -109,15 +103,15 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// Generate query options based on available queries
|
||||
const queryOptions = useMemo(() => {
|
||||
if (!activeBuilder?.queryData) {
|
||||
if (!currentQuery?.builder?.queryData) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return activeBuilder.queryData.map((query, index) => ({
|
||||
return currentQuery.builder.queryData.map((query, index) => ({
|
||||
label: query.queryName || String.fromCharCode(65 + index),
|
||||
value: index,
|
||||
}));
|
||||
}, [activeBuilder?.queryData]);
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
|
||||
// Show dropdown in ListView only for TRACES_EXPLORER source
|
||||
const shouldShowDropdownInListView =
|
||||
@@ -133,35 +127,36 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
|
||||
// clear all the filters for the query which is in sync with filters
|
||||
const handleReset = (): void => {
|
||||
const activeQuery = activeBuilder?.queryData?.[activeQueryIndex];
|
||||
const activeQuery = currentQuery?.builder.queryData?.[activeQueryIndex];
|
||||
|
||||
if (!activeQuery) {
|
||||
return;
|
||||
}
|
||||
|
||||
const slot = getBuilderSlotKey(currentQuery.queryType);
|
||||
const builder = getBuilderAt(currentQuery, slot);
|
||||
const preparedQuery: Query = withBuilderAt(currentQuery, slot, {
|
||||
...builder,
|
||||
queryData: builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => {
|
||||
if (idx !== activeQueryIndex) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
});
|
||||
return {
|
||||
...item,
|
||||
filter: {
|
||||
...item.filter,
|
||||
expression: '',
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items: [],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
};
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(preparedQuery);
|
||||
@@ -171,12 +166,13 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
|
||||
};
|
||||
|
||||
const lastQueryName =
|
||||
showQueryName && activeBuilder?.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
showQueryName &&
|
||||
currentQuery.builder.queryData?.[lastUsedQuery || 0]?.queryName;
|
||||
|
||||
// In ListView, always show the 0th query's name; otherwise use the active query's name
|
||||
const displayedQueryName =
|
||||
isListView || isAIObservabilityRowView
|
||||
? showQueryName && activeBuilder?.queryData?.[0]?.queryName
|
||||
? showQueryName && currentQuery.builder.queryData?.[0]?.queryName
|
||||
: lastQueryName;
|
||||
|
||||
const handleQueryChange = (value: number): void => {
|
||||
|
||||
@@ -311,10 +311,14 @@ export const initialQueryMeterWithType: Query = {
|
||||
|
||||
export const initialQueryAIWithType: Query = {
|
||||
...initialQueryWithType,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: {
|
||||
builder: {
|
||||
...initialQueryWithType.builder,
|
||||
queryData: [initialQueryBuilderFormValuesMap.traces],
|
||||
queryData: [
|
||||
{
|
||||
...initialQueryBuilderFormValuesMap.traces,
|
||||
builderQueryType: 'builder_ai_query',
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,11 +13,6 @@ import {
|
||||
Query,
|
||||
TagFilterItem,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import {
|
||||
getBuilderAt,
|
||||
getBuilderSlotKey,
|
||||
withBuilderAt,
|
||||
} from 'lib/query/builderSlot';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
/** Param shape emitted by the AI for every filter-touching action. */
|
||||
@@ -61,11 +56,12 @@ export function replaceFirstQueryData(
|
||||
query: Query,
|
||||
updated: IBuilderQuery,
|
||||
): Query {
|
||||
const slot = getBuilderSlotKey(query.queryType);
|
||||
const builder = getBuilderAt(query, slot);
|
||||
const queryData = [...builder.queryData];
|
||||
const queryData = [...query.builder.queryData];
|
||||
queryData[0] = updated;
|
||||
return withBuilderAt(query, slot, { ...builder, queryData });
|
||||
return {
|
||||
...query,
|
||||
builder: { ...query.builder, queryData },
|
||||
};
|
||||
}
|
||||
|
||||
/** Page-state deps required by the runQuery / addFilter actions. */
|
||||
|
||||
@@ -40,7 +40,6 @@ export function getQueryNames(currentQuery: Query): BaseOptionType[] {
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -218,7 +218,6 @@ function FormAlertRules({
|
||||
[EQueryType.PROM]: () => getSelectedQueryOptions(currentQuery.promql),
|
||||
[EQueryType.CLICKHOUSE]: () =>
|
||||
getSelectedQueryOptions(currentQuery.clickhouse_sql),
|
||||
[EQueryType.AI_QUERY_BUILDER]: () => [],
|
||||
};
|
||||
|
||||
return queryConfig[currentQuery.queryType]?.() || [];
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
useHandleExplorerTabChange,
|
||||
} from 'hooks/useHandleExplorerTabChange';
|
||||
import { useIsAIAssistantEnabled } from 'hooks/useIsAIAssistantEnabled';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEmpty } from 'lodash-es';
|
||||
import ErrorBoundaryFallback from 'pages/ErrorBoundaryFallback/ErrorBoundaryFallback';
|
||||
import { ExplorerViews } from 'pages/LogsExplorer/utils';
|
||||
@@ -187,9 +186,7 @@ function Explorer(): JSX.Element {
|
||||
|
||||
const isFilterApplied = useMemo(() => {
|
||||
// if any of the non-disabled queries has filters applied, return true
|
||||
const result = (
|
||||
stagedQuery ? getActiveBuilder(stagedQuery) : undefined
|
||||
)?.queryData?.filter(
|
||||
const result = stagedQuery?.builder?.queryData?.filter(
|
||||
(item) => !isEmpty(item.filters?.items) && !item.disabled,
|
||||
);
|
||||
return !!result?.length;
|
||||
|
||||
@@ -15,7 +15,6 @@ import { convertDataValueToMs } from 'container/TimeSeriesView/utils';
|
||||
import { useGetQueryRange } from 'hooks/queryBuilder/useGetQueryRange';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import useUrlYAxisUnit from 'hooks/useUrlYAxisUnit';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AppState } from 'store/reducers';
|
||||
import { Warning } from 'types/api';
|
||||
import APIError from 'types/api/error';
|
||||
@@ -36,7 +35,7 @@ function TimeSeriesViewContainer({
|
||||
const isValidToConvertToMs = useMemo(() => {
|
||||
const isValid: boolean[] = [];
|
||||
|
||||
getActiveBuilder(currentQuery).queryData.forEach(
|
||||
currentQuery.builder.queryData.forEach(
|
||||
({ aggregateAttribute, aggregateOperator }) => {
|
||||
const isExistDurationNanoAttribute =
|
||||
aggregateAttribute?.key === 'durationNano' ||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
FilterDeps,
|
||||
replaceFirstQueryData,
|
||||
} from 'container/AIAssistant/pageActions/builderQueryHelpers';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
ActionResult,
|
||||
PageAction,
|
||||
@@ -94,7 +93,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ filters }): Promise<ActionResult> => {
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
@@ -118,7 +117,7 @@ export function tracesRunQueryAction(
|
||||
},
|
||||
getContext: (): Record<string, unknown> => ({
|
||||
filters:
|
||||
getActiveBuilder(deps.currentQuery).queryData[0]?.filters?.items?.map(
|
||||
deps.currentQuery.builder.queryData[0]?.filters?.items?.map(
|
||||
(f: TagFilterItem) => ({
|
||||
key: f.key?.key,
|
||||
op: f.op,
|
||||
@@ -159,7 +158,7 @@ export function tracesAddFilterAction(
|
||||
},
|
||||
autoApply: true,
|
||||
execute: async ({ key, op, value }): Promise<ActionResult> => {
|
||||
const baseQuery = getActiveBuilder(deps.currentQuery).queryData[0];
|
||||
const baseQuery = deps.currentQuery.builder.queryData[0];
|
||||
if (!baseQuery) {
|
||||
throw new Error('No active query found in Traces Explorer.');
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { initialQueriesMap } from 'constants/queryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { cloneDeep } from 'lodash-es';
|
||||
import { OrderByPayload, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { parseListOrderBy } from 'utils/explorerUtils';
|
||||
@@ -16,9 +15,8 @@ export const getListViewQuery = (
|
||||
? [parseListOrderBy(orderBy)]
|
||||
: [];
|
||||
|
||||
const { queryData: builderQueries } = getActiveBuilder(query);
|
||||
for (let i = 0; i < builderQueries.length; i++) {
|
||||
const queryData = builderQueries[i];
|
||||
for (let i = 0; i < query.builder.queryData.length; i++) {
|
||||
const queryData = query.builder.queryData[i];
|
||||
queryData.groupBy = [];
|
||||
queryData.having = {
|
||||
expression: '',
|
||||
|
||||
@@ -3,13 +3,11 @@ import logEvent from 'api/common/logEvent';
|
||||
import { convertToApiError } from 'api/ErrorResponseHandlerForGeneratedAPIs';
|
||||
import { useListRulesV3 } from 'api/generated/services/rules';
|
||||
import type {
|
||||
ListRulesV3200,
|
||||
ListRulesV3Params,
|
||||
RenderErrorResponseDTO,
|
||||
RuletypesLabelPairDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import type { AxiosError } from 'axios';
|
||||
import { GeneratedAPIInstance } from 'api/generatedAPIInstance';
|
||||
import { isUndefined } from 'lodash-es';
|
||||
|
||||
import type { AlertRule } from './types';
|
||||
@@ -35,18 +33,7 @@ export function useAlertRulesData(
|
||||
const hasLoggedEvent = useRef(false);
|
||||
|
||||
const rulesResponse = useListRulesV3(params, {
|
||||
query: {
|
||||
keepPreviousData: true,
|
||||
// The backend binds repeated `states=` keys; axios' default sends `states[]=`.
|
||||
queryFn: ({ signal }) =>
|
||||
GeneratedAPIInstance<ListRulesV3200>({
|
||||
url: '/api/v3/rules',
|
||||
method: 'GET',
|
||||
params,
|
||||
paramsSerializer: { indexes: null },
|
||||
signal,
|
||||
}),
|
||||
},
|
||||
query: { keepPreviousData: true },
|
||||
});
|
||||
|
||||
const rules = useMemo(
|
||||
|
||||
@@ -29,7 +29,6 @@ import { getMetricNameForQuery } from '../utils';
|
||||
import OptionRenderer from '../OptionRenderer/OptionRenderer';
|
||||
import { GroupByFilterProps } from './GroupByFilter.interfaces';
|
||||
import { toAttributeKey } from './utils';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export const GroupByFilter = memo(function GroupByFilter({
|
||||
query,
|
||||
@@ -46,7 +45,6 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
|
||||
const debouncedValue = useDebounce(searchText, DEBOUNCE_DELAY);
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -54,7 +52,7 @@ export const GroupByFilter = memo(function GroupByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedValue,
|
||||
},
|
||||
builderQueryType,
|
||||
query.builderQueryType,
|
||||
{ enabled: !disabled && isFocused, keepPreviousData: true },
|
||||
);
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ import { selectStyle } from '../QueryBuilderSearchV2/config';
|
||||
import { getMetricNameForQuery } from '../utils';
|
||||
import { OrderByFilterProps } from './OrderByFilter.interfaces';
|
||||
import { useOrderByFilter } from './useOrderByFilter';
|
||||
import { useBuilderQueryType } from 'hooks/queryBuilder/useBuilderQueryType';
|
||||
|
||||
export function OrderByFilter({
|
||||
query,
|
||||
@@ -35,7 +34,6 @@ export function OrderByFilter({
|
||||
handleSearchKeys,
|
||||
} = useOrderByFilter({ query, onChange, entityVersion });
|
||||
|
||||
const builderQueryType = useBuilderQueryType(query);
|
||||
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
|
||||
{
|
||||
signal: DATA_SOURCE_TO_SIGNAL[query.dataSource],
|
||||
@@ -43,7 +41,7 @@ export function OrderByFilter({
|
||||
metricName: getMetricNameForQuery(query),
|
||||
searchText: debouncedSearchText,
|
||||
},
|
||||
builderQueryType,
|
||||
query.builderQueryType,
|
||||
{
|
||||
enabled: !!query.aggregateAttribute?.key || isRawQuery,
|
||||
keepPreviousData: true,
|
||||
|
||||
@@ -9,7 +9,6 @@ import {
|
||||
useState,
|
||||
} from 'react';
|
||||
import { type RefSelectProps, Select, Spin } from 'antd';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { Pill } from '@signozhq/ui/pill';
|
||||
import cx from 'classnames';
|
||||
import {
|
||||
@@ -857,10 +856,14 @@ function QueryBuilderSearchV2(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [query]);
|
||||
|
||||
const isLastQuery = useMemo(() => {
|
||||
const { queryData } = getActiveBuilder(currentQuery);
|
||||
return isEqual(queryData[queryData.length - 1], query);
|
||||
}, [currentQuery, query]);
|
||||
const isLastQuery = useMemo(
|
||||
() =>
|
||||
isEqual(
|
||||
currentQuery.builder.queryData[currentQuery.builder.queryData.length - 1],
|
||||
query,
|
||||
),
|
||||
[currentQuery, query],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLastQuery) {
|
||||
|
||||
@@ -2,7 +2,6 @@ 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';
|
||||
@@ -42,7 +41,7 @@ export function useGraphContextMenu({
|
||||
} {
|
||||
const drilldownQuery = useGetCompositeQueryParam() || query;
|
||||
|
||||
const isQueryTypeBuilder = isBuilderQueryType(drilldownQuery?.queryType);
|
||||
const isQueryTypeBuilder = drilldownQuery?.queryType === 'builder';
|
||||
|
||||
const { aggregateDrilldownConfig } = useAggregateDrilldown({
|
||||
query: drilldownQuery,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { useAppParams } from 'lib/router/useAppParams';
|
||||
import cx from 'classnames';
|
||||
import { ResizeTable } from 'components/ResizeTable';
|
||||
import Download from 'container/Download/Download';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import {
|
||||
createTableColumnsFromQuery,
|
||||
RowData,
|
||||
@@ -34,7 +33,7 @@ export function QueryTable({
|
||||
...props
|
||||
}: QueryTableProps): JSX.Element {
|
||||
const { isDownloadEnabled = false, fileName = '' } = downloadOption || {};
|
||||
const isQueryTypeBuilder = isBuilderQueryType(query.queryType);
|
||||
const isQueryTypeBuilder = query.queryType === 'builder';
|
||||
|
||||
const { servicename: encodedServiceName } = useAppParams<'servicename'>();
|
||||
const servicename = decodeURIComponent(encodedServiceName || '');
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import WidgetHeader from 'container/WidgetCard/Header';
|
||||
import { mockAppLocation } from 'tests/router-mocks';
|
||||
import { fireEvent, render } from 'tests/test-utils';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { QueryTable } from '../QueryTable';
|
||||
import { QueryTableProps, WidgetHeaderProps } from './mocks';
|
||||
@@ -30,21 +29,6 @@ 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();
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
import { useState } from 'react';
|
||||
import { tags } from '@lezer/highlight';
|
||||
import { copilotDarkStyle } from '@uiw/codemirror-theme-copilot';
|
||||
import { githubLightStyle } from '@uiw/codemirror-theme-github';
|
||||
import { useIsDarkMode } from 'hooks/useDarkMode';
|
||||
import { act, render, screen } from 'tests/test-utils';
|
||||
|
||||
import SavedViewHighlightedExpression from '../SavedViewHighlightedExpression';
|
||||
|
||||
jest.mock('hooks/useDarkMode', () => ({
|
||||
useIsDarkMode: jest.fn(),
|
||||
}));
|
||||
|
||||
const mockUseIsDarkMode = useIsDarkMode as jest.MockedFunction<
|
||||
typeof useIsDarkMode
|
||||
>;
|
||||
|
||||
const EXPRESSION = "service.name = 'frontend'";
|
||||
|
||||
function stringColor(styles: typeof copilotDarkStyle): string {
|
||||
return (styles ?? []).find(({ tag }) =>
|
||||
(Array.isArray(tag) ? tag : [tag]).includes(tags.string),
|
||||
)?.color as string;
|
||||
}
|
||||
|
||||
describe('SavedViewHighlightedExpression', () => {
|
||||
it.each([
|
||||
['dark', true, copilotDarkStyle],
|
||||
['light', false, githubLightStyle],
|
||||
])('colours a string value with the %s theme', (_, isDarkMode, styles) => {
|
||||
mockUseIsDarkMode.mockReturnValue(isDarkMode);
|
||||
|
||||
render(<SavedViewHighlightedExpression expression={EXPRESSION} />);
|
||||
|
||||
expect(screen.getByText("'frontend'")).toHaveStyle({
|
||||
color: stringColor(styles),
|
||||
});
|
||||
});
|
||||
|
||||
it('takes the new theme colours when the theme changes', () => {
|
||||
let setIsDarkMode: (isDarkMode: boolean) => void = () => {};
|
||||
function useSwitchableDarkMode(): boolean {
|
||||
const [isDarkMode, setter] = useState(true);
|
||||
setIsDarkMode = setter;
|
||||
return isDarkMode;
|
||||
}
|
||||
mockUseIsDarkMode.mockImplementation(useSwitchableDarkMode);
|
||||
render(<SavedViewHighlightedExpression expression={EXPRESSION} />);
|
||||
|
||||
act(() => setIsDarkMode(false));
|
||||
|
||||
expect(screen.getByText("'frontend'")).toHaveStyle({
|
||||
color: stringColor(githubLightStyle),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -5,19 +5,11 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from '@testing-library/react';
|
||||
import {
|
||||
AuthtypesTransactionDTO,
|
||||
SavedviewtypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
AUTHZ_CHECK_URL,
|
||||
authzMockResponse,
|
||||
} from 'lib/authz/utils/authz-test-utils';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -230,31 +222,6 @@ describe('SavedViewHoverCard', () => {
|
||||
|
||||
expect(card()).toBeNull();
|
||||
});
|
||||
|
||||
it('does not open when the row menu is pressed before the delay', async () => {
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
await renderPanel();
|
||||
|
||||
fireEvent.mouseEnter(row('Errors'));
|
||||
advance(SAVED_VIEW_HOVER_CARD_OPEN_DELAY_MS / 2);
|
||||
await user.click(within(row('Errors')).getByTestId('saved-views-row-menu'));
|
||||
advance(SAVED_VIEW_HOVER_CARD_OPEN_DELAY_MS);
|
||||
|
||||
expect(card()).toBeNull();
|
||||
});
|
||||
|
||||
it('closes on escape, leaving nothing of the popover behind', async () => {
|
||||
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
|
||||
await renderPanel();
|
||||
await openCard('Errors');
|
||||
|
||||
await user.keyboard('{Escape}');
|
||||
|
||||
expect(card()).toBeNull();
|
||||
expect(
|
||||
document.querySelector('[data-radix-popper-content-wrapper]'),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('content', () => {
|
||||
@@ -345,78 +312,6 @@ describe('SavedViewHoverCard', () => {
|
||||
await waitFor(() => expect(fetches).toBe(2));
|
||||
});
|
||||
|
||||
it('shows a skeleton, not the failure, while the view loads', async () => {
|
||||
let requested = false;
|
||||
let release: () => void = () => undefined;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
server.use(
|
||||
rest.get(`${API}/:id`, async (_req, res, ctx) => {
|
||||
requested = true;
|
||||
await gate;
|
||||
return res(ctx.json({ status: 'success', data: errors }));
|
||||
}),
|
||||
);
|
||||
await renderPanel();
|
||||
|
||||
fireEvent.mouseEnter(row('Errors'));
|
||||
advance(SAVED_VIEW_HOVER_CARD_OPEN_DELAY_MS);
|
||||
await waitFor(() => expect(requested).toBe(true));
|
||||
|
||||
expect(
|
||||
screen.getByTestId('saved-view-hover-card-loading'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-view-hover-card-error')).toBeNull();
|
||||
|
||||
release();
|
||||
await screen.findByTestId('saved-view-hover-card-query');
|
||||
});
|
||||
|
||||
it('shows a skeleton while the read check is pending', async () => {
|
||||
let release: () => void = () => undefined;
|
||||
const gate = new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
});
|
||||
const isRead = (transaction: AuthtypesTransactionDTO): boolean =>
|
||||
transaction.relation === 'read';
|
||||
const isReadAll = (transaction: AuthtypesTransactionDTO): boolean =>
|
||||
isRead(transaction) && transaction.object.selector === '*';
|
||||
// Read on every view would skip the per-view check, so it is denied.
|
||||
server.use(
|
||||
rest.post(AUTHZ_CHECK_URL, async (req, res, ctx) => {
|
||||
const payload = (await req.json()) as AuthtypesTransactionDTO[];
|
||||
if (
|
||||
payload.some(
|
||||
(transaction) => isRead(transaction) && !isReadAll(transaction),
|
||||
)
|
||||
) {
|
||||
await gate;
|
||||
}
|
||||
return res(
|
||||
ctx.json(
|
||||
authzMockResponse(
|
||||
payload,
|
||||
payload.map((transaction) => !isReadAll(transaction)),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
await renderPanel();
|
||||
|
||||
fireEvent.mouseEnter(row('Errors'));
|
||||
advance(SAVED_VIEW_HOVER_CARD_OPEN_DELAY_MS);
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('saved-view-hover-card-loading'),
|
||||
).resolves.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('saved-view-hover-card-query')).toBeNull();
|
||||
|
||||
release();
|
||||
await screen.findByTestId('saved-view-hover-card-query');
|
||||
});
|
||||
|
||||
it('says the view failed to load when its fetch fails', async () => {
|
||||
server.use(
|
||||
rest.get(`${API}/:id`, (_req, res, ctx) =>
|
||||
|
||||
@@ -339,45 +339,6 @@ describe('SavedViews - AuthZ', () => {
|
||||
).resolves.not.toHaveAttribute('data-disabled');
|
||||
});
|
||||
|
||||
it('disables save as new without create, keeping update', async () => {
|
||||
mockSavedViewsApi([errors]);
|
||||
server.use(setupAuthzDeny(SavedViewCreatePermission));
|
||||
const { history } = renderHeader(viewUrl(PATH, errors));
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('saved-views-name')).toHaveTextContent('Errors'),
|
||||
);
|
||||
|
||||
history.push(
|
||||
explorerUrl(PATH, {
|
||||
query: queryWith(DataSource.TRACES, 'has_error = false'),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
viewKey: errors.id,
|
||||
}),
|
||||
);
|
||||
await userEvent.click(await screen.findByTestId('saved-views-save-changes'));
|
||||
|
||||
await expect(
|
||||
settledRow('saved-views-save-changes-item-save-as-new'),
|
||||
).resolves.toHaveAttribute('data-disabled');
|
||||
await expect(
|
||||
settledRow('saved-views-save-changes-item-update'),
|
||||
).resolves.not.toHaveAttribute('data-disabled');
|
||||
});
|
||||
|
||||
it('offers clear on a view it cannot read', async () => {
|
||||
mockSavedViewsApi([errors]);
|
||||
server.use(setupAuthzDeny(buildSavedViewReadPermission(errors.id)));
|
||||
renderHeader(viewUrl(PATH, errors));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(screen.getByTestId('saved-views-name')).toHaveTextContent(
|
||||
'No view access',
|
||||
),
|
||||
);
|
||||
|
||||
expect(screen.getByTestId('saved-views-clear')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('says no access when the check denies read, even if the server returns the view', async () => {
|
||||
mockSavedViewsApi([errors]);
|
||||
server.use(setupAuthzDeny(buildSavedViewReadPermission(errors.id)));
|
||||
|
||||
@@ -3,7 +3,6 @@ import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { userEvent } from 'tests/test-utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SavedViewsHeader from '../SavedViewsHeader';
|
||||
@@ -76,39 +75,4 @@ describe('SavedViewsHeader last used view', () => {
|
||||
expect(chip()).toHaveTextContent('My view');
|
||||
expect(urlParam(history, QueryParams.viewKey)).toBeNull();
|
||||
});
|
||||
|
||||
it('does not bring the view back once it is cleared', async () => {
|
||||
mockSavedViewsApi([errors]);
|
||||
const { history } = renderHeader(explorerUrl(PATH, {}));
|
||||
await waitForView('Errors');
|
||||
|
||||
await userEvent.click(screen.getByTestId('saved-views-clear'));
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 200)));
|
||||
|
||||
expect(chip()).toHaveTextContent('My view');
|
||||
expect(urlParam(history, QueryParams.viewKey)).toBeNull();
|
||||
});
|
||||
|
||||
it('forgets a last used view that no longer loads', async () => {
|
||||
mockSavedViewsApi([]);
|
||||
renderHeader(explorerUrl(PATH, {}));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(
|
||||
JSON.parse(
|
||||
localStorage.getItem(LOCALSTORAGE.LAST_USED_SAVED_VIEWS) ?? '{}',
|
||||
),
|
||||
).toStrictEqual({}),
|
||||
);
|
||||
expect(chip()).toHaveTextContent('My view');
|
||||
});
|
||||
|
||||
it('starts on a bare explorer when the stored last used views are unreadable', async () => {
|
||||
localStorage.setItem(LOCALSTORAGE.LAST_USED_SAVED_VIEWS, '{not json');
|
||||
mockSavedViewsApi([errors]);
|
||||
renderHeader(explorerUrl(PATH, {}));
|
||||
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 200)));
|
||||
expect(chip()).toHaveTextContent('My view');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,14 +1,10 @@
|
||||
import { act, screen, waitFor, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { Button } from '@signozhq/ui/button';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SavedViewsHeader from '../SavedViewsHeader';
|
||||
@@ -23,7 +19,7 @@ import {
|
||||
urlParam,
|
||||
viewUrl,
|
||||
} from './savedViewsTestUtils';
|
||||
import { API, mockSavedViewsApi } from './savedViewsApiMock';
|
||||
import { mockSavedViewsApi } from './savedViewsApiMock';
|
||||
|
||||
jest.mock('@signozhq/ui/sonner', () => ({
|
||||
...jest.requireActual('@signozhq/ui/sonner'),
|
||||
@@ -47,27 +43,6 @@ function renderHeader(
|
||||
);
|
||||
}
|
||||
|
||||
// Edits the current query without running it, so it is not the staged one.
|
||||
function EditCurrentQuery(): JSX.Element {
|
||||
const { currentQuery, handleSetQueryData } = useQueryBuilder();
|
||||
return (
|
||||
<Button
|
||||
size="md"
|
||||
variant="solid"
|
||||
color="primary"
|
||||
testId="edit-current-query"
|
||||
onClick={(): void =>
|
||||
handleSetQueryData(0, {
|
||||
...currentQuery.builder.queryData[0],
|
||||
filter: { expression: 'unstaged = true' },
|
||||
})
|
||||
}
|
||||
>
|
||||
Edit query
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
const modal = (): HTMLElement => screen.getByTestId('save-view-modal');
|
||||
const isModalOpen = (): boolean => !!screen.queryByTestId('save-view-modal');
|
||||
|
||||
@@ -185,76 +160,6 @@ describe('SavedViewsHeader saving', () => {
|
||||
expect(toast.success).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('does not create a view from a blank name submitted with Enter', async () => {
|
||||
const requests = mockSavedViewsApi([]);
|
||||
renderHeader(explorerUrl(PATH, {}));
|
||||
|
||||
await clickCreate();
|
||||
await userEvent.type(screen.getByTestId('save-view-name'), ' {enter}');
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 200)));
|
||||
|
||||
expect(requests.created).toHaveLength(0);
|
||||
expect(isModalOpen()).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores Enter while the view is being saved', async () => {
|
||||
let creates = 0;
|
||||
mockSavedViewsApi([]);
|
||||
server.use(
|
||||
rest.post(API, (_req, res, ctx) => {
|
||||
creates += 1;
|
||||
return res(
|
||||
ctx.delay(400),
|
||||
ctx.status(201),
|
||||
ctx.json({ status: 'success', data: { id: 'view-new' } }),
|
||||
);
|
||||
}),
|
||||
);
|
||||
renderHeader(explorerUrl(PATH, {}));
|
||||
|
||||
await clickCreate();
|
||||
await submitName('Slow checkout');
|
||||
await waitFor(() => expect(creates).toBe(1));
|
||||
await userEvent.type(screen.getByTestId('save-view-name'), '{enter}');
|
||||
|
||||
await waitFor(() => expect(isModalOpen()).toBe(false));
|
||||
expect(creates).toBe(1);
|
||||
});
|
||||
|
||||
it('saves the query that was run, not edits made since', async () => {
|
||||
const requests = mockSavedViewsApi([], { createdId: 'view-new' });
|
||||
const { history } = renderWithExplorerProviders(
|
||||
<>
|
||||
<SavedViewsHeader
|
||||
source={SavedviewtypesSourceDTO.traces}
|
||||
onOpenViews={jest.fn()}
|
||||
/>
|
||||
<EditCurrentQuery />
|
||||
</>,
|
||||
explorerUrl(PATH, {}),
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByTestId('edit-current-query'));
|
||||
await clickCreate();
|
||||
await submitName('Slow checkout');
|
||||
|
||||
await waitFor(() => expect(requests.created).toHaveLength(1));
|
||||
const body = requests.created[0] as {
|
||||
spec: { queries: { spec: { filter?: { expression?: string } } }[] };
|
||||
};
|
||||
expect(body.spec.queries[0].spec.filter?.expression ?? '').not.toContain(
|
||||
'unstaged',
|
||||
);
|
||||
await waitFor(() =>
|
||||
expect(urlParam(history, QueryParams.viewKey)).toBe(
|
||||
JSON.stringify('view-new'),
|
||||
),
|
||||
);
|
||||
expect(
|
||||
decodeURIComponent(urlParam(history, QueryParams.compositeQuery) ?? ''),
|
||||
).not.toContain('unstaged');
|
||||
});
|
||||
|
||||
it('cancel closes the modal without saving', async () => {
|
||||
const requests = mockSavedViewsApi([]);
|
||||
renderHeader(explorerUrl(PATH, {}));
|
||||
@@ -326,37 +231,6 @@ describe('SavedViewsHeader saving', () => {
|
||||
expect(requests.created).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('update that fails reports the error and never says the view was updated', async () => {
|
||||
const errors = makeView({ id: 'view-1', displayName: 'Errors' });
|
||||
const requests = mockSavedViewsApi([errors], { updateStatus: 500 });
|
||||
await openDirtyView(errors);
|
||||
|
||||
await chooseFromSaveMenu('Update selected view');
|
||||
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalled());
|
||||
await act(() => new Promise((resolve) => setTimeout(resolve, 100)));
|
||||
expect(requests.updated).toHaveLength(1);
|
||||
expect(toast.success).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('update keeps the color of the open view', async () => {
|
||||
const base = makeView({ id: 'view-1', displayName: 'Errors' });
|
||||
const colored = {
|
||||
...base,
|
||||
spec: { ...base.spec, display: { color: '#e5484d' } },
|
||||
};
|
||||
const requests = mockSavedViewsApi([colored]);
|
||||
await openDirtyView(colored);
|
||||
|
||||
await chooseFromSaveMenu('Update selected view');
|
||||
|
||||
await waitFor(() => expect(requests.updated).toHaveLength(1));
|
||||
const { body } = requests.updated[0] as {
|
||||
body: { spec: { display?: { color?: string } } };
|
||||
};
|
||||
expect(body.spec.display?.color).toBe('#e5484d');
|
||||
});
|
||||
|
||||
describe('view name toggle with unsaved changes', () => {
|
||||
const errors = makeView({ id: 'view-1', displayName: 'Errors' });
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { LOCALSTORAGE } from 'constants/localStorage';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import {
|
||||
@@ -10,7 +9,6 @@ import {
|
||||
waitFor,
|
||||
within,
|
||||
} from 'tests/test-utils';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import SavedViewsPanel from '../SavedViewsPanel';
|
||||
import { mockSavedViewsApi } from './savedViewsApiMock';
|
||||
@@ -221,75 +219,6 @@ describe('SavedViewsPanel list', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('lists only the views of its own source', async () => {
|
||||
mockSavedViewsApi([
|
||||
mine,
|
||||
makeView({
|
||||
id: 'view-9',
|
||||
displayName: 'Noisy logs',
|
||||
source: SavedviewtypesSourceDTO.logs,
|
||||
}),
|
||||
]);
|
||||
renderPanel();
|
||||
|
||||
await screen.findByTestId('saved-views-list');
|
||||
expect(rowNames('saved-views-created-by-me')).toStrictEqual(['Errors']);
|
||||
});
|
||||
|
||||
describe('last used view', () => {
|
||||
const lastUsedViews = (): unknown =>
|
||||
JSON.parse(localStorage.getItem(LOCALSTORAGE.LAST_USED_SAVED_VIEWS) ?? '{}');
|
||||
const logsEntry = { logs: { key: 'logs-view', value: 'Noisy logs' } };
|
||||
|
||||
it('is remembered under the panel source, keeping the other sources', async () => {
|
||||
localStorage.setItem(
|
||||
LOCALSTORAGE.LAST_USED_SAVED_VIEWS,
|
||||
JSON.stringify(logsEntry),
|
||||
);
|
||||
mockSavedViewsApi([mine, alsoMine]);
|
||||
renderPanel();
|
||||
await screen.findByTestId('saved-views-list');
|
||||
await waitFor(() => expectUsable(rowName('Slow spans')));
|
||||
|
||||
await userEvent.click(rowName('Slow spans'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(lastUsedViews()).toStrictEqual({
|
||||
...logsEntry,
|
||||
traces: { key: alsoMine.id, value: 'Slow spans' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('is filed under the source of the panel it was opened from', async () => {
|
||||
const logsView = makeView({
|
||||
id: 'view-9',
|
||||
displayName: 'Noisy logs',
|
||||
source: SavedviewtypesSourceDTO.logs,
|
||||
dataSource: DataSource.LOGS,
|
||||
});
|
||||
mockSavedViewsApi([logsView]);
|
||||
renderWithExplorerProviders(
|
||||
<SavedViewsPanel
|
||||
source={SavedviewtypesSourceDTO.logs}
|
||||
onClose={jest.fn()}
|
||||
/>,
|
||||
explorerUrl(ROUTES.LOGS_EXPLORER, {}),
|
||||
DataSource.LOGS,
|
||||
);
|
||||
await screen.findByTestId('saved-views-list');
|
||||
await waitFor(() => expectUsable(rowName('Noisy logs')));
|
||||
|
||||
await userEvent.click(rowName('Noisy logs'));
|
||||
|
||||
await waitFor(() =>
|
||||
expect(lastUsedViews()).toStrictEqual({
|
||||
logs: { key: logsView.id, value: 'Noisy logs' },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('offers to create a view when there are none, and opens it', async () => {
|
||||
const requests = mockSavedViewsApi([], { createdId: 'view-new' });
|
||||
const { history } = renderPanel();
|
||||
|
||||
@@ -4,13 +4,11 @@ import { toast } from '@signozhq/ui/sonner';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { server } from 'mocks-server/server';
|
||||
import { rest } from 'msw';
|
||||
import { NANO_SECOND_MULTIPLIER } from 'store/globalTime';
|
||||
|
||||
import SavedViewsHeader from '../SavedViewsHeader';
|
||||
import SavedViewsPanel from '../SavedViewsPanel';
|
||||
import { API, mockSavedViewsApi } from './savedViewsApiMock';
|
||||
import { mockSavedViewsApi } from './savedViewsApiMock';
|
||||
import {
|
||||
expectRowUsable,
|
||||
explorerUrl,
|
||||
@@ -183,87 +181,6 @@ describe('SavedViewsPanel row menu', () => {
|
||||
await waitFor(() => expect(row('Slow checkout spans')).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('renames from the stored view, not a stale list row', async () => {
|
||||
const views = [errors, slow];
|
||||
const requests = mockSavedViewsApi(views);
|
||||
renderPanelWithHeader();
|
||||
await screen.findByTestId('saved-views-list');
|
||||
const stored = makeView({
|
||||
id: slow.id,
|
||||
displayName: 'Slow spans',
|
||||
expression: 'duration_nano > 5000',
|
||||
});
|
||||
views[1] = stored;
|
||||
|
||||
await chooseFromRowMenu('Slow spans', 'Edit details');
|
||||
const name = screen.getByTestId('save-view-name');
|
||||
await userEvent.clear(name);
|
||||
await userEvent.type(name, 'Slow checkout spans');
|
||||
await userEvent.click(screen.getByTestId('save-view-submit'));
|
||||
|
||||
await waitFor(() => expect(requests.updated).toHaveLength(1));
|
||||
expect(requests.updated[0].body).toMatchObject({
|
||||
spec: { ...stored.spec, displayName: 'Slow checkout spans' },
|
||||
});
|
||||
});
|
||||
|
||||
it('renames from the list row when the view cannot be read', async () => {
|
||||
const requests = mockSavedViewsApi([errors, slow]);
|
||||
server.use(
|
||||
rest.get(`${API}/:id`, (_req, res, ctx) =>
|
||||
res(ctx.status(403), ctx.json({ status: 'error' })),
|
||||
),
|
||||
);
|
||||
renderPanelWithHeader();
|
||||
|
||||
await chooseFromRowMenu('Slow spans', 'Edit details');
|
||||
const name = screen.getByTestId('save-view-name');
|
||||
await userEvent.clear(name);
|
||||
await userEvent.type(name, 'Slow checkout spans');
|
||||
await userEvent.click(screen.getByTestId('save-view-submit'));
|
||||
|
||||
await waitFor(() => expect(requests.updated).toHaveLength(1));
|
||||
expect(requests.updated[0].body).toMatchObject({
|
||||
spec: { ...slow.spec, displayName: 'Slow checkout spans' },
|
||||
});
|
||||
});
|
||||
|
||||
it('does not rename when the stored view fails to load', async () => {
|
||||
const requests = mockSavedViewsApi([errors, slow]);
|
||||
server.use(
|
||||
rest.get(`${API}/:id`, (_req, res, ctx) =>
|
||||
res(ctx.status(500), ctx.json({ status: 'error' })),
|
||||
),
|
||||
);
|
||||
renderPanelWithHeader();
|
||||
|
||||
await chooseFromRowMenu('Slow spans', 'Edit details');
|
||||
const name = screen.getByTestId('save-view-name');
|
||||
await userEvent.type(name, ' 2');
|
||||
await userEvent.click(screen.getByTestId('save-view-submit'));
|
||||
|
||||
await waitFor(() => expect(toast.error).toHaveBeenCalled());
|
||||
expect(requests.updated).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('ignores Enter while the stored view loads', async () => {
|
||||
const requests = mockSavedViewsApi([errors, slow], { getDelay: 300 });
|
||||
renderPanelWithHeader();
|
||||
|
||||
await chooseFromRowMenu('Slow spans', 'Edit details');
|
||||
const name = screen.getByTestId('save-view-name');
|
||||
await userEvent.type(name, ' 2{enter}');
|
||||
await userEvent.type(name, '{enter}');
|
||||
|
||||
await waitFor(() =>
|
||||
expect(toast.success).toHaveBeenCalledWith(
|
||||
'View updated',
|
||||
expect.anything(),
|
||||
),
|
||||
);
|
||||
expect(requests.updated).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('shows the new name in the header when the open view is renamed', async () => {
|
||||
mockSavedViewsApi([errors]);
|
||||
renderPanelWithHeader(viewUrl(PATH, errors));
|
||||
@@ -319,19 +236,4 @@ describe('SavedViewsPanel row menu', () => {
|
||||
);
|
||||
await waitFor(() => expect(chip()).toHaveTextContent('My view'));
|
||||
});
|
||||
|
||||
it('keeps the open view when another view is deleted', async () => {
|
||||
mockSavedViewsApi([errors, slow]);
|
||||
const { history } = renderPanelWithHeader(viewUrl(PATH, errors));
|
||||
await waitFor(() => expect(chip()).toHaveTextContent('Errors'));
|
||||
|
||||
await chooseFromRowMenu('Slow spans', 'Delete');
|
||||
await userEvent.click(screen.getByTestId('delete-saved-view-confirm'));
|
||||
|
||||
await waitFor(() => expect(row('Slow spans')).toBeUndefined());
|
||||
expect(urlParam(history, QueryParams.viewKey)).toBe(
|
||||
JSON.stringify(errors.id),
|
||||
);
|
||||
expect(chip()).toHaveTextContent('Errors');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider } from 'react-redux';
|
||||
import { SavedviewtypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { QueryParams } from 'constants/query';
|
||||
import ROUTES from 'constants/routes';
|
||||
import configureStore from 'redux-mock-store';
|
||||
import thunk from 'redux-thunk';
|
||||
import store from 'store';
|
||||
import { act, screen, userEvent, waitFor, within } from 'tests/test-utils';
|
||||
import { GlobalReducer } from 'types/reducer/globalTime';
|
||||
|
||||
import SavedViewsPanel from '../SavedViewsPanel';
|
||||
import { mockSavedViewsApi } from './savedViewsApiMock';
|
||||
import {
|
||||
expectRowUsable,
|
||||
explorerUrl,
|
||||
makeView,
|
||||
MENU_CHECK_TIMEOUT,
|
||||
renderWithExplorerProviders,
|
||||
} from './savedViewsTestUtils';
|
||||
|
||||
const mockCopy: jest.MockedFunction<(text: string) => void> = jest.fn();
|
||||
|
||||
// copy-to-clipboard falls back to window.prompt in jsdom.
|
||||
jest.mock('react-use', () => ({
|
||||
...jest.requireActual('react-use'),
|
||||
useCopyToClipboard: (): [unknown, typeof mockCopy] => [null, mockCopy],
|
||||
}));
|
||||
|
||||
const PATH = ROUTES.TRACES_EXPLORER;
|
||||
const slow = makeView({ id: 'view-2', displayName: 'Slow spans' });
|
||||
|
||||
// A store whose global time can change while the panel stays mounted.
|
||||
function renderPanelWithTime(selectedTime: GlobalReducer['selectedTime']): {
|
||||
setTime: (time: Partial<GlobalReducer>) => void;
|
||||
} {
|
||||
let globalTime: GlobalReducer = {
|
||||
...store.getState().globalTime,
|
||||
selectedTime,
|
||||
};
|
||||
const timeStore = configureStore([thunk])(() => ({
|
||||
...store.getState(),
|
||||
globalTime,
|
||||
}));
|
||||
|
||||
renderWithExplorerProviders(
|
||||
<Provider store={timeStore}>
|
||||
<SavedViewsPanel
|
||||
source={SavedviewtypesSourceDTO.traces}
|
||||
onClose={jest.fn()}
|
||||
/>
|
||||
</Provider>,
|
||||
explorerUrl(PATH, {}),
|
||||
);
|
||||
|
||||
return {
|
||||
setTime: (time): void => {
|
||||
globalTime = { ...globalTime, ...time };
|
||||
act(() => {
|
||||
timeStore.dispatch({ type: 'TEST_TIME_CHANGED' });
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const row = (name: string): HTMLElement =>
|
||||
screen
|
||||
.getAllByTestId('saved-views-row')
|
||||
.find((element) => element.textContent?.includes(name)) as HTMLElement;
|
||||
|
||||
const relativeTimeOf = (link: string): string | null =>
|
||||
new URL(link, window.location.origin).searchParams.get(
|
||||
QueryParams.relativeTime,
|
||||
);
|
||||
|
||||
describe('SavedViewsPanel time range', () => {
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
mockCopy.mockClear();
|
||||
mockSavedViewsApi([slow]);
|
||||
});
|
||||
|
||||
it('moves the row link to the new time range when it changes', async () => {
|
||||
const { setTime } = renderPanelWithTime('1h');
|
||||
await screen.findByTestId('saved-views-list');
|
||||
const name = within(row('Slow spans')).getByTestId('saved-views-row-name');
|
||||
expect(relativeTimeOf(name.getAttribute('href') as string)).toBe('1h');
|
||||
|
||||
setTime({ selectedTime: '6h' });
|
||||
|
||||
expect(relativeTimeOf(name.getAttribute('href') as string)).toBe('6h');
|
||||
});
|
||||
|
||||
it('copies a link in the time range that is selected when it is copied', async () => {
|
||||
const { setTime } = renderPanelWithTime('1h');
|
||||
await screen.findByTestId('saved-views-list');
|
||||
setTime({ selectedTime: '6h' });
|
||||
|
||||
await userEvent.click(
|
||||
within(row('Slow spans')).getByTestId('saved-views-row-menu'),
|
||||
);
|
||||
const copyLink = await screen.findByTestId(
|
||||
'saved-views-row-menu-item-copyLink',
|
||||
);
|
||||
await waitFor(() => expectRowUsable(copyLink), MENU_CHECK_TIMEOUT);
|
||||
await userEvent.click(copyLink);
|
||||
|
||||
expect(mockCopy).toHaveBeenCalledTimes(1);
|
||||
expect(relativeTimeOf(mockCopy.mock.calls[0][0])).toBe('6h');
|
||||
});
|
||||
});
|
||||
@@ -1,11 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useMutation, useQueryClient } from 'react-query';
|
||||
import { useQueryClient } from 'react-query';
|
||||
import { useCopyToClipboard } from 'react-use';
|
||||
import { toast } from '@signozhq/ui/sonner';
|
||||
import {
|
||||
getGetSavedViewQueryOptions,
|
||||
updateSavedView,
|
||||
useDeleteSavedView,
|
||||
useUpdateSavedView,
|
||||
} from 'api/generated/services/saved-view';
|
||||
import {
|
||||
RenderErrorResponseDTO,
|
||||
@@ -46,39 +45,8 @@ export function useSavedViewRowActions(
|
||||
const timeSearch = useTimeSearchParams();
|
||||
const [, copyToClipboard] = useCopyToClipboard();
|
||||
const logSavedViewEvent = useSavedViewLogEvent(source);
|
||||
// The update replaces the whole spec, so it starts from the stored view, not
|
||||
// the list row, which can be stale. Without read on the view, the list row is
|
||||
// the only copy there is.
|
||||
const { mutateAsync: renameSavedView, isLoading: isRenaming } = useMutation(
|
||||
async ({
|
||||
view,
|
||||
displayName,
|
||||
}: {
|
||||
view: SavedviewtypesSavedViewDTO;
|
||||
displayName: string;
|
||||
}) => {
|
||||
let { spec } = view;
|
||||
try {
|
||||
({
|
||||
data: { spec },
|
||||
} = await queryClient.fetchQuery(
|
||||
getGetSavedViewQueryOptions({ id: view.id }),
|
||||
));
|
||||
} catch (error) {
|
||||
if ((error as ErrorType<unknown>).response?.status !== 403) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
await updateSavedView(
|
||||
{ id: view.id },
|
||||
{
|
||||
source,
|
||||
schemaVersion: SAVED_VIEW_SCHEMA_VERSION,
|
||||
spec: { ...spec, displayName },
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
const { mutateAsync: updateSavedView, isLoading: isRenaming } =
|
||||
useUpdateSavedView();
|
||||
const { mutateAsync: deleteSavedView, isLoading: isDeleting } =
|
||||
useDeleteSavedView();
|
||||
|
||||
@@ -109,7 +77,14 @@ export function useSavedViewRowActions(
|
||||
displayName: string,
|
||||
): Promise<boolean> => {
|
||||
try {
|
||||
await renameSavedView({ view, displayName });
|
||||
await updateSavedView({
|
||||
pathParams: { id: view.id },
|
||||
data: {
|
||||
source,
|
||||
schemaVersion: SAVED_VIEW_SCHEMA_VERSION,
|
||||
spec: { ...view.spec, displayName },
|
||||
},
|
||||
});
|
||||
await invalidateSavedViews(queryClient, source, view.id);
|
||||
toast.success('View updated', { position: SAVED_VIEW_TOAST_POSITION });
|
||||
logSavedViewEvent(SavedViewEvents.ViewDetailsEdited, {
|
||||
@@ -121,7 +96,7 @@ export function useSavedViewRowActions(
|
||||
return false;
|
||||
}
|
||||
},
|
||||
[renameSavedView, source, queryClient, logSavedViewEvent],
|
||||
[updateSavedView, source, queryClient, logSavedViewEvent],
|
||||
);
|
||||
|
||||
// The deleted view's own query is left alone: refetching it would fail
|
||||
|
||||
@@ -127,48 +127,6 @@ describe('hasUnsavedViewChanges', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is clean while no query is staged', () => {
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery: null,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
sourcePage: SavedviewtypesSourceDTO.traces,
|
||||
options,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('is dirty when the columns are reordered', () => {
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery: staged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
sourcePage: SavedviewtypesSourceDTO.traces,
|
||||
options: { ...options, selectColumns: [columns[1], columns[0]] },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('is dirty when a column is swapped for another', () => {
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view,
|
||||
stagedQuery: staged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
sourcePage: SavedviewtypesSourceDTO.traces,
|
||||
options: {
|
||||
...options,
|
||||
selectColumns: [
|
||||
columns[0],
|
||||
{ name: 'duration_nano', key: 'duration_nano' } as TelemetryFieldKey,
|
||||
],
|
||||
},
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('reads a traces view without columns as the default columns', () => {
|
||||
const bare = viewFrom(
|
||||
toSavedViewSpec({
|
||||
@@ -226,21 +184,6 @@ describe('hasUnsavedViewChanges', () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['format', { format: 'table' as LogViewMode }],
|
||||
['font size', { fontSize: FontSize.LARGE }],
|
||||
])('is dirty when only the %s changes', (_, change) => {
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view: logsView,
|
||||
stagedQuery: logsStaged,
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
sourcePage: SavedviewtypesSourceDTO.logs,
|
||||
options: { selectColumns: shownColumns, ...formatting, ...change },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('reads a view without formatting as the default formatting', () => {
|
||||
const bare = viewFrom(
|
||||
toSavedViewSpec({
|
||||
@@ -352,137 +295,6 @@ describe('hasUnsavedViewChanges', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it.each([
|
||||
['point', 'attribute'],
|
||||
['spanfield', 'span'],
|
||||
['logfield', 'log'],
|
||||
['tracefield', 'trace'],
|
||||
])(
|
||||
'reads a group-by staged as %s and stored as %s as the same field',
|
||||
(legacyContext, storedContext) => {
|
||||
const base = viewFrom(
|
||||
toSavedViewSpec({
|
||||
query: query(DataSource.METRICS, ''),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
displayName: 'By os',
|
||||
}),
|
||||
);
|
||||
const [storedQuery] = base.spec.queries;
|
||||
const storedView = {
|
||||
...base,
|
||||
spec: {
|
||||
...base.spec,
|
||||
queries: [
|
||||
{
|
||||
...storedQuery,
|
||||
spec: {
|
||||
...storedQuery.spec,
|
||||
groupBy: [
|
||||
{
|
||||
name: 'os.type',
|
||||
fieldContext: storedContext,
|
||||
fieldDataType: 'string',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
} as SavedviewtypesSavedViewDTO;
|
||||
const opened = stagedFrom(storedView, DataSource.METRICS);
|
||||
const stagedLegacy = {
|
||||
...opened,
|
||||
builder: {
|
||||
...opened.builder,
|
||||
queryData: opened.builder.queryData.map((queryData) => ({
|
||||
...queryData,
|
||||
groupBy: [{ key: 'os.type', dataType: 'string', type: legacyContext }],
|
||||
})),
|
||||
},
|
||||
} as Query;
|
||||
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view: storedView,
|
||||
stagedQuery: stagedLegacy,
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
sourcePage: SavedviewtypesSourceDTO.metrics,
|
||||
}),
|
||||
).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
// The query still staged after a save keeps the builder's values, while
|
||||
// the server returns the view without them.
|
||||
describe('right after saving a query that carries builder defaults', () => {
|
||||
const withPatch = (
|
||||
dataSource: DataSource,
|
||||
patch: Partial<Query['builder']['queryData'][number]>,
|
||||
): Query => {
|
||||
const base = query(dataSource, '');
|
||||
return {
|
||||
...base,
|
||||
builder: {
|
||||
...base.builder,
|
||||
queryData: base.builder.queryData.map((queryData) => ({
|
||||
...queryData,
|
||||
...patch,
|
||||
})),
|
||||
},
|
||||
} as Query;
|
||||
};
|
||||
|
||||
it.each([
|
||||
[
|
||||
'a zero offset on a list',
|
||||
DataSource.LOGS,
|
||||
SavedviewtypesSourceDTO.logs,
|
||||
PANEL_TYPES.LIST,
|
||||
{ offset: 0 },
|
||||
],
|
||||
[
|
||||
'an empty group-by description',
|
||||
DataSource.TRACES,
|
||||
SavedviewtypesSourceDTO.traces,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
{
|
||||
groupBy: [
|
||||
{
|
||||
key: 'service.name',
|
||||
dataType: 'string',
|
||||
type: 'resource',
|
||||
description: '',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
'a function without arguments',
|
||||
DataSource.METRICS,
|
||||
SavedviewtypesSourceDTO.metrics,
|
||||
PANEL_TYPES.TIME_SERIES,
|
||||
{ functions: [{ name: 'absolute', args: [], namedArgs: {} }] },
|
||||
],
|
||||
])('is clean with %s', (_, dataSource, sourcePage, panelType, patch) => {
|
||||
const saved = withPatch(
|
||||
dataSource,
|
||||
patch as Partial<Query['builder']['queryData'][number]>,
|
||||
);
|
||||
const savedView = viewFrom(
|
||||
toSavedViewSpec({ query: saved, panelType, displayName: 'Saved' }),
|
||||
);
|
||||
|
||||
expect(
|
||||
hasUnsavedViewChanges({
|
||||
view: savedView,
|
||||
stagedQuery: saved,
|
||||
panelType,
|
||||
sourcePage,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('list order', () => {
|
||||
const withOrderBy = (
|
||||
staged: Query,
|
||||
|
||||
@@ -80,66 +80,6 @@ describe('toSavedViewSpec', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps the description and unit of a column, naming it by name before key', () => {
|
||||
const spec = toSavedViewSpec({
|
||||
query: tracesQuery(),
|
||||
panelType: PANEL_TYPES.LIST,
|
||||
displayName: 'Errors',
|
||||
options: {
|
||||
selectColumns: [
|
||||
{
|
||||
name: 'duration_nano',
|
||||
key: 'durationNano',
|
||||
description: 'Span duration',
|
||||
unit: 'ns',
|
||||
},
|
||||
] as unknown as TelemetryFieldKey[],
|
||||
format: 'raw' as LogViewMode,
|
||||
maxLines: 2,
|
||||
fontSize: FontSize.SMALL,
|
||||
},
|
||||
});
|
||||
|
||||
expect(spec.selectedFields).toStrictEqual([
|
||||
{ name: 'duration_nano', description: 'Span duration', unit: 'ns' },
|
||||
]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
PANEL_TYPES.LIST,
|
||||
SavedviewtypesPanelTypeDTO.list,
|
||||
Querybuildertypesv5RequestTypeDTO.raw,
|
||||
],
|
||||
[
|
||||
PANEL_TYPES.TABLE,
|
||||
SavedviewtypesPanelTypeDTO.table,
|
||||
Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
],
|
||||
[
|
||||
PANEL_TYPES.TRACE,
|
||||
SavedviewtypesPanelTypeDTO.trace,
|
||||
Querybuildertypesv5RequestTypeDTO.trace,
|
||||
],
|
||||
[
|
||||
PANEL_TYPES.PIE,
|
||||
SavedviewtypesPanelTypeDTO.graph,
|
||||
Querybuildertypesv5RequestTypeDTO.scalar,
|
||||
],
|
||||
])(
|
||||
'stores the %s panel as panel type %s with request type %s',
|
||||
(panelType, storedPanelType, requestType) => {
|
||||
const spec = toSavedViewSpec({
|
||||
query: tracesQuery(),
|
||||
panelType,
|
||||
displayName: 'Errors',
|
||||
});
|
||||
|
||||
expect(spec.panelType).toBe(storedPanelType);
|
||||
expect(spec.requestType).toBe(requestType);
|
||||
},
|
||||
);
|
||||
|
||||
it('keeps the colour in display', () => {
|
||||
const spec = toSavedViewSpec({
|
||||
query: tracesQuery(),
|
||||
|
||||
@@ -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: (Array.isArray(widgetQueryData) ? widgetQueryData : []).some(
|
||||
isButtonEnabled: (widget?.query?.builder?.queryData &&
|
||||
Array.isArray(widget.query.builder.queryData)
|
||||
? widget.query.builder.queryData
|
||||
: []
|
||||
).some(
|
||||
(q) =>
|
||||
q.dataSource === DataSource.TRACES || q.dataSource === DataSource.LOGS,
|
||||
),
|
||||
@@ -219,7 +219,7 @@ function WidgetGraphComponent({
|
||||
customOnDragSelect={customOnDragSelect}
|
||||
setCurrentGraphRef={setCurrentGraphRef}
|
||||
enableDrillDown={
|
||||
enableDrillDown && isBuilderQueryType(widget?.query?.queryType)
|
||||
enableDrillDown && widget?.query?.queryType === EQueryType.QUERY_BUILDER
|
||||
}
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
createColumnsAndDataSource,
|
||||
@@ -48,18 +47,6 @@ 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,7 +4,6 @@ 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';
|
||||
@@ -120,15 +119,14 @@ 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 = getActiveBuilder(currentQuery)?.queryData?.find(
|
||||
legend = currentQuery?.builder?.queryData?.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
|
||||
if (!legend) {
|
||||
// check if the value is present in the formula
|
||||
legend = getActiveBuilder(currentQuery)?.queryFormulas?.find(
|
||||
legend = currentQuery?.builder?.queryFormulas?.find(
|
||||
(query) => query.queryName === queryName,
|
||||
)?.legend;
|
||||
}
|
||||
@@ -201,7 +199,7 @@ export function createColumnsAndDataSource(
|
||||
: undefined;
|
||||
|
||||
const isNewAggregation =
|
||||
getActiveBuilder(currentQuery).queryData?.find(
|
||||
currentQuery?.builder?.queryData?.find(
|
||||
(query) => query.queryName === item.queryName,
|
||||
)?.aggregations?.length || 0;
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@ 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 {
|
||||
@@ -73,7 +72,8 @@ function GridValueComponent({
|
||||
);
|
||||
}
|
||||
|
||||
const isQueryTypeBuilder = isBuilderQueryType(widget?.query?.queryType);
|
||||
const isQueryTypeBuilder =
|
||||
widget?.query?.queryType === EQueryType.QUERY_BUILDER;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
||||
@@ -58,27 +58,6 @@ 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,
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import { useBuilderQueryType } from '../useBuilderQueryType';
|
||||
import { useQueryBuilder } from '../useQueryBuilder';
|
||||
|
||||
jest.mock('../useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
}));
|
||||
|
||||
const withQueryType = (queryType: EQueryType): void => {
|
||||
(useQueryBuilder as jest.Mock).mockReturnValue({
|
||||
currentQuery: { queryType },
|
||||
});
|
||||
};
|
||||
|
||||
describe('useBuilderQueryType', () => {
|
||||
it('is AI for every query while AI is the query type', () => {
|
||||
withQueryType(EQueryType.AI_QUERY_BUILDER);
|
||||
|
||||
expect(renderHook(() => useBuilderQueryType({})).result.current).toBe(
|
||||
'builder_ai_query',
|
||||
);
|
||||
});
|
||||
|
||||
it("follows the query's own tag otherwise", () => {
|
||||
withQueryType(EQueryType.QUERY_BUILDER);
|
||||
|
||||
expect(
|
||||
renderHook(() =>
|
||||
useBuilderQueryType({ builderQueryType: 'builder_ai_query' }),
|
||||
).result.current,
|
||||
).toBe('builder_ai_query');
|
||||
expect(
|
||||
renderHook(() => useBuilderQueryType({})).result.current,
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
@@ -2,7 +2,6 @@ import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import { initialQueriesMap, initialQueryState } from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { useGetCompositeQueryParam } from 'hooks/queryBuilder/useGetCompositeQueryParam';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { AllTheProviders, renderHook } from 'tests/test-utils';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
@@ -106,25 +105,18 @@ describe('useGetCompositeQueryParam', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const { QUERY_BUILDER, AI_QUERY_BUILDER } = EQueryType;
|
||||
[
|
||||
[ROUTES.LOGS_EXPLORER, QUERY_BUILDER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, AI_QUERY_BUILDER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, QUERY_BUILDER, DataSource.METRICS, ''],
|
||||
[
|
||||
ROUTES.METER_EXPLORER,
|
||||
QUERY_BUILDER,
|
||||
DataSource.METRICS,
|
||||
TelemetrytypesSourceDTO.meter,
|
||||
],
|
||||
].forEach(([route, queryType, dataSource, source]) => {
|
||||
[ROUTES.LOGS_EXPLORER, DataSource.LOGS, ''],
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER, DataSource.TRACES, ''],
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER, DataSource.METRICS, ''],
|
||||
[ROUTES.METER_EXPLORER, DataSource.METRICS, TelemetrytypesSourceDTO.meter],
|
||||
].forEach(([route, dataSource, source]) => {
|
||||
const query = renderOn(clickhouseWithOperator, route);
|
||||
const builder = query && getActiveBuilder(query);
|
||||
|
||||
expect(query?.queryType).toBe(queryType);
|
||||
expect(builder?.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(builder?.queryData[0].source ?? '').toBe(source);
|
||||
expect(builder?.queryTraceOperator).toStrictEqual([]);
|
||||
expect(query?.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(query?.builder.queryData[0].dataSource).toBe(dataSource);
|
||||
expect(query?.builder.queryData[0].source ?? '').toBe(source);
|
||||
expect(query?.builder.queryTraceOperator).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
|
||||
import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { BuilderQuerySource, DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
export type ExplorerQueryRules = {
|
||||
dataSource: DataSource;
|
||||
/** `meter` on the meter explorer, which shares the metrics signal; '' elsewhere. */
|
||||
source: BuilderQuerySource;
|
||||
/** AI_QUERY_BUILDER on the AI explorer, whose queries live in the AI slot. */
|
||||
queryType: EQueryType.QUERY_BUILDER | EQueryType.AI_QUERY_BUILDER;
|
||||
builderQueryType: BuilderQueryType;
|
||||
allowsTraceOperator: boolean;
|
||||
};
|
||||
|
||||
@@ -16,31 +18,31 @@ const EXPLORER_QUERY_RULES: Partial<Record<string, ExplorerQueryRules>> = {
|
||||
[ROUTES.LOGS_EXPLORER]: {
|
||||
dataSource: DataSource.LOGS,
|
||||
source: '',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.TRACES_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
allowsTraceOperator: true,
|
||||
},
|
||||
[ROUTES.AI_OBSERVABILITY_EXPLORER]: {
|
||||
dataSource: DataSource.TRACES,
|
||||
source: '',
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_ai_query,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METRICS_EXPLORER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: '',
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
[ROUTES.METER_EXPLORER]: {
|
||||
dataSource: DataSource.METRICS,
|
||||
source: TelemetrytypesSourceDTO.meter,
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builderQueryType: Querybuildertypesv5QueryTypeDTO.builder_query,
|
||||
allowsTraceOperator: false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -8,8 +8,11 @@ import {
|
||||
initialQueryState,
|
||||
} from 'constants/queryBuilder';
|
||||
import ROUTES from 'constants/routes';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import {
|
||||
IBuilderQuery,
|
||||
Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
@@ -41,9 +44,8 @@ function firstBuilderQuery(
|
||||
queryData: unknown[],
|
||||
pathname: string,
|
||||
): IBuilderQuery {
|
||||
return getActiveBuilder(
|
||||
normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname).query,
|
||||
).queryData[0];
|
||||
return normalizeCompositeQuery(withBuilderQueries(query, queryData), pathname)
|
||||
.query.builder.queryData[0];
|
||||
}
|
||||
|
||||
function without(builderQuery: object, key: keyof IBuilderQuery): object {
|
||||
@@ -60,11 +62,9 @@ describe('normalizeCompositeQuery', () => {
|
||||
queryType: EQueryType.CLICKHOUSE,
|
||||
extra: 'kept',
|
||||
};
|
||||
const aiQuery: Query = {
|
||||
...initialQueriesMap.traces,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
};
|
||||
const aiQuery = withBuilderQueries(initialQueriesMap.traces, [
|
||||
{ ...tracesQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER },
|
||||
]);
|
||||
|
||||
[NOT_AN_EXPLORER, ROUTES.LOGS_EXPLORER].forEach((pathname) =>
|
||||
expect(
|
||||
@@ -86,33 +86,6 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it('moves the AI queries of an older link into the AI slot', () => {
|
||||
const aiQueryData = [
|
||||
{ ...tracesQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER },
|
||||
];
|
||||
const { query, changed } = normalizeCompositeQuery(
|
||||
withBuilderQueries(initialQueriesMap.traces, aiQueryData),
|
||||
ROUTES.AI_OBSERVABILITY_EXPLORER,
|
||||
);
|
||||
|
||||
expect(changed).toBe(true);
|
||||
expect(query.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(query.builder_ai_query?.queryData).toStrictEqual([tracesQuery]);
|
||||
expect(query.builder).toStrictEqual(initialQueryState.builder);
|
||||
});
|
||||
|
||||
it('drops the AI slot outside the AI explorer', () => {
|
||||
const { query } = normalizeCompositeQuery(
|
||||
{
|
||||
...initialQueriesMap.traces,
|
||||
builder_ai_query: initialQueriesMap.traces.builder,
|
||||
},
|
||||
ROUTES.TRACES_EXPLORER,
|
||||
);
|
||||
|
||||
expect(query.builder_ai_query).toBeUndefined();
|
||||
});
|
||||
|
||||
it('adds missing blocks and a stable id without changing its inputs', () => {
|
||||
const { builder: _, ...builderless } = {
|
||||
...initialQueriesMap.logs,
|
||||
@@ -236,7 +209,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
logs,
|
||||
[{ ...logsQuery, builderQueryType: EQueryType.AI_QUERY_BUILDER }],
|
||||
[{ ...logsQuery, builderQueryType: QueryMode.AI_QUERY_BUILDER }],
|
||||
ROUTES.LOGS_EXPLORER,
|
||||
).builderQueryType,
|
||||
).toBeUndefined();
|
||||
@@ -247,7 +220,7 @@ describe('normalizeCompositeQuery', () => {
|
||||
{
|
||||
...tracesQuery,
|
||||
queryName: 'D',
|
||||
builderQueryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
},
|
||||
],
|
||||
@@ -270,7 +243,10 @@ describe('normalizeCompositeQuery', () => {
|
||||
).toBe(TelemetrytypesSourceDTO.meter);
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...tracesQuery, ...llmFilter }], ai),
|
||||
).toStrictEqual({ ...tracesQuery, ...llmFilter });
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
...llmFilter,
|
||||
});
|
||||
expect(
|
||||
firstBuilderQuery(
|
||||
traces,
|
||||
@@ -284,13 +260,17 @@ describe('normalizeCompositeQuery', () => {
|
||||
],
|
||||
ai,
|
||||
),
|
||||
).toStrictEqual({ ...tracesQuery, queryName: 'B', ...llmFilter });
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
queryName: 'B',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
expect(
|
||||
firstBuilderQuery(traces, [{ ...logsQuery, queryName: 'C' }], ai),
|
||||
).toMatchObject({
|
||||
builderQueryType: QueryMode.AI_QUERY_BUILDER,
|
||||
dataSource: DataSource.TRACES,
|
||||
queryName: 'C',
|
||||
filter: { expression: '' },
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -7,10 +7,8 @@ import {
|
||||
Querybuildertypesv5QueryTypeDTO,
|
||||
TelemetrytypesSourceDTO,
|
||||
} from 'api/generated/services/sigNoz.schemas';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { IBuilderQuery, Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import type { QueryBuilderData } from 'types/common/queryBuilder';
|
||||
import { v5 as uuidv5 } from 'uuid';
|
||||
|
||||
import {
|
||||
@@ -80,14 +78,6 @@ function addMissingBlocks(query: Query, urlQuery: object): void {
|
||||
if (!query.builder.queryTraceOperator) {
|
||||
query.builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
if (isPlainObject(query.builder_ai_query)) {
|
||||
const aiBuilder = query.builder_ai_query as QueryBuilderData;
|
||||
aiBuilder.queryFormulas = aiBuilder.queryFormulas ?? [];
|
||||
aiBuilder.queryTraceOperator = aiBuilder.queryTraceOperator ?? [];
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,21 +93,8 @@ function isListOfObjects(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
// Explorers only have a query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = rules.queryType;
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
// Older links carry the AI queries inside `builder`.
|
||||
if (!query.builder_ai_query) {
|
||||
query.builder_ai_query = query.builder;
|
||||
query.builder = cloneDeep(initialQueryState.builder);
|
||||
}
|
||||
} else {
|
||||
delete query.builder_ai_query;
|
||||
}
|
||||
const builder = getActiveBuilder(query);
|
||||
if (!isListOfObjects(builder.queryData) || builder.queryData.length === 0) {
|
||||
builder.queryData = cloneDeep(initialQueryState.builder.queryData);
|
||||
}
|
||||
// Explorers only have the query builder: no PromQL or ClickHouse editor.
|
||||
query.queryType = EQueryType.QUERY_BUILDER;
|
||||
|
||||
if (query.promql.some((promql) => promql.query !== '')) {
|
||||
query.promql = cloneDeep(initialQueryState.promql);
|
||||
@@ -128,10 +105,10 @@ function fitQueryToExplorer(query: Query, rules: ExplorerQueryRules): void {
|
||||
}
|
||||
|
||||
if (!rules.allowsTraceOperator) {
|
||||
builder.queryTraceOperator = [];
|
||||
query.builder.queryTraceOperator = [];
|
||||
}
|
||||
|
||||
builder.queryData = builder.queryData.map((builderQuery) =>
|
||||
query.builder.queryData = query.builder.queryData.map((builderQuery) =>
|
||||
fitBuilderQueryToExplorer(builderQuery, rules),
|
||||
);
|
||||
}
|
||||
@@ -171,9 +148,12 @@ function fillMissingSignal(
|
||||
builderQuery.source = rules.source;
|
||||
}
|
||||
|
||||
// The AI slot marks its queries as AI; a tag from an older link is redundant.
|
||||
if (rules.queryType === EQueryType.AI_QUERY_BUILDER) {
|
||||
delete builderQuery.builderQueryType;
|
||||
// A missing `builderQueryType` already means builder_query, so only AI needs it written.
|
||||
if (
|
||||
builderQuery.builderQueryType === undefined &&
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -189,11 +169,15 @@ function belongsToExplorer(
|
||||
return false;
|
||||
}
|
||||
|
||||
// Only the AI explorer runs AI queries.
|
||||
return (
|
||||
builderQuery.builderQueryType !==
|
||||
Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
);
|
||||
const builderQueryType =
|
||||
builderQuery.builderQueryType ??
|
||||
Querybuildertypesv5QueryTypeDTO.builder_query;
|
||||
|
||||
if (builderQueryType !== rules.builderQueryType) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
@@ -202,5 +186,11 @@ function getDefaultBuilderQuery(rules: ExplorerQueryRules): IBuilderQuery {
|
||||
);
|
||||
builderQuery.source = rules.source;
|
||||
|
||||
if (
|
||||
rules.builderQueryType === Querybuildertypesv5QueryTypeDTO.builder_ai_query
|
||||
) {
|
||||
builderQuery.builderQueryType = rules.builderQueryType;
|
||||
}
|
||||
|
||||
return builderQuery;
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { IBuilderQuery } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { BuilderQueryType } from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
/**
|
||||
* The query's builder type, for picking the field suggestion endpoints. Alert editors
|
||||
* keep AI queries in `builder`, so a query's own tag still counts.
|
||||
*/
|
||||
export function useBuilderQueryType(
|
||||
query: Pick<IBuilderQuery, 'builderQueryType'>,
|
||||
): BuilderQueryType | undefined {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
return currentQuery?.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
? 'builder_ai_query'
|
||||
: query.builderQueryType;
|
||||
}
|
||||
@@ -7,7 +7,6 @@ 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,
|
||||
@@ -489,15 +488,13 @@ export const useQueryOperations: UseQueryOperations = ({
|
||||
],
|
||||
);
|
||||
|
||||
const activeQueryCount = getActiveBuilder(currentQuery).queryData.length;
|
||||
|
||||
const handleDeleteQuery = useCallback(() => {
|
||||
if (activeQueryCount > 1) {
|
||||
if (currentQuery.builder.queryData.length > 1) {
|
||||
removeQueryBuilderEntityByIndex('queryData', index);
|
||||
}
|
||||
setLastUsedQuery(0);
|
||||
}, [
|
||||
activeQueryCount,
|
||||
currentQuery.builder.queryData.length,
|
||||
setLastUsedQuery,
|
||||
removeQueryBuilderEntityByIndex,
|
||||
index,
|
||||
|
||||
@@ -67,23 +67,6 @@ 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,6 +1,5 @@
|
||||
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';
|
||||
|
||||
@@ -8,12 +7,11 @@ export const useGetQueryLabels = (
|
||||
currentQuery: Query,
|
||||
): { label: string; value: string }[] =>
|
||||
useMemo(() => {
|
||||
if (currentQuery && isBuilderQueryType(currentQuery.queryType)) {
|
||||
const activeBuilder = getActiveBuilder(currentQuery);
|
||||
if (currentQuery?.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const queryLabels = getQueryLabelWithAggregation(
|
||||
activeBuilder?.queryData || [],
|
||||
currentQuery?.builder?.queryData || [],
|
||||
);
|
||||
const formulaLabels = (activeBuilder?.queryFormulas ?? []).map(
|
||||
const formulaLabels = (currentQuery?.builder?.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 { getActiveBuilder, isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
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 (!isBuilderQueryType(query.queryType)) {
|
||||
if (query.queryType !== 'builder') {
|
||||
return true; // Non-builder queries don't need this validation
|
||||
}
|
||||
|
||||
const { queryData } = getActiveBuilder(query);
|
||||
const { queryData } = query.builder;
|
||||
|
||||
// Check if any METRICS data source queries exist
|
||||
const metricsQueries = queryData.filter(
|
||||
@@ -124,15 +124,14 @@ export const getLegend = (
|
||||
labelName: string,
|
||||
): string => {
|
||||
// For non-query builder queries, return the label name directly
|
||||
if (!isBuilderQueryType(payloadQuery.queryType)) {
|
||||
if (payloadQuery.queryType !== EQueryType.QUERY_BUILDER) {
|
||||
return labelName;
|
||||
}
|
||||
|
||||
const activeBuilder = getActiveBuilder(payloadQuery);
|
||||
// Combine queryData and queryTraceOperator
|
||||
const allQueries = [
|
||||
...(activeBuilder?.queryData || []),
|
||||
...(activeBuilder?.queryTraceOperator || []),
|
||||
...(payloadQuery?.builder?.queryData || []),
|
||||
...(payloadQuery?.builder?.queryTraceOperator || []),
|
||||
];
|
||||
|
||||
const aggregationPerQuery = allQueries.reduce((acc, query) => {
|
||||
|
||||
@@ -161,35 +161,6 @@ 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,8 +1,6 @@
|
||||
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:
|
||||
@@ -52,11 +50,9 @@ export function containsAnyVariableReference(text: string): boolean {
|
||||
return !!text && ANY_VARIABLE_REFERENCE.test(text);
|
||||
}
|
||||
|
||||
function extractQueryBuilderTexts(
|
||||
builder: QueryBuilderData | undefined,
|
||||
): string[] {
|
||||
function extractQueryBuilderTexts(query: Query): string[] {
|
||||
const texts: string[] = [];
|
||||
const queryDataList = builder?.queryData;
|
||||
const queryDataList = query.builder?.queryData;
|
||||
if (isArray(queryDataList)) {
|
||||
queryDataList.forEach((queryData) => {
|
||||
queryData.filters?.items?.forEach((filter: TagFilterItem) => {
|
||||
@@ -106,13 +102,13 @@ function extractClickhouseSQLTexts(query: Query): string[] {
|
||||
/**
|
||||
* Extracts all text strings from a widget Query that could contain variable
|
||||
* references. Covers:
|
||||
* - QUERY_BUILDER / AI_QUERY_BUILDER: filter items' string values + filter.expression
|
||||
* - QUERY_BUILDER: filter items' string values + filter.expression
|
||||
* - PROM: each promql[].query
|
||||
* - CLICKHOUSE: each clickhouse_sql[].query
|
||||
*/
|
||||
export function extractQueryTextStrings(query: Query): string[] {
|
||||
if (isBuilderQueryType(query.queryType)) {
|
||||
return extractQueryBuilderTexts(getActiveBuilder(query));
|
||||
if (query.queryType === EQueryType.QUERY_BUILDER) {
|
||||
return extractQueryBuilderTexts(query);
|
||||
}
|
||||
|
||||
if (query.queryType === EQueryType.PROM) {
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
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,6 +1,5 @@
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { ICompositeMetricQuery } from 'types/api/alerts/compositeQuery';
|
||||
import { toWireQuery } from 'lib/query/builderSlot';
|
||||
import {
|
||||
BuilderClickHouseResource,
|
||||
BuilderPromQLResource,
|
||||
@@ -92,13 +91,8 @@ const buildPromQuery = (
|
||||
return compositeQuery;
|
||||
};
|
||||
|
||||
const queryTypeMethodMapping: Record<
|
||||
EQueryType,
|
||||
(query: Query, panelType: PANEL_TYPES | null) => ICompositeMetricQuery
|
||||
> = {
|
||||
const queryTypeMethodMapping = {
|
||||
[EQueryType.QUERY_BUILDER]: buildBuilderQuery,
|
||||
[EQueryType.AI_QUERY_BUILDER]: (query, panelType) =>
|
||||
buildBuilderQuery(toWireQuery(query), panelType),
|
||||
[EQueryType.CLICKHOUSE]: buildClickHouseQuery,
|
||||
[EQueryType.PROM]: buildPromQuery,
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
PromQuery,
|
||||
QueryBuilderFormula,
|
||||
} from 'types/api/v5/queryRange';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
convertBuilderQueryToIBuilderQuery,
|
||||
convertQueryBuilderFormulaToIBuilderFormula,
|
||||
@@ -38,7 +37,6 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
> = {};
|
||||
const promQueries: IPromQLQuery[] = [];
|
||||
const clickhouseQueries: IClickHouseQuery[] = [];
|
||||
const isAIQuery = compositeQuery.queryType === EQueryType.AI_QUERY_BUILDER;
|
||||
|
||||
compositeQuery.queries?.forEach((q) => {
|
||||
const spec = q.spec as BuilderQuery | PromQuery | ClickHouseQuery;
|
||||
@@ -51,13 +49,10 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
}
|
||||
} else if (q.type === 'builder_ai_query') {
|
||||
if (spec.name) {
|
||||
const builderQuery = convertBuilderQueryToIBuilderQuery(
|
||||
spec as BuilderQuery,
|
||||
);
|
||||
// Only AI queries kept in `builder` (alerts) need the tag; the AI slot marks its own.
|
||||
builderQueries[spec.name] = isAIQuery
|
||||
? builderQuery
|
||||
: { ...builderQuery, builderQueryType: 'builder_ai_query' };
|
||||
builderQueries[spec.name] = {
|
||||
...convertBuilderQueryToIBuilderQuery(spec as BuilderQuery),
|
||||
builderQueryType: 'builder_ai_query',
|
||||
};
|
||||
builderQueryTypes[spec.name] = 'builder_ai_query';
|
||||
}
|
||||
} else if (q.type === 'builder_formula') {
|
||||
@@ -92,14 +87,8 @@ const mapQueryFromV5 = (compositeQuery: ICompositeMetricQuery): Query => {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
const builder = transformQueryBuilderDataModel(
|
||||
builderQueries,
|
||||
builderQueryTypes,
|
||||
);
|
||||
return {
|
||||
builder: isAIQuery ? transformQueryBuilderDataModel({}) : builder,
|
||||
...(isAIQuery && { builder_ai_query: builder }),
|
||||
builder: transformQueryBuilderDataModel(builderQueries, builderQueryTypes),
|
||||
promql: promQueries,
|
||||
clickhouse_sql: clickhouseQueries,
|
||||
queryType: compositeQuery.queryType,
|
||||
|
||||
@@ -1,93 +0,0 @@
|
||||
import {
|
||||
initialQueriesMap,
|
||||
initialQueryAIWithType,
|
||||
} from 'constants/queryBuilder';
|
||||
import {
|
||||
getActiveBuilder,
|
||||
getBuilderSlotKey,
|
||||
isBuilderQueryType,
|
||||
toWireQuery,
|
||||
} from 'lib/query/builderSlot';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
const baseQuery = initialQueriesMap[DataSource.METRICS];
|
||||
const aiBuilder = getActiveBuilder(initialQueryAIWithType);
|
||||
|
||||
describe('isBuilderQueryType', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, true],
|
||||
[EQueryType.AI_QUERY_BUILDER, true],
|
||||
[EQueryType.CLICKHOUSE, false],
|
||||
[EQueryType.PROM, false],
|
||||
])('%s → %s', (queryType, expected) => {
|
||||
expect(isBuilderQueryType(queryType)).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getBuilderSlotKey', () => {
|
||||
it.each([
|
||||
[EQueryType.QUERY_BUILDER, 'builder'],
|
||||
[EQueryType.CLICKHOUSE, 'builder'],
|
||||
[EQueryType.PROM, 'builder'],
|
||||
[EQueryType.AI_QUERY_BUILDER, 'builder_ai_query'],
|
||||
])('maps %s to %s', (queryType, slot) => {
|
||||
expect(getBuilderSlotKey(queryType)).toBe(slot);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getActiveBuilder', () => {
|
||||
it('reads builder for the Query Builder type', () => {
|
||||
const query: Query = { ...baseQuery, builder_ai_query: aiBuilder };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
|
||||
it('reads the AI slot for the AI type', () => {
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: aiBuilder,
|
||||
};
|
||||
expect(getActiveBuilder(query)).toBe(aiBuilder);
|
||||
});
|
||||
|
||||
it('reads an unused AI slot as holding no queries', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.AI_QUERY_BUILDER };
|
||||
expect(getActiveBuilder(query)).toStrictEqual({
|
||||
queryData: [],
|
||||
queryFormulas: [],
|
||||
queryTraceOperator: [],
|
||||
});
|
||||
});
|
||||
|
||||
it('keeps builder for ClickHouse and PromQL', () => {
|
||||
const query: Query = { ...baseQuery, queryType: EQueryType.PROM };
|
||||
expect(getActiveBuilder(query)).toBe(baseQuery.builder);
|
||||
});
|
||||
});
|
||||
|
||||
describe('toWireQuery', () => {
|
||||
it('passes non-AI queries through untouched', () => {
|
||||
expect(toWireQuery(baseQuery)).toBe(baseQuery);
|
||||
});
|
||||
|
||||
it('sends AI as a builder query holding the AI slot, every query tagged', () => {
|
||||
const untagged = {
|
||||
...aiBuilder.queryData[0],
|
||||
builderQueryType: undefined,
|
||||
};
|
||||
const query: Query = {
|
||||
...baseQuery,
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder_ai_query: { ...aiBuilder, queryData: [untagged] },
|
||||
};
|
||||
|
||||
const wire = toWireQuery(query);
|
||||
|
||||
expect(wire.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(wire.builder.queryData).toStrictEqual([
|
||||
{ ...untagged, builderQueryType: 'builder_ai_query' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -1,73 +0,0 @@
|
||||
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,7 +12,6 @@ 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,
|
||||
@@ -94,8 +93,8 @@ const getQueryByName = <T extends keyof QueryBuilderData>(
|
||||
queryArray.find((q) => q.name === currentQueryName) || defaultQueryValue
|
||||
);
|
||||
}
|
||||
if (isBuilderQueryType(query.queryType)) {
|
||||
const queryArray = (getActiveBuilder(query)[type] || []) as (
|
||||
if (query.queryType === EQueryType.QUERY_BUILDER) {
|
||||
const queryArray = (query.builder[type] || []) as (
|
||||
| IBuilderQuery
|
||||
| IBuilderFormula
|
||||
)[];
|
||||
@@ -174,7 +173,7 @@ const addOperatorFormulaColumns = (
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBuilderQueryType(queryType)) {
|
||||
if (queryType === EQueryType.QUERY_BUILDER) {
|
||||
const currentQueryData = query as IBuilderQuery;
|
||||
let operatorLabel = `${currentQueryData.aggregateOperator}`;
|
||||
if (currentQueryData.aggregateAttribute?.key) {
|
||||
|
||||
51
frontend/src/lib/query/queryMode.ts
Normal file
51
frontend/src/lib/query/queryMode.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
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,9 +1,10 @@
|
||||
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';
|
||||
@@ -32,7 +33,7 @@ function ConfigActions({
|
||||
//TODO: AI query check should be removed after we support alerts on ai query @tewarig
|
||||
if (
|
||||
!actions.createAlert ||
|
||||
currentQuery.queryType === EQueryType.AI_QUERY_BUILDER
|
||||
getQueryMode(currentQuery) === QueryMode.AI_QUERY_BUILDER
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -81,7 +81,7 @@ describe('PanelTypeSwitcher', () => {
|
||||
return {
|
||||
...OPTIONS.find((option) => option.kind === kind),
|
||||
mode: kind === 'signoz/TextPanel' ? 'static' : 'query',
|
||||
supportedQueryTypesKindMap: Object.fromEntries(
|
||||
supportedQueryModesKindMap: Object.fromEntries(
|
||||
queryTypes.map((queryType) => [
|
||||
queryType,
|
||||
queryType === EQueryType.QUERY_BUILDER
|
||||
|
||||
@@ -25,25 +25,6 @@ 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,7 +12,6 @@ const QUERY_TYPE_LABEL: Record<EQueryType, string> = {
|
||||
[EQueryType.QUERY_BUILDER]: 'Query Builder',
|
||||
[EQueryType.CLICKHOUSE]: 'ClickHouse',
|
||||
[EQueryType.PROM]: 'PromQL',
|
||||
[EQueryType.AI_QUERY_BUILDER]: 'AI Query Builder',
|
||||
};
|
||||
|
||||
const SIGNAL_LABEL: Record<TelemetrytypesSignalDTO, string> = {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from 'react';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { useDashboardVariableNames } from 'pages/DashboardPage/DashboardContainer/hooks/useDashboardVariableNames';
|
||||
|
||||
import type { VariableItem } from './types';
|
||||
@@ -21,16 +20,13 @@ const GLOBAL_TIMESTAMP_VARIABLES: VariableItem[] = [
|
||||
*/
|
||||
export function useContextLinkVariables(): VariableItem[] {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
const activeQueryData = currentQuery
|
||||
? getActiveBuilder(currentQuery).queryData
|
||||
: undefined;
|
||||
|
||||
const dashboardVariableNames = useDashboardVariableNames();
|
||||
|
||||
// `_`-prefixed to match V1 and avoid colliding with dashboard-variable names.
|
||||
const fieldVariableNames = useMemo(() => {
|
||||
const names = new Set<string>();
|
||||
(activeQueryData ?? []).forEach((query) => {
|
||||
(currentQuery?.builder?.queryData ?? []).forEach((query) => {
|
||||
(query.groupBy ?? []).forEach((field) => {
|
||||
if (field.key) {
|
||||
names.add(`_${field.key}`);
|
||||
@@ -38,7 +34,7 @@ export function useContextLinkVariables(): VariableItem[] {
|
||||
});
|
||||
});
|
||||
return Array.from(names);
|
||||
}, [activeQueryData]);
|
||||
}, [currentQuery?.builder?.queryData]);
|
||||
|
||||
return useMemo(
|
||||
() => [
|
||||
|
||||
@@ -14,7 +14,7 @@ jest.mock('pages/DashboardPage/DashboardContainer/Panels/registry', () => ({
|
||||
displayName: 'Time Series',
|
||||
icon: (): null => null,
|
||||
mode: 'query',
|
||||
supportedQueryTypesKindMap: {
|
||||
supportedQueryModesKindMap: {
|
||||
builder: { kind: 'signal', signals: ['metrics', 'logs', 'traces'] },
|
||||
clickhouse_sql: { kind: 'signal-less' },
|
||||
promql: { kind: 'signal-less' },
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import {
|
||||
type KeyboardEvent,
|
||||
type ReactNode,
|
||||
@@ -24,20 +23,22 @@ import { useSyncQueryBuilderFields } from 'components/QueryBuilderV2/useSyncQuer
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
import {
|
||||
getQueryTypeSignals,
|
||||
getQueryModeSignals,
|
||||
isRawRequest,
|
||||
} from '../../Panels/types/panelCapabilities';
|
||||
import type { RenderableQueryPanelDefinition } from '../../Panels/types/panelDefinition';
|
||||
import { toPanelType } from '../../Panels/types/panelKind';
|
||||
import { isBuilderQueryType } from 'lib/query/builderSlot';
|
||||
import { getQueryMode, isBuilderMode } from 'lib/query/queryMode';
|
||||
import { QueryMode } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
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. */
|
||||
@@ -57,6 +58,7 @@ interface PanelEditorQueryBuilderProps {
|
||||
*/
|
||||
function PanelEditorQueryBuilder({
|
||||
panelDefinition,
|
||||
onChangeQueryMode,
|
||||
isLoadingQueries,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
@@ -66,10 +68,6 @@ 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);
|
||||
@@ -101,10 +99,10 @@ function PanelEditorQueryBuilder({
|
||||
|
||||
const items = useMemo(() => {
|
||||
const queryTypeComponents: Record<
|
||||
EQueryType,
|
||||
QueryMode,
|
||||
{ icon: ReactNode; label: string; component: ReactNode }
|
||||
> = {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
icon: <Atom size={14} />,
|
||||
label: 'Query Builder',
|
||||
component: (
|
||||
@@ -112,9 +110,9 @@ function PanelEditorQueryBuilder({
|
||||
<QueryBuilderV2
|
||||
panelType={panelType}
|
||||
fieldsConfig={panelDefinition.queryBuilderFields}
|
||||
allowedDataSources={getQueryTypeSignals(
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
EQueryType.QUERY_BUILDER,
|
||||
allowedDataSources={getQueryModeSignals(
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
QueryMode.QUERY_BUILDER,
|
||||
)}
|
||||
showTraceOperator={!isRawQuery}
|
||||
version="v3"
|
||||
@@ -125,12 +123,12 @@ function PanelEditorQueryBuilder({
|
||||
</div>
|
||||
),
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: {
|
||||
[QueryMode.CLICKHOUSE]: {
|
||||
icon: <Terminal size={14} />,
|
||||
label: 'ClickHouse Query',
|
||||
component: <ClickHouseQueryContainer />,
|
||||
},
|
||||
[EQueryType.PROM]: {
|
||||
[QueryMode.PROM]: {
|
||||
icon: (
|
||||
<PromQLIcon
|
||||
fillColor={isDarkMode ? Color.BG_VANILLA_200 : Color.BG_INK_300}
|
||||
@@ -139,7 +137,7 @@ function PanelEditorQueryBuilder({
|
||||
label: 'PromQL',
|
||||
component: <PromQLQueryContainer />,
|
||||
},
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
icon: <Brain size={14} />,
|
||||
label: 'AI Query Builder',
|
||||
component: (
|
||||
@@ -162,8 +160,8 @@ function PanelEditorQueryBuilder({
|
||||
};
|
||||
|
||||
const modes = Object.keys(
|
||||
panelDefinition.supportedQueryTypesKindMap,
|
||||
) as EQueryType[];
|
||||
panelDefinition.supportedQueryModesKindMap,
|
||||
) as QueryMode[];
|
||||
|
||||
return modes.map((queryType) => ({
|
||||
key: queryType,
|
||||
@@ -178,7 +176,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: isBuilderQueryType(queryType),
|
||||
destroyInactiveTabPane: isBuilderMode(queryType),
|
||||
}));
|
||||
}, [panelDefinition, panelType, isDarkMode, isRawQuery]);
|
||||
|
||||
@@ -195,8 +193,8 @@ function PanelEditorQueryBuilder({
|
||||
className={cx(styles.tabsContainer, {
|
||||
[styles.stickyNav]: stickyHeader,
|
||||
})}
|
||||
activeKey={currentQuery.queryType}
|
||||
onChange={onChangeQueryType}
|
||||
activeKey={getQueryMode(currentQuery)}
|
||||
onChange={onChangeQueryMode}
|
||||
tabBarExtraContent={
|
||||
<span className={styles.runQueryBtnContainer}>
|
||||
<TextToolTip text="This will temporarily save the current query and graph state. This will persist across tab change" />
|
||||
|
||||
@@ -8,6 +8,7 @@ import PanelEditorQueryBuilder from './PanelEditorQueryBuilder';
|
||||
function QueryBuilderEditorPane({
|
||||
panelDefinition,
|
||||
isLoadingQueries,
|
||||
onChangeQueryMode,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
stickyHeader,
|
||||
@@ -16,6 +17,7 @@ function QueryBuilderEditorPane({
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
onCancelQuery={onCancelQuery}
|
||||
stickyHeader={stickyHeader}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
import { render, screen, userEvent } from 'tests/test-utils';
|
||||
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 type { PanelKind } from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import {
|
||||
toPanelType,
|
||||
type PanelKind,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelKind';
|
||||
import type { BuildersByVariant } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
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();
|
||||
@@ -51,19 +57,25 @@ const BUILDER_QUERY = {
|
||||
builder: { queryData: [{ dataSource: 'traces' }] },
|
||||
};
|
||||
const AI_QUERY = {
|
||||
queryType: EQueryType.AI_QUERY_BUILDER,
|
||||
builder: { queryData: [] },
|
||||
builder_ai_query: {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
},
|
||||
};
|
||||
|
||||
/** Stands in for the editor shell. */
|
||||
/** Stands in for the editor shell: owns the tab switch and its parked Query Builder query. */
|
||||
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()}
|
||||
@@ -160,12 +172,11 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('seeds an AI traces query when switching to the AI tab', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
it('seeds an AI traces query when switching to the AI tab', () => {
|
||||
mockBuilder(BUILDER_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
|
||||
expect(updateAllQueriesOperators).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
@@ -173,16 +184,11 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
'traces',
|
||||
);
|
||||
const [seeded] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(seeded.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(seeded.builder_ai_query.queryData[0]).toMatchObject({
|
||||
dataSource: 'traces',
|
||||
});
|
||||
expect(seeded.builder_ai_query.queryData[0].builderQueryType).toBeUndefined();
|
||||
expect(seeded.builder).toBe(BUILDER_QUERY.builder);
|
||||
expect(seeded.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
expect(seeded.builder.queryData[0].builderQueryType).toBe('builder_ai_query');
|
||||
});
|
||||
|
||||
it('keeps the builder query when returning from ClickHouse', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
it('keeps the builder query when returning from ClickHouse', () => {
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ dataSource: 'logs', legend: 'kept' }] },
|
||||
@@ -190,19 +196,18 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
mockBuilder(logsQuery);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
await user.click(screen.getByText('ClickHouse Query'));
|
||||
fireEvent.click(screen.getByText('ClickHouse Query'));
|
||||
const [onClickHouse] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(onClickHouse.queryType).toBe(EQueryType.CLICKHOUSE);
|
||||
// The builder slot already holds the Query Builder query — nothing to swap.
|
||||
expect(onClickHouse.builder).toBe(logsQuery.builder);
|
||||
});
|
||||
|
||||
it('seeds a default query for the kind when leaving AI for an unused Query Builder', async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
it('shows a fresh default query when leaving AI', () => {
|
||||
mockBuilder(AI_QUERY);
|
||||
renderBuilder('signoz/TimeSeriesPanel');
|
||||
|
||||
await user.click(screen.getByText('Query Builder'));
|
||||
fireEvent.click(screen.getByText('Query Builder'));
|
||||
|
||||
const [next] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
expect(next.queryType).toBe(EQueryType.QUERY_BUILDER);
|
||||
@@ -210,22 +215,7 @@ 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", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
mockBuilder({
|
||||
...AI_QUERY,
|
||||
builder: { queryData: [{ dataSource: 'metrics' }] },
|
||||
});
|
||||
renderBuilder('signoz/ListPanel');
|
||||
|
||||
await user.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", async () => {
|
||||
const user = userEvent.setup({ pointerEventsCheck: 0 });
|
||||
it("keeps each tab's own query across Query Builder ↔ AI round trips", () => {
|
||||
const logsQuery = {
|
||||
queryType: EQueryType.QUERY_BUILDER,
|
||||
builder: { queryData: [{ dataSource: 'logs' }] },
|
||||
@@ -236,26 +226,23 @@ describe('PanelEditorQueryBuilder AI tab', () => {
|
||||
mockBuilder(logsQuery);
|
||||
const { rerender } = render(host());
|
||||
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
const [aiQuery] = redirectWithQueryBuilderData.mock.calls[0];
|
||||
// The user writes an AI query, then goes back to Query Builder.
|
||||
const editedAIBuilder = {
|
||||
queryData: [{ dataSource: 'traces', builderQueryType: 'builder_ai_query' }],
|
||||
};
|
||||
mockBuilder({ ...aiQuery, builder_ai_query: editedAIBuilder });
|
||||
mockBuilder({ ...aiQuery, builder: editedAIBuilder });
|
||||
rerender(host());
|
||||
await user.click(screen.getByText('Query Builder'));
|
||||
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());
|
||||
await user.click(screen.getByText('AI Query Builder'));
|
||||
fireEvent.click(screen.getByText('AI Query Builder'));
|
||||
const [backToAI] = redirectWithQueryBuilderData.mock.calls[2];
|
||||
expect(backToAI.queryType).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
expect(backToAI.builder_ai_query).toBe(editedAIBuilder);
|
||||
expect(backToAI.builder).toBe(editedAIBuilder);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,72 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
import type { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getQueryTypeSignals,
|
||||
type SupportedQueryTypesKindMap,
|
||||
} from 'pages/DashboardPage/DashboardContainer/Panels/types/panelCapabilities';
|
||||
import { seedBuilderForMode } from 'pages/DashboardPage/DashboardContainer/Panels/utils/seedBuilderForMode';
|
||||
|
||||
interface UseQueryTypeChangeArgs {
|
||||
panelType: PANEL_TYPES;
|
||||
supportedQueryTypesKindMap: SupportedQueryTypesKindMap;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tab switching: each query type keeps its own slot, so a switch only changes `queryType`.
|
||||
* A builder slot is seeded when it can't be used as-is: an AI slot never opened, or
|
||||
* Query Builder queries on a signal this kind can't author (an AI panel's default).
|
||||
*/
|
||||
export function useQueryTypeChange({
|
||||
panelType,
|
||||
supportedQueryTypesKindMap,
|
||||
}: UseQueryTypeChangeArgs): (key: string) => void {
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
} = useQueryBuilder();
|
||||
|
||||
return useCallback(
|
||||
(key: string): void => {
|
||||
const queryType = key as EQueryType;
|
||||
const builderSignals = getQueryTypeSignals(
|
||||
supportedQueryTypesKindMap,
|
||||
EQueryType.QUERY_BUILDER,
|
||||
);
|
||||
const seed = (): Query['builder'] =>
|
||||
seedBuilderForMode({
|
||||
mode: queryType,
|
||||
defaultSignal: builderSignals[0],
|
||||
panelType,
|
||||
updateAllQueriesOperators,
|
||||
});
|
||||
|
||||
const next: Query = { ...currentQuery, queryType };
|
||||
if (queryType === EQueryType.AI_QUERY_BUILDER && !next.builder_ai_query) {
|
||||
next.builder_ai_query = seed();
|
||||
}
|
||||
const authorableSignals = new Set<string>(builderSignals);
|
||||
const builderFitsKind =
|
||||
next.builder.queryData.length > 0 &&
|
||||
next.builder.queryData.every((query) =>
|
||||
authorableSignals.has(query.dataSource),
|
||||
);
|
||||
if (queryType === EQueryType.QUERY_BUILDER && !builderFitsKind) {
|
||||
next.builder = seed();
|
||||
}
|
||||
|
||||
redirectWithQueryBuilderData(next);
|
||||
},
|
||||
[
|
||||
currentQuery,
|
||||
panelType,
|
||||
redirectWithQueryBuilderData,
|
||||
supportedQueryTypesKindMap,
|
||||
updateAllQueriesOperators,
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -72,6 +72,8 @@ 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -93,6 +95,7 @@ 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.
|
||||
@@ -309,6 +312,7 @@ function QueryEditorBody({
|
||||
panelDefinition={panelDefinition}
|
||||
signal={listSignal}
|
||||
isLoadingQueries={isFetching}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={runQuery}
|
||||
onCancelQuery={cancelQuery}
|
||||
spec={spec}
|
||||
|
||||
@@ -60,8 +60,9 @@ jest.mock('../hooks/usePanelEditorQuerySync', () => ({
|
||||
}));
|
||||
|
||||
const mockUseTypeSwitch = jest.fn();
|
||||
jest.mock('../hooks/usePanelKindChange', () => ({
|
||||
usePanelKindChange: (args: unknown): unknown => mockUseTypeSwitch(args),
|
||||
jest.mock('../hooks/usePanelKindAndQueryModeSwitch', () => ({
|
||||
usePanelKindAndQueryModeSwitch: (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 { useState } from 'react';
|
||||
import { useRef, useState } from 'react';
|
||||
import { QueryClient, QueryClientProvider } from 'react-query';
|
||||
// eslint-disable-next-line no-restricted-imports
|
||||
import { Provider as ReduxProvider } from 'react-redux';
|
||||
@@ -22,6 +22,8 @@ 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', () => {
|
||||
@@ -79,10 +81,17 @@ function EditorRoute({ panelId }: { panelId: string }): JSX.Element {
|
||||
savedQueries: panel.spec.queries,
|
||||
});
|
||||
const panelDefinition = requireQueryPanelDefinition('signoz/TimeSeriesPanel');
|
||||
const parkedBuilders = useRef<BuildersByVariant>({});
|
||||
const onChangeQueryMode = useQueryModeChange({
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
supportedQueryModesKindMap: panelDefinition.supportedQueryModesKindMap,
|
||||
parkedBuilders,
|
||||
});
|
||||
|
||||
return (
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
isLoadingQueries={false}
|
||||
onStageRunQuery={noop}
|
||||
onCancelQuery={noop}
|
||||
|
||||
@@ -5,11 +5,11 @@ import { handleQueryChange } from 'lib/query/panelQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import { resolveQueryType } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { resolveQueryMode } from 'pages/DashboardPage/DashboardContainer/Panels/capabilities';
|
||||
import { getBuilderQueries } from '../../../Panels/utils/getBuilderQueries';
|
||||
import { toPerses } from '../../../queryV5/persesQueryAdapters';
|
||||
import { getSwitchedPluginSpec } from '../../getSwitchedPluginSpec';
|
||||
import { usePanelKindChange } from '../usePanelKindChange';
|
||||
import { usePanelKindAndQueryModeSwitch } from '../usePanelKindAndQueryModeSwitch';
|
||||
|
||||
jest.mock('hooks/queryBuilder/useQueryBuilder', () => ({
|
||||
useQueryBuilder: jest.fn(),
|
||||
@@ -18,8 +18,10 @@ jest.mock('lib/query/panelQuery', () => ({
|
||||
handleQueryChange: jest.fn(),
|
||||
}));
|
||||
jest.mock('../../../Panels/capabilities', () => ({
|
||||
resolveQueryType: jest.fn(),
|
||||
resolveQueryMode: 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')
|
||||
@@ -37,7 +39,7 @@ jest.mock('../../../Panels/utils/getBuilderQueries', () => ({
|
||||
|
||||
const mockUseQueryBuilder = useQueryBuilder as unknown as jest.Mock;
|
||||
const mockHandleQueryChange = handleQueryChange as unknown as jest.Mock;
|
||||
const mockResolveQueryType = resolveQueryType as unknown as jest.Mock;
|
||||
const mockResolveQueryMode = resolveQueryMode 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;
|
||||
@@ -100,7 +102,7 @@ function builderState(currentQuery: Query): {
|
||||
};
|
||||
}
|
||||
|
||||
describe('usePanelKindChange', () => {
|
||||
describe('usePanelKindAndQueryModeSwitch', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockHandleQueryChange.mockReturnValue(TRANSFORMED);
|
||||
@@ -109,7 +111,7 @@ describe('usePanelKindChange', () => {
|
||||
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.
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
mockResolveQueryMode.mockReturnValue('builder');
|
||||
});
|
||||
|
||||
it('does nothing when switching to the current kind', () => {
|
||||
@@ -118,7 +120,7 @@ describe('usePanelKindChange', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
@@ -137,7 +139,7 @@ describe('usePanelKindChange', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(state);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: tableSpec,
|
||||
panelType: PANEL_TYPES.TABLE,
|
||||
setSpec,
|
||||
@@ -169,7 +171,7 @@ describe('usePanelKindChange', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -200,7 +202,7 @@ describe('usePanelKindChange', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -236,7 +238,7 @@ describe('usePanelKindChange', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -277,7 +279,7 @@ describe('usePanelKindChange', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.HEATMAP,
|
||||
setSpec,
|
||||
@@ -318,7 +320,7 @@ describe('usePanelKindChange', () => {
|
||||
} as unknown as Query);
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/HeatmapPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.HEATMAP,
|
||||
setSpec,
|
||||
@@ -336,7 +338,7 @@ describe('usePanelKindChange', () => {
|
||||
mockUseQueryBuilder.mockReturnValue(builderState(promQuery));
|
||||
|
||||
const { result } = renderHook(() =>
|
||||
usePanelKindChange({
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: makeSpec('signoz/TimeSeriesPanel', {}, TABLE_QUERIES),
|
||||
panelType: PANEL_TYPES.TIME_SERIES,
|
||||
setSpec,
|
||||
@@ -345,7 +347,7 @@ describe('usePanelKindChange', () => {
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
|
||||
// The hook asks the guard to resolve the active query type against the new kind…
|
||||
expect(mockResolveQueryType).toHaveBeenCalledWith(
|
||||
expect(mockResolveQueryMode).toHaveBeenCalledWith(
|
||||
'signoz/ListPanel',
|
||||
'promql',
|
||||
);
|
||||
@@ -354,128 +356,211 @@ describe('usePanelKindChange', () => {
|
||||
expect((queryArg as Query).queryType).toBe('builder');
|
||||
});
|
||||
|
||||
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];
|
||||
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');
|
||||
|
||||
beforeEach(() => {
|
||||
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);
|
||||
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 }) =>
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
// Leave Table on AI; on List the user moves to Query Builder, then returns.
|
||||
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');
|
||||
act(() => result.current.onChangePanelKind('signoz/ListPanel'));
|
||||
state = builderState(withBothSlots('list', 'builder'));
|
||||
mockResolveQueryType.mockReturnValue('builder');
|
||||
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' },
|
||||
]),
|
||||
);
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
|
||||
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' }]),
|
||||
);
|
||||
mockUseQueryBuilder.mockImplementation(() => state);
|
||||
it('swaps in the Query Builder query when revisiting a kind left on AI', () => {
|
||||
const { result, rerender, setSpec, setState } = leaveTableOnAI();
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
// On List the user is back on Query Builder, then returns to Table.
|
||||
const state = setState(
|
||||
fakeQuery('list-qb', 'builder', [{ dataSource: 'logs' }]),
|
||||
);
|
||||
// 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');
|
||||
mockResolveQueryMode.mockReturnValue('builder');
|
||||
rerender({ spec: listSpec, panelType: PANEL_TYPES.LIST });
|
||||
act(() => result.current.onChangePanelKind('signoz/TablePanel'));
|
||||
|
||||
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);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -488,7 +573,7 @@ describe('usePanelKindChange', () => {
|
||||
|
||||
const { result, rerender } = renderHook(
|
||||
(props: { spec: DashboardtypesPanelSpecDTO; panelType: PANEL_TYPES }) =>
|
||||
usePanelKindChange({ ...props, setSpec }),
|
||||
usePanelKindAndQueryModeSwitch({ ...props, setSpec }),
|
||||
{ initialProps: { spec: tableSpec, panelType: PANEL_TYPES.TABLE } },
|
||||
);
|
||||
|
||||
@@ -9,7 +9,6 @@ import { initialQueriesMap, PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { getIsQueryModified } from 'lib/query/panelQuery';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useShareBuilderUrl } from 'hooks/queryBuilder/useShareBuilderUrl';
|
||||
import { getActiveBuilder } from 'lib/query/builderSlot';
|
||||
import { isEqual } from 'lodash-es';
|
||||
import type { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
@@ -112,10 +111,9 @@ export function usePanelEditorQuerySync({
|
||||
|
||||
// Re-commit on a query-type/datasource switch so the preview refetches. Skip
|
||||
// mount: the initial query is synced into the draft by the staged-query effect below.
|
||||
const activeQueryData = getActiveBuilder(currentQuery).queryData;
|
||||
const dataSources = useMemo(
|
||||
() => (activeQueryData ?? []).map((q) => q.dataSource),
|
||||
[activeQueryData],
|
||||
() => (currentQuery.builder?.queryData ?? []).map((q) => q.dataSource),
|
||||
[currentQuery.builder],
|
||||
);
|
||||
const dataSourceSignature = dataSources.join(',');
|
||||
const prevDataSourcesRef = useRef(dataSources);
|
||||
|
||||
@@ -11,16 +11,27 @@ 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 {
|
||||
OrderByPayload,
|
||||
Query,
|
||||
import {
|
||||
type BuilderVariant,
|
||||
type BuildersByVariant,
|
||||
type OrderByPayload,
|
||||
type Query,
|
||||
QueryMode,
|
||||
} from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
|
||||
import {
|
||||
getQueryPanelDefinition,
|
||||
isStaticPanelKind,
|
||||
resolveQueryType,
|
||||
resolveQueryMode,
|
||||
} 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';
|
||||
@@ -32,6 +43,7 @@ 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[] = [
|
||||
@@ -57,17 +69,21 @@ interface KindState {
|
||||
pluginSpec: DashboardtypesPanelPluginDTO['spec'];
|
||||
queries: DashboardtypesQueryDTO[];
|
||||
query: Query;
|
||||
/** Both builder variants' queries as the kind was left. */
|
||||
builders: BuildersByVariant;
|
||||
}
|
||||
|
||||
interface UsePanelKindChangeArgs {
|
||||
interface UsePanelKindAndQueryModeSwitchArgs {
|
||||
spec: DashboardtypesPanelSpecDTO;
|
||||
panelType: PANEL_TYPES;
|
||||
setSpec: (next: DashboardtypesPanelSpecDTO) => void;
|
||||
}
|
||||
|
||||
interface UsePanelKindChangeApi {
|
||||
interface UsePanelKindAndQueryModeSwitchApi {
|
||||
/** 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;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,15 +92,22 @@ interface UsePanelKindChangeApi {
|
||||
* 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 usePanelKindChange({
|
||||
export function usePanelKindAndQueryModeSwitch({
|
||||
spec,
|
||||
panelType,
|
||||
setSpec,
|
||||
}: UsePanelKindChangeArgs): UsePanelKindChangeApi {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
}: UsePanelKindAndQueryModeSwitchArgs): UsePanelKindAndQueryModeSwitchApi {
|
||||
const {
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
updateAllQueriesOperators,
|
||||
} = 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);
|
||||
@@ -94,6 +117,14 @@ export function usePanelKindChange({
|
||||
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;
|
||||
@@ -111,9 +142,20 @@ export function usePanelKindChange({
|
||||
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
|
||||
@@ -130,12 +172,10 @@ export function usePanelKindChange({
|
||||
} as DashboardtypesPanelPluginDTO,
|
||||
queries,
|
||||
});
|
||||
|
||||
const queryType = resolveQueryType(newKind, query.queryType);
|
||||
const rebuildForNewKind = (builder: Query['builder']): Query['builder'] => {
|
||||
const rebuildForNewKind = (source: Query): Query => {
|
||||
const transformed = handleQueryChange(
|
||||
newPanelType as keyof PartialPanelTypes,
|
||||
{ ...query, queryType: EQueryType.QUERY_BUILDER, builder },
|
||||
{ ...source, queryType: targetQueryType },
|
||||
panelTypeRef.current,
|
||||
);
|
||||
// Match a fresh list panel's default order so the builder's Order By isn't empty.
|
||||
@@ -143,41 +183,62 @@ export function usePanelKindChange({
|
||||
newKind === 'signoz/ListPanel'
|
||||
? withDefaultListOrder(transformed)
|
||||
: transformed;
|
||||
return (
|
||||
newKind === 'signoz/HeatmapPanel'
|
||||
? withSingleHeatmapQuery(ordered)
|
||||
: withPercentileHistogramAggregation(ordered)
|
||||
).builder;
|
||||
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 };
|
||||
};
|
||||
|
||||
// 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.
|
||||
// Revisit → restore the stash in the active mode; a static kind has no builder to re-seed.
|
||||
const cached = cacheRef.current.get(newKind);
|
||||
if (cached) {
|
||||
if (isStaticPanelKind(newKind)) {
|
||||
setSpec(buildSpec(cached.pluginSpec, cached.queries));
|
||||
return;
|
||||
}
|
||||
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.
|
||||
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.
|
||||
setSpec(
|
||||
buildSpec(
|
||||
cached.pluginSpec,
|
||||
queryType === cached.query.queryType
|
||||
restored.query.builder === cached.query.builder
|
||||
? cached.queries
|
||||
: toPerses(restored, newPanelType),
|
||||
: toPerses(restored.query, newPanelType),
|
||||
),
|
||||
);
|
||||
redirectWithQueryBuilderData(restored);
|
||||
redirectWithQueryBuilderData({
|
||||
...restored.query,
|
||||
queryType: targetQueryType,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -191,15 +252,25 @@ export function usePanelKindChange({
|
||||
return;
|
||||
}
|
||||
|
||||
// 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),
|
||||
}),
|
||||
};
|
||||
// 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;
|
||||
const signal = getBuilderQueries(currentSpec.queries)[0]
|
||||
?.signal as TelemetrytypesSignalDTO;
|
||||
|
||||
@@ -211,8 +282,8 @@ export function usePanelKindChange({
|
||||
);
|
||||
redirectWithQueryBuilderData(nextQuery);
|
||||
},
|
||||
[setSpec, redirectWithQueryBuilderData],
|
||||
[setSpec, redirectWithQueryBuilderData, updateAllQueriesOperators],
|
||||
);
|
||||
|
||||
return { onChangePanelKind };
|
||||
return { onChangePanelKind, onChangeQueryMode };
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
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 { usePanelKindChange } from './hooks/usePanelKindChange';
|
||||
import { usePanelKindAndQueryModeSwitch } from './hooks/usePanelKindAndQueryModeSwitch';
|
||||
|
||||
export interface PanelEditorContainerProps {
|
||||
dashboardId: string;
|
||||
@@ -40,11 +40,12 @@ function PanelEditorContainer(props: PanelEditorContainerProps): JSX.Element {
|
||||
const panelDefinition = getPanelDefinition(panelKind);
|
||||
const originalPanelKind = (savedPanel ?? panel).spec.plugin.kind;
|
||||
|
||||
const { onChangePanelKind } = usePanelKindChange({
|
||||
spec: draftApi.draft.spec,
|
||||
panelType: toPanelType(panelKind),
|
||||
setSpec: draftApi.setSpec,
|
||||
});
|
||||
const { onChangePanelKind, onChangeQueryMode } =
|
||||
usePanelKindAndQueryModeSwitch({
|
||||
spec: draftApi.draft.spec,
|
||||
panelType: toPanelType(panelKind),
|
||||
setSpec: draftApi.setSpec,
|
||||
});
|
||||
|
||||
if (panelDefinition.mode === 'static') {
|
||||
return (
|
||||
@@ -65,6 +66,7 @@ function PanelEditorContainer(props: PanelEditorContainerProps): JSX.Element {
|
||||
panelDefinition={panelDefinition}
|
||||
onChangePanelKind={onChangePanelKind}
|
||||
originalPanelKind={originalPanelKind}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ 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';
|
||||
@@ -11,12 +12,13 @@ import { NO_PANEL_ACTIONS } from '../types/panelDefinition';
|
||||
import {
|
||||
getQueryPanelDefinition,
|
||||
requireQueryPanelDefinition,
|
||||
getSupportedQueryModes,
|
||||
getSupportedQueryTypes,
|
||||
getSupportedSignals,
|
||||
isPanelCombinationValid,
|
||||
isQueryTypeSupportedByPanelKind,
|
||||
isSignalSupported,
|
||||
resolveQueryType,
|
||||
resolveQueryMode,
|
||||
} from '../capabilities';
|
||||
import type { PanelKind } from '../types/panelKind';
|
||||
|
||||
@@ -25,44 +27,59 @@ const { logs, traces, metrics } = TelemetrytypesSignalDTO;
|
||||
const { time_series, scalar, raw, heatmap } = Querybuildertypesv5RequestTypeDTO;
|
||||
|
||||
const EXPECTED_QUERY_TYPES: Record<PanelKind, EQueryType[]> = {
|
||||
'signoz/TimeSeriesPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/BarChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/AreaChartPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/NumberPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HistogramPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/ListPanel': [QUERY_BUILDER],
|
||||
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
// Static kind: no query surface at all.
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
|
||||
const EXPECTED_QUERY_MODES: Record<PanelKind, QueryMode[]> = {
|
||||
'signoz/TimeSeriesPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/BarChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/AreaChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/NumberPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HistogramPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
PROM,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/HeatmapPanel': [QUERY_BUILDER, CLICKHOUSE, PROM],
|
||||
'signoz/PieChartPanel': [
|
||||
QUERY_BUILDER,
|
||||
CLICKHOUSE,
|
||||
EQueryType.AI_QUERY_BUILDER,
|
||||
QueryMode.AI_QUERY_BUILDER,
|
||||
],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, EQueryType.AI_QUERY_BUILDER],
|
||||
'signoz/TablePanel': [QUERY_BUILDER, CLICKHOUSE, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/ListPanel': [QUERY_BUILDER, QueryMode.AI_QUERY_BUILDER],
|
||||
'signoz/ScatterPlotPanel': [QUERY_BUILDER, CLICKHOUSE],
|
||||
'signoz/TextPanel': [],
|
||||
};
|
||||
@@ -264,10 +281,10 @@ describe('panel capabilities guard', () => {
|
||||
|
||||
it('reads a specific mode when one is given', () => {
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toStrictEqual([traces]);
|
||||
expect(
|
||||
getSupportedSignals('signoz/TablePanel', EQueryType.CLICKHOUSE),
|
||||
getSupportedSignals('signoz/TablePanel', QueryMode.CLICKHOUSE),
|
||||
).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
@@ -318,21 +335,36 @@ describe('panel capabilities guard', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('resolveQueryType', () => {
|
||||
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', () => {
|
||||
it('keeps a supported mode', () => {
|
||||
expect(resolveQueryType('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(resolveQueryMode('signoz/TimeSeriesPanel', PROM)).toBe(PROM);
|
||||
expect(
|
||||
resolveQueryType('signoz/TablePanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
resolveQueryMode('signoz/TablePanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.AI_QUERY_BUILDER);
|
||||
expect(
|
||||
resolveQueryType('signoz/ListPanel', EQueryType.AI_QUERY_BUILDER),
|
||||
).toBe(EQueryType.AI_QUERY_BUILDER);
|
||||
resolveQueryMode('signoz/ListPanel', QueryMode.AI_QUERY_BUILDER),
|
||||
).toBe(QueryMode.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(resolveQueryType('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryType('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryMode('signoz/ListPanel', PROM)).toBe(QUERY_BUILDER);
|
||||
expect(resolveQueryMode('signoz/TablePanel', PROM)).toBe(QUERY_BUILDER);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
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 { getQueryTypeSignals } from './types/panelCapabilities';
|
||||
import { getQueryModeSignals } from './types/panelCapabilities';
|
||||
import type { RenderableQueryPanelDefinition } from './types/panelDefinition';
|
||||
import type { PanelKind } from './types/panelKind';
|
||||
|
||||
@@ -49,16 +50,16 @@ export function requireQueryPanelDefinition(
|
||||
return definition;
|
||||
}
|
||||
|
||||
const DEFAULT_QUERY_TYPE = EQueryType.QUERY_BUILDER;
|
||||
const DEFAULT_QUERY_MODE = QueryMode.QUERY_BUILDER;
|
||||
|
||||
/** Signals a kind can visualize in `queryType`. */
|
||||
/** Signals a kind can visualize in `mode`. */
|
||||
export function getSupportedSignals(
|
||||
kind: PanelKind,
|
||||
queryType: EQueryType = DEFAULT_QUERY_TYPE,
|
||||
mode: QueryMode = DEFAULT_QUERY_MODE,
|
||||
): TelemetrytypesSignalDTO[] {
|
||||
return getQueryTypeSignals(
|
||||
getQueryPanelDefinition(kind)?.supportedQueryTypesKindMap,
|
||||
queryType,
|
||||
return getQueryModeSignals(
|
||||
getQueryPanelDefinition(kind)?.supportedQueryModesKindMap,
|
||||
mode,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -69,11 +70,16 @@ export function isSignalSupported(
|
||||
return getSupportedSignals(kind).includes(signal);
|
||||
}
|
||||
|
||||
/** Query types a kind supports, in tab order. */
|
||||
/** 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). */
|
||||
export function getSupportedQueryTypes(kind: PanelKind): EQueryType[] {
|
||||
const queryTypes =
|
||||
getQueryPanelDefinition(kind)?.supportedQueryTypesKindMap ?? {};
|
||||
return Object.keys(queryTypes) as EQueryType[];
|
||||
return getSupportedQueryModes(kind).filter(
|
||||
(mode): mode is EQueryType => mode !== QueryMode.AI_QUERY_BUILDER,
|
||||
);
|
||||
}
|
||||
|
||||
export function isQueryTypeSupportedByPanelKind(
|
||||
@@ -110,15 +116,15 @@ export function isPanelCombinationValid({
|
||||
return true;
|
||||
}
|
||||
|
||||
/** `preferred` if the kind supports it, else its first supported query type (e.g. PromQL → List gives Query Builder). */
|
||||
export function resolveQueryType(
|
||||
/** `preferred` if the kind supports it, else its first supported mode (e.g. PromQL → List gives Query Builder). */
|
||||
export function resolveQueryMode(
|
||||
kind: PanelKind,
|
||||
preferred: EQueryType,
|
||||
): EQueryType {
|
||||
const supported = getSupportedQueryTypes(kind);
|
||||
preferred: QueryMode,
|
||||
): QueryMode {
|
||||
const supported = getSupportedQueryModes(kind);
|
||||
if (supported.includes(preferred)) {
|
||||
return preferred;
|
||||
}
|
||||
// A query-less kind has no supported query types; the builder is the neutral answer.
|
||||
return supported[0] ?? DEFAULT_QUERY_TYPE;
|
||||
// A query-less kind has no supported modes; the builder is the neutral answer.
|
||||
return supported[0] ?? DEFAULT_QUERY_MODE;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { ChartArea } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -10,6 +9,7 @@ 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,
|
||||
supportedQueryTypesKindMap: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
supportedQueryModesKindMap: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [
|
||||
TelemetrytypesSignalDTO.metrics,
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.traces],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -10,6 +9,7 @@ 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,
|
||||
supportedQueryTypesKindMap: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
supportedQueryModesKindMap: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [
|
||||
TelemetrytypesSignalDTO.metrics,
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.traces],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
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 { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -12,6 +11,7 @@ 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,
|
||||
supportedQueryTypesKindMap: {
|
||||
supportedQueryModesKindMap: {
|
||||
// Only metrics carry a bucket axis; the request rejects the other signals.
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.metrics],
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
},
|
||||
// The request rejects both: a point is a count per bucket, not a single value.
|
||||
queryBuilderFields: {
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { BarChart } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import QueryBuilderEditorPane from 'pages/DashboardPage/DashboardContainer/PanelEditor/PanelEditorQueryBuilder/QueryBuilderEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -10,6 +9,7 @@ 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,
|
||||
supportedQueryTypesKindMap: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
supportedQueryModesKindMap: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [
|
||||
TelemetrytypesSignalDTO.metrics,
|
||||
TelemetrytypesSignalDTO.logs,
|
||||
TelemetrytypesSignalDTO.traces,
|
||||
],
|
||||
},
|
||||
[EQueryType.CLICKHOUSE]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.PROM]: { kind: QueryTypeKind.SIGNAL_LESS },
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
[QueryMode.CLICKHOUSE]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.PROM]: { kind: QueryModeKind.SIGNAL_LESS },
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.traces],
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,6 +10,7 @@ function ListEditorPane({
|
||||
panelDefinition,
|
||||
signal,
|
||||
isLoadingQueries,
|
||||
onChangeQueryMode,
|
||||
onStageRunQuery,
|
||||
onCancelQuery,
|
||||
stickyHeader,
|
||||
@@ -20,6 +21,7 @@ function ListEditorPane({
|
||||
<PanelEditorQueryBuilder
|
||||
panelDefinition={panelDefinition}
|
||||
isLoadingQueries={isLoadingQueries}
|
||||
onChangeQueryMode={onChangeQueryMode}
|
||||
onStageRunQuery={onStageRunQuery}
|
||||
onCancelQuery={onCancelQuery}
|
||||
stickyHeader={stickyHeader}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { EQueryType } from 'types/common/dashboard';
|
||||
import { List } from '@signozhq/icons';
|
||||
|
||||
import type { PanelDefinition } from '../../types/panelDefinition';
|
||||
import { QueryTypeKind } from '../../types/panelCapabilities';
|
||||
import { QueryModeKind } from '../../types/panelCapabilities';
|
||||
import ListEditorPane from './ListEditorPane';
|
||||
import Renderer from './Renderer';
|
||||
import { sections } from './sections';
|
||||
@@ -10,6 +9,7 @@ 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.
|
||||
supportedQueryTypesKindMap: {
|
||||
[EQueryType.QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
supportedQueryModesKindMap: {
|
||||
[QueryMode.QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.logs, TelemetrytypesSignalDTO.traces],
|
||||
},
|
||||
[EQueryType.AI_QUERY_BUILDER]: {
|
||||
kind: QueryTypeKind.SIGNAL,
|
||||
[QueryMode.AI_QUERY_BUILDER]: {
|
||||
kind: QueryModeKind.SIGNAL,
|
||||
signals: [TelemetrytypesSignalDTO.traces],
|
||||
},
|
||||
},
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user