mirror of
https://github.com/SigNoz/signoz.git
synced 2026-10-24 02:50:50 +01:00
Compare commits
4 Commits
main
...
feat/funne
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
945d3ba4f3 | ||
|
|
6efb28e450 | ||
|
|
dec7d60095 | ||
|
|
9793a6dad5 |
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -130,7 +130,7 @@ function CeleryOverviewConfigOptions(): JSX.Element {
|
||||
{
|
||||
placeholder: 'Service Name',
|
||||
queryParam: QueryParams.service,
|
||||
filterType: 'serviceName',
|
||||
filterType: 'service.name',
|
||||
},
|
||||
{
|
||||
placeholder: 'Span Name',
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
},
|
||||
};
|
||||
}),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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 ?? [],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
];
|
||||
},
|
||||
|
||||
30
frontend/src/utils/__tests__/fieldContext.test.ts
Normal file
30
frontend/src/utils/__tests__/fieldContext.test.ts
Normal 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'],
|
||||
);
|
||||
});
|
||||
});
|
||||
37
frontend/src/utils/fieldContext.ts
Normal file
37
frontend/src/utils/fieldContext.ts
Normal 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,
|
||||
},
|
||||
})),
|
||||
};
|
||||
21
frontend/src/utils/fieldValues.ts
Normal file
21
frontend/src/utils/fieldValues.ts
Normal 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 ?? [];
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user