Compare commits

...

4 Commits

20 changed files with 370 additions and 358 deletions

View File

@@ -4,10 +4,17 @@ import { AxiosError } from 'axios';
import { ErrorV2Resp, SuccessResponseV2 } from 'types/api';
import { Pipeline } from 'types/api/pipeline/def';
import { Props } from 'types/api/pipeline/post';
import { toLegacyTagFilter } from 'utils/fieldContext';
const post = async (props: Props): Promise<SuccessResponseV2<Pipeline>> => {
try {
const response = await axios.post('/logs/pipelines', props.data);
const response = await axios.post('/logs/pipelines', {
...props.data,
pipelines: props.data.pipelines.map((pipeline) => ({
...pipeline,
filter: toLegacyTagFilter(pipeline.filter),
})),
});
return {
httpStatusCode: response.status,

View File

@@ -1,6 +1,7 @@
import axios from 'api';
import { ILog } from 'types/api/logs/log';
import { PipelineData } from 'types/api/pipeline/def';
import { toLegacyTagFilter } from 'utils/fieldContext';
export interface PipelineSimulationRequest {
logs: ILog[];
@@ -15,7 +16,13 @@ const simulatePipelineProcessing = async (
requestBody: PipelineSimulationRequest,
): Promise<PipelineSimulationResponse> =>
axios
.post('/logs/pipelines/preview', requestBody)
.post('/logs/pipelines/preview', {
...requestBody,
pipelines: requestBody.pipelines.map((pipeline) => ({
...pipeline,
filter: toLegacyTagFilter(pipeline.filter),
})),
})
.then((res) => res.data.data);
export default simulatePipelineProcessing;

View File

@@ -130,7 +130,7 @@ function CeleryOverviewConfigOptions(): JSX.Element {
{
placeholder: 'Service Name',
queryParam: QueryParams.service,
filterType: 'serviceName',
filterType: 'service.name',
},
{
placeholder: 'Span Name',

View File

@@ -2,22 +2,22 @@ import { useQuery } from 'react-query';
// eslint-disable-next-line no-restricted-imports
import { useSelector } from 'react-redux';
import type { DefaultOptionType } from 'antd/es/select';
import { getAttributesValues } from 'api/queryBuilder/getAttributesValues';
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
import { getFieldValueSuggestions } from 'api/querySuggestions/getFieldValueSuggestions';
import { FIELD_API_CACHE_TIME } from 'constants/queryCacheTime';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { toFieldValues } from 'hooks/querySuggestions/useFieldValuesSuggestion';
import { AppState } from 'store/reducers';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { DataSource } from 'types/common/queryBuilder';
import { DATA_SOURCE_TO_SIGNAL, DataSource } from 'types/common/queryBuilder';
import { GlobalReducer } from 'types/reducer/globalTime';
import { getValuesForDataType } from 'utils/fieldValues';
export interface Filters {
searchText: string;
attributeKey: string | string[];
aggregateOperator?: string;
dataSource?: DataSource;
aggregateAttribute?: string;
metricName?: string;
filterAttributeKeyDataType?: DataTypes;
tagType?: string;
}
export interface GetAllFiltersResponse {
@@ -29,11 +29,9 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const {
searchText,
attributeKey,
aggregateOperator,
dataSource,
aggregateAttribute,
metricName,
filterAttributeKeyDataType,
tagType,
} = props;
const { minTime, maxTime } = useSelector<AppState, GlobalReducer>(
@@ -43,6 +41,8 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const { data, isLoading } = useQuery(
[
REACT_QUERY_KEY.GET_ATTRIBUTE_VALUES,
dataSource,
metricName,
attributeKey,
searchText,
minTime,
@@ -53,30 +53,20 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
const responses = await Promise.all(
keys.map((key) =>
getAttributesValues({
aggregateOperator: aggregateOperator || 'noop',
dataSource: dataSource || DataSource.TRACES,
aggregateAttribute: aggregateAttribute || '',
attributeKey: key,
getFieldValueSuggestions({
signal: DATA_SOURCE_TO_SIGNAL[dataSource || DataSource.TRACES],
name: key,
searchText: searchText ?? '',
filterAttributeKeyDataType:
filterAttributeKeyDataType || DataTypes.String,
tagType: tagType || 'tag',
metricName,
}),
),
);
const uniqueValues = [
...new Set(
responses.flatMap(({ payload }) => {
if (!payload) {
return [];
}
const dataType = filterAttributeKeyDataType || DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
return key ? payload[key] || [] : [];
}),
responses.flatMap((response) =>
getValuesForDataType(toFieldValues(response), filterAttributeKeyDataType),
),
),
];
@@ -85,6 +75,7 @@ export function useGetAllFilters(props: Filters): GetAllFiltersResponse {
value: val,
}));
},
{ staleTime: FIELD_API_CACHE_TIME },
);
return { options: data ?? [], isFetching: isLoading };

View File

@@ -73,11 +73,9 @@ export default function CeleryTaskGraphGrid({
const { options } = useCeleryFilterOptions(
'worker',
'rate',
DataSource.METRICS,
'flower_task_runtime_seconds_sum',
DataTypes.String,
'tag',
);
const isDarkMode = useIsDarkMode();

View File

@@ -8,11 +8,9 @@ import { useGetAllFilters } from './CeleryTaskConfigOptions/useGetCeleryFilters'
export const useCeleryFilterOptions = (
type: string | string[],
aggregateOperator?: string,
dataSource?: DataSource,
aggregateAttribute?: string,
metricName?: string,
filterAttributeKeyDataType?: DataTypes,
tagType?: string,
): {
searchText: string;
handleSearch: (value: string) => void;
@@ -23,11 +21,9 @@ export const useCeleryFilterOptions = (
const { isFetching, options } = useGetAllFilters({
attributeKey: type,
searchText,
aggregateOperator,
dataSource,
aggregateAttribute,
metricName,
filterAttributeKeyDataType,
tagType,
});
const handleDebouncedSearch = useDebouncedFn((searchText): void => {
setSearchText(searchText as string);

View File

@@ -1,6 +1,6 @@
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { TelemetryFieldKey } from 'types/api/v5/queryRange';
import { fieldContextToAttributeType } from 'utils/fieldContext';
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
import { FiltersType, IQuickFiltersConfig, SignalType } from './types';
@@ -15,19 +15,6 @@ const FILTER_TYPE_MAP: Record<string, FiltersType> = {
duration_nano: FiltersType.DURATION,
};
// The map below exists only for the old v3 attribute-values fetch
// (useCheckboxFilterValues), the sole reader of attributeKey.dataType/type.
// Once the values fetch moves to fields/values, remove this and reduce
// attributeKey to { id, key }.
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
[TelemetrytypesFieldContextDTO.resource]: 'resource',
};
const mapFieldContext = (fieldContext?: string): string =>
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
const getFilterName = (str: string): string => {
if (FILTER_TITLE_MAP[str]) {
return FILTER_TITLE_MAP[str];
@@ -64,11 +51,15 @@ export const getFilterConfig = (
type: getFilterType(att),
title: getFilterName(att.name),
dataSource: SIGNAL_DATA_SOURCE_MAP[signal],
// The type mapping below exists only for the old v3 attribute-values fetch
// (useCheckboxFilterValues), the sole reader of attributeKey.dataType/type.
// Once the values fetch moves to fields/values, remove this and reduce
// attributeKey to { id, key }.
attributeKey: {
id: att.name,
key: att.name,
dataType: fieldDataTypeToDataType(att.fieldDataType),
type: mapFieldContext(att.fieldContext),
type: fieldContextToAttributeType(att.fieldContext),
},
defaultOpen: index < 2,
}) as IQuickFiltersConfig,

View File

@@ -10,7 +10,6 @@ import {
userEvent,
waitFor,
} from 'tests/test-utils';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { pipelineApiResponseMockData } from '../mocks/pipeline';
import PipelineListsView from '../PipelineListsView';
@@ -85,7 +84,7 @@ jest.mock('providers/preferences/sync/usePreferenceSync', () => ({
}));
const BASE_URL = ENVIRONMENT.baseURL;
const attributeKeysURL = `${BASE_URL}/api/v3/filter_suggestions`;
const fieldKeysURL = `${BASE_URL}/api/v1/fields/keys`;
describe('PipelinePage container test', () => {
beforeAll(() => {
@@ -339,42 +338,41 @@ describe('PipelinePage container test', () => {
expect(screen.queryAllByText('source = nginx')).toHaveLength(2);
server.use(
rest.get(attributeKeysURL, (_req, res, ctx) =>
rest.get(fieldKeysURL, (_req, res, ctx) =>
res(
ctx.status(200),
ctx.json({
status: 'success',
data: {
attributes: [
{
key: 'otelServiceName',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.instance.id',
dataType: DataTypes.String,
type: 'resource',
isColumn: false,
isJSON: false,
},
{
key: 'service.name',
dataType: DataTypes.String,
type: 'tag',
isColumn: false,
isJSON: false,
},
],
complete: true,
keys: {
otelServiceName: [
{
name: 'otelServiceName',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
'service.name': [
{
name: 'service.name',
fieldContext: 'resource',
fieldDataType: 'string',
},
{
name: 'service.name',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
'service.instance.id': [
{
name: 'service.instance.id',
fieldContext: 'resource',
fieldDataType: 'string',
},
],
},
},
}),
),

View File

@@ -12,7 +12,6 @@ import { Select, Spin } from 'antd';
import { Pill } from '@signozhq/ui/pill';
import cx from 'classnames';
import {
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
OperatorConfigKeys,
OPERATORS,
QUERY_BUILDER_OPERATORS_BY_TYPES,
@@ -22,23 +21,14 @@ import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
import { LogsExplorerShortcuts } from 'constants/shortcuts/logsExplorerShortcuts';
import { useDynamicVariableSuggestions } from 'hooks/dashboard/useDynamicVariableSuggestions';
import { useKeyboardHotkeys } from 'hooks/hotkeys/useKeyboardHotkeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { validationMapper } from 'hooks/queryBuilder/useIsValidTag';
import { operatorTypeMapper } from 'hooks/queryBuilder/useOperatorType';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import { useRunQueryWithFilterItems } from 'hooks/queryBuilder/useRunQueryWithFilterItems';
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
import { useFieldValuesSuggestion } from 'hooks/querySuggestions/useFieldValuesSuggestion';
import useDebounceValue from 'hooks/useDebounce';
import {
cloneDeep,
isArray,
isEmpty,
isEqual,
isObject,
isUndefined,
unset,
} from 'lodash-es';
import { isArray, isEmpty, isEqual, isObject, isUndefined } from 'lodash-es';
import { ChevronDown, ChevronUp } from '@signozhq/icons';
import type { BaseSelectRef } from 'rc-select';
import {
@@ -49,7 +39,13 @@ import {
IBuilderQuery,
TagFilter,
} from 'types/api/queryBuilder/queryBuilderData';
import { DataSource } from 'types/common/queryBuilder';
import {
DATA_SOURCE_TO_SIGNAL,
DataSource,
QUERY_SOURCE_TO_FIELD_SOURCE,
} from 'types/common/queryBuilder';
import { attributeTypeToFieldContext } from 'utils/fieldContext';
import { getValuesForDataType } from 'utils/fieldValues';
import { useSelectPopupContainer } from 'utils/selectPopupContainer';
import { v4 as uuid } from 'uuid';
@@ -62,7 +58,8 @@ import {
getTagToken,
isInNInOperator,
} from './utils';
import { filterByOperatorConfig } from '../utils';
import { toAttributeKey } from '../GroupByFilter/utils';
import { filterByOperatorConfig, getMetricNameForQuery } from '../utils';
import QueryBuilderSearchDropdown from './QueryBuilderSearchDropdown';
import SpanScopeSelector from './SpanScopeSelector';
import Suggestions from './Suggestions';
@@ -187,70 +184,15 @@ function QueryBuilderSearchV2(
[query.dataSource],
);
const memoizedSearchParams = useMemo(
() => [
searchValue,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute?.key,
],
[
searchValue,
query.dataSource,
query.aggregateOperator,
query.aggregateAttribute?.key,
],
);
const queryFiltersWithoutId = useMemo(
() => ({
...query.filters,
items: query.filters?.items.map((item) => {
const filterWithoutId = cloneDeep(item);
unset(filterWithoutId, 'id');
return filterWithoutId;
}),
}),
[query.filters],
);
const memoizedSuggestionsParams = useMemo(
() => [searchValue, query.dataSource, queryFiltersWithoutId],
[query.dataSource, queryFiltersWithoutId, searchValue],
);
const memoizedValueParams = useMemo(
() => [
query.aggregateOperator,
query.dataSource,
query.aggregateAttribute?.key,
currentFilterItem?.key?.key || '',
currentFilterItem?.key?.dataType,
currentFilterItem?.key?.type ?? '',
isArray(currentFilterItem?.value)
? currentFilterItem?.value?.[currentFilterItem.value.length - 1]
: currentFilterItem?.value,
],
[
query.aggregateOperator,
query.dataSource,
query.aggregateAttribute?.key,
currentFilterItem?.key?.key,
currentFilterItem?.key?.dataType,
currentFilterItem?.key?.type,
currentFilterItem?.value,
],
);
const searchParams = useDebounceValue(memoizedSearchParams, DEBOUNCE_DELAY);
const valueParams = useDebounceValue(memoizedValueParams, DEBOUNCE_DELAY);
const suggestionsParams = useDebounceValue(
memoizedSuggestionsParams,
const debouncedKeySearchText = useDebounceValue(
searchValue?.split(' ')[0] ?? '',
DEBOUNCE_DELAY,
);
const valueSearchText = isArray(currentFilterItem?.value)
? String(currentFilterItem?.value?.[currentFilterItem.value.length - 1] ?? '')
: currentFilterItem?.value?.toString() || '';
const isQueryEnabled = useMemo(() => {
if (currentState === DropdownState.ATTRIBUTE_KEY) {
return query.dataSource === DataSource.METRICS
@@ -263,53 +205,58 @@ function QueryBuilderSearchV2(
const dashboardDynamicVariables = useDynamicVariableSuggestions();
const { data, isFetching } = useGetAggregateKeys(
const signal = DATA_SOURCE_TO_SIGNAL[query.dataSource];
const source = QUERY_SOURCE_TO_FIELD_SOURCE[query.source ?? ''];
const metricName = getMetricNameForQuery(query);
const { data: fieldKeys, isFetching } = useFieldKeysSuggestion(
{
searchText: searchValue?.split(' ')[0],
dataSource: query.dataSource,
aggregateOperator: query.aggregateOperator || '',
aggregateAttribute: query.aggregateAttribute?.key || '',
tagType: query.aggregateAttribute?.type ?? null,
signal,
source,
metricName,
fieldContext: attributeTypeToFieldContext(query.aggregateAttribute?.type),
searchText: debouncedKeySearchText,
},
query.builderQueryType,
{
queryKey: [searchParams],
enabled: isQueryEnabled && !isLogsDataSource && !hardcodedAttributeKeys,
enabled: isQueryEnabled && !hardcodedAttributeKeys,
keepPreviousData: true,
},
);
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
useGetAttributeSuggestions(
{
searchText: searchValue?.split(' ')[0],
dataSource: query.dataSource,
filters: query.filters || { items: [], op: 'AND' },
},
{
queryKey: [suggestionsParams],
enabled: isQueryEnabled && isLogsDataSource,
},
);
const attributeKeys = useMemo(
() => (fieldKeys ?? []).map(toAttributeKey),
[fieldKeys],
);
const { data: attributeValues, isFetching: isFetchingAttributeValues } =
useGetAggregateValues(
{
aggregateOperator: query.aggregateOperator || '',
dataSource: query.dataSource,
aggregateAttribute: query.aggregateAttribute?.key || '',
attributeKey: currentFilterItem?.key?.key || '',
filterAttributeKeyDataType:
currentFilterItem?.key?.dataType ?? DataTypes.EMPTY,
tagType: currentFilterItem?.key?.type ?? '',
searchText: isArray(currentFilterItem?.value)
? String(currentFilterItem?.value?.[currentFilterItem.value.length - 1]) ||
''
: currentFilterItem?.value?.toString() || '',
},
{
enabled: currentState === DropdownState.ATTRIBUTE_VALUE,
queryKey: [valueParams],
},
);
const fieldValuesConfig = useMemo(
() => ({
signal,
source,
metricName,
name: currentFilterItem?.key?.key || '',
fieldContext: attributeTypeToFieldContext(currentFilterItem?.key?.type),
searchText: valueSearchText,
}),
[
signal,
source,
metricName,
currentFilterItem?.key?.key,
currentFilterItem?.key?.type,
valueSearchText,
],
);
const debouncedFieldValuesConfig = useDebounceValue(
fieldValuesConfig,
DEBOUNCE_DELAY,
);
const { data: fieldValues, isFetching: isFetchingAttributeValues } =
useFieldValuesSuggestion(debouncedFieldValuesConfig, query.builderQueryType, {
enabled: currentState === DropdownState.ATTRIBUTE_VALUE,
});
const handleDropdownSelect = useCallback(
(value: string) => {
@@ -534,15 +481,13 @@ function QueryBuilderSearchV2(
if (tagKey && isUndefined(currentFilterItem?.key)) {
let currentRunningAttributeKey;
const isSuggestedKeyInAutocomplete =
suggestionsData?.payload?.attributes?.some(
(value) => value.key === tagKey.split(' ')[0],
);
isLogsDataSource &&
attributeKeys.some((value) => value.key === tagKey.split(' ')[0]);
if (isSuggestedKeyInAutocomplete) {
const allAttributesMatchingTheKey =
suggestionsData?.payload?.attributes?.filter(
(value) => value.key === tagKey.split(' ')[0],
) || [];
const allAttributesMatchingTheKey = attributeKeys.filter(
(value) => value.key === tagKey.split(' ')[0],
);
if (allAttributesMatchingTheKey?.length === 1) {
[currentRunningAttributeKey] = allAttributesMatchingTheKey;
@@ -649,9 +594,10 @@ function QueryBuilderSearchV2(
currentFilterItem,
currentFilterItem?.key,
currentFilterItem?.op,
suggestionsData?.payload?.attributes,
attributeKeys,
isLogsDataSource,
searchValue,
isFetchingSuggestions,
isFetching,
currentState,
]);
@@ -663,9 +609,7 @@ function QueryBuilderSearchV2(
// add the user typed option in the dropdown to select that and move ahead irrespective of the matches and all
setDropdownOptions([
...(!isEmpty(tagKey) &&
!suggestionsData?.payload?.attributes?.some((val) =>
isEqual(val.key, tagKey),
)
!attributeKeys.some((val) => isEqual(val.key, tagKey))
? [
{
label: tagKey,
@@ -677,10 +621,10 @@ function QueryBuilderSearchV2(
},
]
: []),
...(suggestionsData?.payload?.attributes?.map((key) => ({
...attributeKeys.map((key) => ({
label: key.key,
value: key,
})) || []),
})),
]);
} else if (hardcodedAttributeKeys) {
const filteredKeys = hardcodedAttributeKeys.filter((key) =>
@@ -698,7 +642,7 @@ function QueryBuilderSearchV2(
setDropdownOptions([
// Add user typed option if it doesn't exist in the payload
...(tagKey.trim().length > 0 &&
!data?.payload?.attributeKeys?.some((val) => val.key === tagKey)
!attributeKeys.some((val) => val.key === tagKey)
? [
{
label: tagKey,
@@ -711,10 +655,10 @@ function QueryBuilderSearchV2(
]
: []),
// Map existing attribute keys from payload
...(data?.payload?.attributeKeys?.map((key) => ({
...attributeKeys.map((key) => ({
label: key.key,
value: key,
})) || []),
})),
]);
}
}
@@ -779,10 +723,10 @@ function QueryBuilderSearchV2(
values.push(tagValue);
}
if (attributeValues?.payload) {
const dataType = currentFilterItem?.key?.dataType || DataTypes.String;
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
values.push(...(attributeValues?.payload?.[key] || []));
if (fieldValues) {
values.push(
...getValuesForDataType(fieldValues, currentFilterItem?.key?.dataType),
);
// here we want to suggest the variable name matching with the key here, we will go over the dynamic variables for the keys
const variableName = dashboardDynamicVariables.find(
@@ -806,13 +750,12 @@ function QueryBuilderSearchV2(
}
}, [
hardcodedAttributeKeys,
attributeValues?.payload,
fieldValues,
currentFilterItem?.key?.dataType,
currentState,
data?.payload?.attributeKeys,
attributeKeys,
isLogsDataSource,
searchValue,
suggestionsData?.payload?.attributes,
operatorConfigKey,
currentFilterItem?.key?.key,
dashboardDynamicVariables,
@@ -881,8 +824,8 @@ function QueryBuilderSearchV2(
}, [deregisterShortcut, isLastQuery, registerShortcut]);
const loading = useMemo(
() => isFetching || isFetchingAttributeValues || isFetchingSuggestions,
[isFetching, isFetchingAttributeValues, isFetchingSuggestions],
() => isFetching || isFetchingAttributeValues,
[isFetching, isFetchingAttributeValues],
);
const isMetricsDataSource = useMemo(
@@ -992,7 +935,7 @@ function QueryBuilderSearchV2(
setTags((prev) => [...prev, ...(val.items as ITag[])]);
}}
searchValue={searchValue}
exampleQueries={suggestionsData?.payload?.example_queries || []}
exampleQueries={[]}
tags={tags}
currentFilterItem={currentFilterItem}
isLogsDataSource={isLogsDataSource}

View File

@@ -6,8 +6,9 @@ import {
initialQueryBuilderFormValues,
} from 'constants/queryBuilder';
import { DynamicVariableSuggestion } from 'providers/Dashboard/store/dynamicVariableSuggestions';
import { useFieldKeysSuggestion } from 'hooks/querySuggestions/useFieldKeysSuggestion';
import { QueryBuilderContext } from 'providers/QueryBuilder';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { FieldDataType, TelemetryFieldKey } from 'types/api/v5/queryRange';
import { DataSource } from 'types/common/queryBuilder';
import QueryBuilderSearchV2 from '../QueryBuilderSearchV2';
@@ -88,57 +89,31 @@ const renderWithContext = (props = {}): RenderResult => {
const getSearchCombobox = (): HTMLElement =>
within(screen.getByTestId('qb-search-select')).getByRole('combobox');
// Constants to fix linter errors
const TYPE_TAG = 'tag';
const IS_COLUMN_FALSE = false;
const IS_JSON_FALSE = false;
const mockAggregateKeysData = {
payload: {
attributeKeys: [
{
key: 'http.status',
dataType: DataTypes.String,
type: TYPE_TAG,
id: 'http.status--string--tag--false',
},
{
key: 'service.name',
dataType: DataTypes.String,
type: TYPE_TAG,
isColumn: IS_COLUMN_FALSE,
isJSON: IS_JSON_FALSE,
id: 'service.name--string--tag--false',
},
{
key: 'unmapped.attribute',
dataType: 'not-a-real-data-type' as unknown as DataTypes,
type: TYPE_TAG,
isColumn: IS_COLUMN_FALSE,
isJSON: IS_JSON_FALSE,
id: 'unmapped.attribute--String--tag--false',
},
],
const mockFieldKeys: TelemetryFieldKey[] = [
{ name: 'http.status', fieldContext: 'attribute', fieldDataType: 'string' },
{ name: 'service.name', fieldContext: 'attribute', fieldDataType: 'string' },
{
name: 'unmapped.attribute',
fieldContext: 'attribute',
fieldDataType: 'not-a-real-data-type' as FieldDataType,
},
};
];
jest.mock('hooks/queryBuilder/useGetAggregateKeys', () => ({
useGetAggregateKeys: jest.fn(() => ({
data: mockAggregateKeysData,
jest.mock('hooks/querySuggestions/useFieldKeysSuggestion', () => ({
useFieldKeysSuggestion: jest.fn(() => ({
data: mockFieldKeys,
isFetching: false,
})),
}));
const mockAggregateValuesData = {
payload: {
stringAttributeValues: ['200', '404', '500'],
numberAttributeValues: [200, 404, 500],
},
const mockFieldValues = {
stringValues: ['200', '404', '500'],
numberValues: [200, 404, 500],
};
jest.mock('hooks/queryBuilder/useGetAggregateValues', () => ({
useGetAggregateValues: jest.fn(() => ({
data: mockAggregateValuesData,
jest.mock('hooks/querySuggestions/useFieldValuesSuggestion', () => ({
useFieldValuesSuggestion: jest.fn(() => ({
data: mockFieldValues,
isFetching: false,
})),
}));
@@ -196,7 +171,11 @@ describe('Suggestion Key -> Operator -> Value Flow', () => {
expect.objectContaining({
items: expect.arrayContaining([
expect.objectContaining({
key: expect.objectContaining({ key: 'http.status' }),
key: expect.objectContaining({
key: 'http.status',
type: 'attribute',
dataType: 'string',
}),
op: '=',
value: '200',
}),
@@ -279,3 +258,33 @@ describe('Dynamic Variable Suggestions', () => {
);
});
});
describe('Key fetch context', () => {
it('limits keys to the aggregate attribute context, as the resource filter needs', () => {
renderWithContext({
query: {
...defaultProps.query,
aggregateAttribute: {
...defaultProps.query.aggregateAttribute,
type: 'resource',
},
},
});
expect(useFieldKeysSuggestion).toHaveBeenLastCalledWith(
expect.objectContaining({ fieldContext: 'resource' }),
undefined,
expect.anything(),
);
});
it('fetches keys from every context when the aggregate attribute has no type', () => {
renderWithContext();
expect(useFieldKeysSuggestion).toHaveBeenLastCalledWith(
expect.objectContaining({ fieldContext: undefined }),
undefined,
expect.anything(),
);
});
});

View File

@@ -24,7 +24,6 @@ import { fieldValuesResponse } from '@/storybook/msw/__story_mockdata__/fields';
import {
EXCEPTION_CATALOGUE_SIZE,
EXCEPTION_QUICK_FILTER_CAP,
exceptionAttributeKeysResponse,
exceptionAttributeValuesResponse,
exceptionFieldKeysResponse,
exceptionQuickFiltersResponse,
@@ -58,7 +57,7 @@ export const exceptionsMocks = defineStoryMocks({
filterKeys: choiceControl<ResponseState>('Filter keys', {
group: FILTERS,
description:
'How `/autocomplete/attribute_keys` answers when the resource filter opens, apart from the page-wide Data control.',
'How `/fields/keys` answers when the resource filter opens, apart from the page-wide Data control.',
options: RESPONSE_STATES,
value: 'loaded',
}),
@@ -95,7 +94,7 @@ export const exceptionsMocks = defineStoryMocks({
rest.get(
'http://localhost/api/v1/fields/keys',
response.json((req) =>
respondWith(values.filterKeys, (req) =>
exceptionFieldKeysResponse(req.url.searchParams.get('searchText')),
),
),
@@ -109,23 +108,6 @@ export const exceptionsMocks = defineStoryMocks({
),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
respondWith(values.filterKeys, (req) =>
exceptionAttributeKeysResponse(req.url.searchParams.get('searchText')),
),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_values',
response.json((req) =>
exceptionAttributeValuesResponse(
req.url.searchParams.get('attributeKey'),
req.url.searchParams.get('searchText'),
),
),
),
],
effect: (values) => {
set(LOCALSTORAGE.SHOW_EXCEPTIONS_QUICK_FILTERS, String(values.filterPanel));

View File

@@ -17,7 +17,6 @@ import type {
import {
type BaseAutocompleteData,
DataTypes,
type IQueryAutocompleteResponse,
} from 'types/api/queryBuilder/queryAutocompleteResponse';
import type { Exception, Order, OrderBy } from 'types/api/errors/getAll';
import type { IAttributeValuesResponse } from 'types/api/queryBuilder/getAttributesValues';
@@ -311,21 +310,6 @@ const ATTRIBUTE_KEYS: BaseAutocompleteData[] = [
{ key: 'cloud.region', dataType: DataTypes.String, type: 'resource' },
];
export const exceptionAttributeKeysResponse = (
searchText: string | null,
): { status: string; data: IQueryAutocompleteResponse } => {
const search = (searchText ?? '').toLowerCase();
return {
status: 'success',
data: {
attributeKeys: ATTRIBUTE_KEYS.filter(({ key }) =>
key.toLowerCase().includes(search),
),
},
};
};
/** The quick filter settings panel lists its "other filters" from `/fields/keys`. */
export const exceptionFieldKeysResponse = (
searchText: string | null,

View File

@@ -9,6 +9,7 @@ import type { QueryRangePayload } from 'types/api/metrics/getQueryRange';
import { choiceControl, countControl } from '@/storybook/controls/controls';
import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
import { fieldValuesResponse } from '@/storybook/msw/__story_mockdata__/fields';
import {
attributeValuesResponse,
@@ -77,28 +78,18 @@ export const celeryTaskMocks = defineStoryMocks({
rest.post('http://localhost/api/v4/query_range', queryRange),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_values',
'http://localhost/api/v1/fields/values',
response.json((req) => {
const attributeKey = req.url.searchParams.get('attributeKey') ?? '';
const values_ = attributeValuesResponse(
attributeKey,
req.url.searchParams.get('searchText') ?? '',
const name = req.url.searchParams.get('name') ?? '';
const found =
attributeValuesResponse(
name,
req.url.searchParams.get('searchText') ?? '',
).data.stringAttributeValues ?? [];
return fieldValuesResponse(
name === 'worker' ? found.slice(0, values.workers) : found,
);
if (attributeKey !== 'worker') {
return values_;
}
return {
...values_,
data: {
...values_.data,
stringAttributeValues: (values_.data.stringAttributeValues ?? []).slice(
0,
values.workers,
),
},
};
}),
),
];

View File

@@ -13,6 +13,7 @@ import {
toggleControl,
} from '@/storybook/controls/controls';
import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
import { fieldValuesResponse } from '@/storybook/msw/__story_mockdata__/fields';
import {
attributeValuesResponse,
@@ -91,11 +92,13 @@ export const queueOverviewMocks = defineStoryMocks({
rest.post('http://localhost/api/v4/query_range', queryRange),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_values',
'http://localhost/api/v1/fields/values',
response.json((req) =>
attributeValuesResponse(
req.url.searchParams.get('attributeKey') ?? '',
req.url.searchParams.get('searchText') ?? '',
fieldValuesResponse(
attributeValuesResponse(
req.url.searchParams.get('name') ?? '',
req.url.searchParams.get('searchText') ?? '',
).data.stringAttributeValues ?? [],
),
),
),

View File

@@ -82,15 +82,15 @@ const CELERY_TASKS = [
const CELERY_WORKERS = ['worker-a', 'worker-b', 'worker-c'];
/**
* Values per autocomplete attribute key. The kafka config selects and both
* celery filter rows read the same v3 endpoint, so one map answers for all of
* Values per attribute key. The kafka config selects read the v3 endpoint and
* the celery filter rows read `/fields/values`, so one map answers for all of
* them; a key nobody seeded answers empty rather than 501.
*/
const ATTRIBUTE_VALUES: Record<string, string[]> = {
group: CONSUMER_GROUPS,
topic: TOPICS,
partition: PARTITIONS,
serviceName: KAFKA_SERVICES,
'service.name': KAFKA_SERVICES,
name: [
'order-events send',
'order-events process',

View File

@@ -145,7 +145,7 @@ function FunnelStep({
<FilterSelect
placeholder="Select Service"
queryParam={QueryParams.service}
filterType="serviceName"
filterType="service.name"
shouldSetQueryParams={false}
values={stepData.service_name}
isMultiple={false}

View File

@@ -3,15 +3,19 @@
* Do not hand-edit: regenerate instead.
*/
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesSignalDTO,
} from 'api/generated/services/sigNoz.schemas';
import ROUTES from 'constants/routes';
import { rest } from 'msw';
import { choiceControl, countControl } from '@/storybook/controls/controls';
import { defineStoryMocks } from '@/storybook/controls/defineStoryMocks';
import {
attributeKeysResponse,
attributeValuesResponse,
} from '@/storybook/msw/__story_mockdata__/attributes';
fieldKeysResponse,
fieldValuesResponse,
} from '@/storybook/msw/__story_mockdata__/fields';
import {
FUNNEL_ID,
@@ -41,13 +45,9 @@ const CONVERSION: Record<DropOff, number> = {
const ENTERED = 24_000;
const STEP_FILTER_KEYS = [
'service.name',
'name',
'http.status_code',
'deployment.environment',
'hasError',
];
const STEP_RESOURCE_KEYS = ['service.name', 'deployment.environment'];
const STEP_ATTRIBUTE_KEYS = ['name', 'http.status_code', 'hasError'];
const STEP_FILTER_VALUES: Record<string, string[]> = {
'service.name': ['frontend', 'checkout', 'payment', 'shipping'],
@@ -115,20 +115,44 @@ export const tracesFunnelDetailsMocks = defineStoryMocks({
'http://localhost/api/v1/trace-funnels/analytics/error-traces',
response.json(() => funnelErrorTracesResponse(values.traces)),
),
// The `Where` box on every step is the trace query builder, which reads
// its suggestions as soon as the step renders.
// The `Where` box on every step reads its suggestions as soon as the step
// renders, and the service and span name selects read `service.name` and
// `name` from the same values endpoint.
rest.get(
'http://localhost/api/v3/autocomplete/attribute_keys',
response.json(() => attributeKeysResponse(STEP_FILTER_KEYS)),
'http://localhost/api/v1/fields/keys',
response.json(() => {
const signal = TelemetrytypesSignalDTO.traces;
const resource = fieldKeysResponse(STEP_RESOURCE_KEYS, {
signal,
fieldContext: TelemetrytypesFieldContextDTO.resource,
});
const attribute = fieldKeysResponse(STEP_ATTRIBUTE_KEYS, {
signal,
fieldContext: TelemetrytypesFieldContextDTO.attribute,
});
return {
...resource,
data: {
...resource.data,
keys: { ...resource.data.keys, ...attribute.data.keys },
},
};
}),
),
rest.get(
'http://localhost/api/v3/autocomplete/attribute_values',
response.json((req) =>
attributeValuesResponse(
STEP_FILTER_VALUES[req.url.searchParams.get('attributeKey') ?? ''] ?? [],
req.url.searchParams.get('searchText') ?? '',
),
),
'http://localhost/api/v1/fields/values',
response.json((req) => {
const searchText = (
req.url.searchParams.get('searchText') ?? ''
).toLowerCase();
return fieldValuesResponse(
(STEP_FILTER_VALUES[req.url.searchParams.get('name') ?? ''] ?? []).filter(
(value) => value.toLowerCase().includes(searchText),
),
);
}),
),
];
},

View File

@@ -0,0 +1,30 @@
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { toLegacyTagFilter } from '../fieldContext';
const item = (type: string): TagFilter['items'][number] => ({
id: type,
key: { key: 'k', dataType: DataTypes.String, type },
op: '=',
value: 'v',
});
describe('toLegacyTagFilter', () => {
it('sends attribute keys as tag and leaves every other type as is', () => {
const filter: TagFilter = {
op: 'AND',
items: [
item('attribute'),
item('resource'),
item('tag'),
item(''),
item('spanSearchScope'),
],
};
expect(toLegacyTagFilter(filter).items.map((i) => i.key?.type)).toStrictEqual(
['tag', 'resource', 'tag', '', 'spanSearchScope'],
);
});
});

View File

@@ -0,0 +1,37 @@
import { TelemetrytypesFieldContextDTO } from 'api/generated/services/sigNoz.schemas';
import type { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
// Legacy filter builders read `tag` / `resource`; any other context is a top-level field there.
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Record<string, string> = {
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
[TelemetrytypesFieldContextDTO.resource]: 'resource',
};
const ATTRIBUTE_TYPE_TO_FIELD_CONTEXT: Record<
string,
TelemetrytypesFieldContextDTO
> = {
tag: TelemetrytypesFieldContextDTO.attribute,
attribute: TelemetrytypesFieldContextDTO.attribute,
resource: TelemetrytypesFieldContextDTO.resource,
};
export const fieldContextToAttributeType = (fieldContext?: string): string =>
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
export const attributeTypeToFieldContext = (
type?: string | null,
): TelemetrytypesFieldContextDTO | undefined =>
type ? ATTRIBUTE_TYPE_TO_FIELD_CONTEXT[type] : undefined;
export const toLegacyTagFilter = (filter: TagFilter): TagFilter =>
filter && {
...filter,
items: (filter.items ?? []).map((item) => ({
...item,
key: item.key && {
...item.key,
type: FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[item.key.type ?? ''] ?? item.key.type,
},
})),
};

View File

@@ -0,0 +1,21 @@
import { TelemetrytypesTelemetryFieldValuesDTO } from 'api/generated/services/sigNoz.schemas';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
// Numbers and bools stay raw so tags keep their value type.
export const getValuesForDataType = (
values: TelemetrytypesTelemetryFieldValuesDTO | undefined,
dataType: DataTypes | undefined,
): string[] => {
switch (dataType) {
case DataTypes.Int64:
case DataTypes.Float64:
case DataTypes.ArrayInt64:
case DataTypes.ArrayFloat64:
return (values?.numberValues ?? []) as unknown as string[];
case DataTypes.bool:
case DataTypes.ArrayBool:
return (values?.boolValues ?? []) as unknown as string[];
default:
return values?.stringValues ?? [];
}
};