Compare commits

...

14 Commits

Author SHA1 Message Date
Gaurav Tewari
a8ead070c2 test: match getFilterId separator change 2026-08-20 12:37:58 +05:30
Gaurav Tewari
fa385ea7d7 chore: remove comments 2026-08-20 12:32:27 +05:30
Gaurav Tewari
ae7346719d refactor: reuse shared fieldDataTypeToDataType in quick filters 2026-08-20 12:25:13 +05:30
Gaurav Tewari
437484e5c1 chore: drop cacheTime override from useFieldKeys 2026-08-20 11:10:53 +05:30
Gaurav Tewari
019443099f chore: condense comments to single lines 2026-08-20 10:56:08 +05:30
Gaurav Tewari
8074044706 feat: handle dublicate key logic 2026-08-20 10:19:52 +05:30
Gaurav Tewari
ea0e781367 feat: migrate meters calls as well 2026-08-20 10:19:51 +05:30
Gaurav Tewari
d1f5c4e9cd chore: add comments 2026-08-20 10:19:51 +05:30
Gaurav Tewari
985294ab2b refactor: quick filter at other places 2026-08-20 10:19:51 +05:30
Gaurav Tewari
18ad5419e9 refactor: quick filters 2026-08-20 10:19:51 +05:30
Gaurav Tewari
b6262102b7 chore: add limit in use field keys 2026-08-20 10:19:51 +05:30
Gaurav Tewari
8be3d711dc refactor: utils in quick filters 2026-08-20 10:19:51 +05:30
Gaurav Tewari
2b9d140f5c chore: self review comments 2026-08-20 10:19:51 +05:30
Gaurav Tewari
182ae01d3b feat: initial migration 2026-08-20 10:19:51 +05:30
11 changed files with 585 additions and 159 deletions

View File

@@ -38,6 +38,7 @@ import Slider from './FilterRenderers/Slider/Slider';
import useFilterConfig from './hooks/useFilterConfig';
import AnnouncementTooltip from './QuickFiltersSettings/AnnouncementTooltip';
import QuickFiltersSettings from './QuickFiltersSettings/QuickFiltersSettings';
import { getFilterId } from './QuickFiltersSettings/utils';
import { FiltersType, IQuickFiltersProps, QuickFiltersSource } from './types';
import './QuickFilters.styles.scss';
@@ -302,7 +303,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
case FiltersType.CHECKBOX:
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
key={getFilterId(filter.attributeKey)}
source={source}
filter={filter}
onFilterChange={onFilterChange}
@@ -311,7 +312,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
) : (
<Checkbox
key={filter.attributeKey.key}
key={getFilterId(filter.attributeKey)}
source={source}
filter={filter}
onFilterChange={onFilterChange}
@@ -321,18 +322,18 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
case FiltersType.DURATION:
return (
<Duration
key={filter.attributeKey.key}
key={getFilterId(filter.attributeKey)}
filter={filter}
onFilterChange={onFilterChange}
/>
);
case FiltersType.SLIDER:
return <Slider key={filter.attributeKey.key} />;
return <Slider key={getFilterId(filter.attributeKey)} />;
// eslint-disable-next-line sonarjs/no-duplicated-branches
default:
return useFieldApis ? (
<CheckboxV2
key={filter.attributeKey.key}
key={getFilterId(filter.attributeKey)}
source={source}
filter={filter}
onFilterChange={onFilterChange}
@@ -341,7 +342,7 @@ export default function QuickFilters(props: IQuickFiltersProps): JSX.Element {
/>
) : (
<Checkbox
key={filter.attributeKey.key}
key={getFilterId(filter.attributeKey)}
source={source}
filter={filter}
onFilterChange={onFilterChange}

View File

@@ -19,6 +19,8 @@ import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { GripVertical } from '@signozhq/icons';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { getFilterId } from './utils';
function SortableFilter({
filter,
onRemove,
@@ -31,7 +33,7 @@ function SortableFilter({
allowRemove: boolean;
}): JSX.Element {
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id: filter.key });
useSortable({ id: getFilterId(filter) });
const style = {
transform: CSS.Transform.toString(transform),
@@ -79,8 +81,8 @@ function AddedFilters({
if (over && active.id !== over.id) {
setAddedFilters((items) => {
const oldIndex = items.findIndex((item) => item.key === active.id);
const newIndex = items.findIndex((item) => item.key === over.id);
const oldIndex = items.findIndex((item) => getFilterId(item) === active.id);
const newIndex = items.findIndex((item) => getFilterId(item) === over.id);
return arrayMove(items, oldIndex, newIndex);
});
@@ -96,7 +98,9 @@ function AddedFilters({
);
const handleRemoveFilter = (filter: FilterType): void => {
setAddedFilters((prev) => prev.filter((f) => f.key !== filter.key));
setAddedFilters((prev) =>
prev.filter((f) => getFilterId(f) !== getFilterId(filter)),
);
};
const allowDrag = inputValue.length === 0;
@@ -116,13 +120,13 @@ function AddedFilters({
<div className="no-values-found">No values found</div>
) : (
<SortableContext
items={addedFilters.map((f) => f.key)}
items={addedFilters.map((f) => getFilterId(f))}
strategy={verticalListSortingStrategy}
disabled={!allowDrag}
>
{filteredAddedFilters.map((filter) => (
<SortableFilter
key={filter.key}
key={getFilterId(filter)}
filter={filter}
onRemove={handleRemoveFilter}
allowDrag={allowDrag}

View File

@@ -1,17 +1,11 @@
import { useMemo } from 'react';
import { Button, Skeleton } from 'antd';
import OverlayScrollbar from 'components/OverlayScrollbar/OverlayScrollbar';
import { SIGNAL_DATA_SOURCE_MAP } from 'components/QuickFilters/QuickFiltersSettings/constants';
import { SignalType } from 'components/QuickFilters/types';
import { REACT_QUERY_KEY } from 'constants/reactQueryKeys';
import { useGetAggregateKeys } from 'hooks/queryBuilder/useGetAggregateKeys';
import { useGetAttributeSuggestions } from 'hooks/queryBuilder/useGetAttributeSuggestions';
import { useGetQueryKeySuggestions } from 'hooks/querySuggestions/useGetQueryKeySuggestions';
import { BaseAutocompleteData } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { TagFilter } from 'types/api/queryBuilder/queryBuilderData';
import { QueryKeyDataSuggestionsProps } from 'types/api/querySuggestions/types';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
import { useFieldKeys } from './hooks/useFieldKeys';
import { getFilterId } from './utils';
function OtherFiltersSkeleton(): JSX.Element {
return (
@@ -40,87 +34,20 @@ function OtherFilters({
addedFilters: FilterType[];
setAddedFilters: React.Dispatch<React.SetStateAction<FilterType[]>>;
}): JSX.Element {
const isLogDataSource = useMemo(
() => SIGNAL_DATA_SOURCE_MAP[signal as SignalType] === DataSource.LOGS,
[signal],
);
const isMeterDataSource = useMemo(
() => signal && signal === SignalType.METER_EXPLORER,
[signal],
);
const { filters: fieldKeyFilters, isFetching } = useFieldKeys({
signal,
searchText: inputValue,
enabled: !!signal,
});
const { data: suggestionsData, isFetching: isFetchingSuggestions } =
useGetAttributeSuggestions(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
filters: {} as TagFilter,
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isLogDataSource,
},
);
const { data: aggregateKeysData, isFetching: isFetchingAggregateKeys } =
useGetAggregateKeys(
{
searchText: inputValue,
dataSource: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
aggregateOperator: 'noop',
aggregateAttribute: '',
tagType: '',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && !isLogDataSource && !isMeterDataSource,
},
);
const { data: fieldKeysData, isLoading: isLoadingFieldKeys } =
useGetQueryKeySuggestions(
{
searchText: inputValue,
signal: SIGNAL_DATA_SOURCE_MAP[signal as SignalType],
signalSource: 'meter',
},
{
queryKey: [REACT_QUERY_KEY.GET_OTHER_FILTERS, inputValue],
enabled: !!signal && isMeterDataSource,
},
);
const otherFilters = useMemo(() => {
let filterAttributes;
if (isLogDataSource) {
filterAttributes = suggestionsData?.payload?.attributes || [];
} else if (isMeterDataSource) {
const fieldKeys: QueryKeyDataSuggestionsProps[] = Object.values(
fieldKeysData?.data?.data?.keys || {},
)?.flat();
filterAttributes = fieldKeys.map(
const otherFilters = useMemo(
() =>
fieldKeyFilters.filter(
(attr) =>
({
key: attr.name,
dataType: attr.fieldDataType,
type: attr.fieldContext,
signal: attr.signal,
}) as BaseAutocompleteData,
);
} else {
filterAttributes = aggregateKeysData?.payload?.attributeKeys || [];
}
return filterAttributes?.filter(
(attr) => !addedFilters.some((filter) => filter.key === attr.key),
);
}, [
suggestionsData,
aggregateKeysData,
addedFilters,
isLogDataSource,
fieldKeysData,
isMeterDataSource,
]);
!addedFilters.some((filter) => getFilterId(filter) === getFilterId(attr)),
),
[addedFilters, fieldKeyFilters],
);
const handleAddFilter = (filter: FilterType): void => {
setAddedFilters((prev) => [
@@ -134,9 +61,7 @@ function OtherFilters({
};
const renderFilters = (): React.ReactNode => {
const isLoading =
isFetchingSuggestions || isFetchingAggregateKeys || isLoadingFieldKeys;
if (isLoading) {
if (isFetching) {
return <OtherFiltersSkeleton />;
}
if (!otherFilters?.length) {
@@ -144,12 +69,12 @@ function OtherFilters({
}
return otherFilters.map((filter) => (
<div key={filter.key} className="qf-filter-item other-filters-item">
<div key={getFilterId(filter)} className="qf-filter-item other-filters-item">
<div className="qf-filter-key">{filter.key}</div>
<Button
className="add-filter-btn periscope-btn"
size="small"
onClick={(): void => handleAddFilter(filter as FilterType)}
onClick={(): void => handleAddFilter(filter)}
>
Add
</Button>

View File

@@ -8,6 +8,7 @@ import { SignalType } from '../types';
import AddedFilters from './AddedFilters';
import useQuickFilterSettings from './hooks/useQuickFilterSettings';
import OtherFilters from './OtherFilters';
import { getFilterId } from './utils';
import './QuickFiltersSettings.styles.scss';
@@ -45,7 +46,8 @@ function QuickFiltersSettings({
!(
addedFilters.length === customFilters.length &&
addedFilters.every(
(filter, index) => filter.key === customFilters[index].key,
(filter, index) =>
getFilterId(filter) === getFilterId(customFilters[index]),
)
),
[addedFilters, customFilters],

View File

@@ -0,0 +1,146 @@
import { ENVIRONMENT } from 'constants/env';
import {
meterFieldKeysResponse,
otherFiltersFieldKeysResponse,
} from 'mocks-server/__mockdata__/customQuickFilters';
import { server } from 'mocks-server/server';
import { rest } from 'msw';
import { render, screen, userEvent, waitFor } from 'tests/test-utils';
import '@testing-library/jest-dom';
import { SignalType } from '../../types';
import OtherFilters from '../OtherFilters';
const fieldsKeysURL = `${ENVIRONMENT.baseURL}/api/v1/fields/keys`;
const setAddedFilters = jest.fn();
let requests: URLSearchParams[] = [];
const setupServer = (): void => {
server.use(
rest.get(fieldsKeysURL, (req, res, ctx) => {
requests.push(req.url.searchParams);
const body =
req.url.searchParams.get('source') === 'meter'
? meterFieldKeysResponse
: otherFiltersFieldKeysResponse;
return res(ctx.status(200), ctx.json(body));
}),
);
};
const renderOtherFilters = (signal: SignalType | undefined): void => {
render(
<OtherFilters
signal={signal}
inputValue=""
addedFilters={[]}
setAddedFilters={setAddedFilters}
/>,
);
};
describe('OtherFilters field keys request', () => {
beforeEach(() => {
requests = [];
setAddedFilters.mockClear();
setupServer();
});
it('requests logs keys with the shared limit and no source', async () => {
renderOtherFilters(SignalType.LOGS);
await waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0].get('signal')).toBe('logs');
expect(requests[0].get('limit')).toBe('100');
expect(requests[0].has('source')).toBe(false);
await expect(screen.findByText('service.name')).resolves.toBeInTheDocument();
});
it.each([
[SignalType.TRACES],
[SignalType.EXCEPTIONS],
[SignalType.API_MONITORING],
])('requests traces keys for the %s signal', async (signal) => {
renderOtherFilters(signal);
await waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0].get('signal')).toBe('traces');
});
it('scopes the meter signal to the metrics signal and the meter source', async () => {
renderOtherFilters(SignalType.METER_EXPLORER);
await waitFor(() => expect(requests).toHaveLength(1));
expect(requests[0].get('signal')).toBe('metrics');
expect(requests[0].get('source')).toBe('meter');
expect(requests[0].get('limit')).toBe('100');
});
it('does not request keys without a signal', async () => {
renderOtherFilters(undefined);
await waitFor(() =>
expect(screen.getByText(/OTHER FILTERS/i)).toBeInTheDocument(),
);
expect(requests).toHaveLength(0);
});
it('renders one row per context for a key that spans contexts', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderOtherFilters(SignalType.LOGS);
const rows = await screen.findAllByText('host.name');
expect(rows).toHaveLength(2);
await user.click(
rows[0].parentElement?.querySelector('button') as HTMLElement,
);
await user.click(
rows[1].parentElement?.querySelector('button') as HTMLElement,
);
const added = setAddedFilters.mock.calls.map((call) => call[0]([])[0]);
expect(added).toStrictEqual([
{ key: 'host.name', dataType: 'string', type: 'resource' },
{ key: 'host.name', dataType: 'string', type: 'tag' },
]);
});
it('excludes an added filter only for its own context', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
render(
<OtherFilters
signal={SignalType.LOGS}
inputValue=""
addedFilters={[{ key: 'host.name', dataType: 'string', type: 'resource' }]}
setAddedFilters={setAddedFilters}
/>,
);
const rows = await screen.findAllByText('host.name');
expect(rows).toHaveLength(1);
await user.click(
rows[0].parentElement?.querySelector('button') as HTMLElement,
);
expect(setAddedFilters.mock.calls[0][0]([])).toStrictEqual([
{ key: 'host.name', dataType: 'string', type: 'tag' },
]);
});
it('keeps the raw meter context and narrows its number data type on add', async () => {
const user = userEvent.setup({ pointerEventsCheck: 0 });
renderOtherFilters(SignalType.METER_EXPLORER);
const row = await screen.findByText('http.status_code');
await user.click(row.parentElement?.querySelector('button') as HTMLElement);
expect(setAddedFilters).toHaveBeenCalledTimes(1);
const updater = setAddedFilters.mock.calls[0][0];
expect(updater([])).toStrictEqual([
{ key: 'http.status_code', dataType: 'float64', type: 'attribute' },
]);
});
});

View File

@@ -0,0 +1,186 @@
import { TelemetrytypesSignalDTO } from 'api/generated/services/sigNoz.schemas';
import { SignalType } from 'components/QuickFilters/types';
import { SIGNAL_DATA_SOURCE_MAP } from '../constants';
import {
DATA_SOURCE_TO_SIGNAL,
getFilterId,
mapFieldKeysToFilters,
mapMeterFieldKeysToFilters,
} from '../utils';
describe('DATA_SOURCE_TO_SIGNAL', () => {
it.each([
[SignalType.LOGS, TelemetrytypesSignalDTO.logs],
[SignalType.TRACES, TelemetrytypesSignalDTO.traces],
[SignalType.EXCEPTIONS, TelemetrytypesSignalDTO.traces],
[SignalType.API_MONITORING, TelemetrytypesSignalDTO.traces],
[SignalType.METER_EXPLORER, TelemetrytypesSignalDTO.metrics],
])('maps %s to the %s signal', (signal, expected) => {
expect(DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]).toBe(expected);
});
});
describe('mapFieldKeysToFilters', () => {
it('returns an empty list when there are no keys', () => {
expect(mapFieldKeysToFilters(undefined)).toStrictEqual([]);
expect(mapFieldKeysToFilters(null)).toStrictEqual([]);
});
it('maps field contexts to v3 attribute types', () => {
const filters = mapFieldKeysToFilters({
'service.name': [
{ name: 'service.name', fieldContext: 'resource', fieldDataType: 'string' },
],
'http.method': [
{ name: 'http.method', fieldContext: 'attribute', fieldDataType: 'string' },
],
body: [{ name: 'body', fieldContext: 'body', fieldDataType: 'string' }],
} as never);
expect(filters).toStrictEqual([
{ key: 'service.name', dataType: 'string', type: 'resource' },
{ key: 'http.method', dataType: 'string', type: 'tag' },
{ key: 'body', dataType: 'string', type: '' },
]);
});
it('narrows the number data type, which v3 attribute keys do not support', () => {
const filters = mapFieldKeysToFilters({
'http.status_code': [
{
name: 'http.status_code',
fieldContext: 'attribute',
fieldDataType: 'number',
},
],
} as never);
expect(filters).toStrictEqual([
{ key: 'http.status_code', dataType: 'float64', type: 'tag' },
]);
});
it('maps array data types rather than dropping them', () => {
const filters = mapFieldKeysToFilters({
'http.request.header': [
{
name: 'http.request.header',
fieldContext: 'attribute',
fieldDataType: '[]string',
},
],
} as never);
expect(filters).toStrictEqual([
{ key: 'http.request.header', dataType: 'array(string)', type: 'tag' },
]);
});
it('returns one filter per context when a key exists under multiple contexts', () => {
const filters = mapFieldKeysToFilters({
'service.name': [
{ name: 'service.name', fieldContext: 'resource', fieldDataType: 'string' },
{
name: 'service.name',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
} as never);
expect(filters).toStrictEqual([
{ key: 'service.name', dataType: 'string', type: 'resource' },
{ key: 'service.name', dataType: 'string', type: 'tag' },
]);
});
it('dedupes variants whose contexts map to the same v3 type', () => {
const filters = mapFieldKeysToFilters({
body: [
{ name: 'body', fieldContext: 'body', fieldDataType: 'string' },
{ name: 'body', fieldContext: 'log', fieldDataType: 'string' },
],
} as never);
expect(filters).toStrictEqual([
{ key: 'body', dataType: 'string', type: '' },
]);
});
it('falls back to unspecified for missing context and data type', () => {
const filters = mapFieldKeysToFilters({
custom: [{ name: 'custom' }],
} as never);
expect(filters).toStrictEqual([{ key: 'custom', dataType: '', type: '' }]);
});
});
describe('getFilterId', () => {
it('disambiguates the same name across types', () => {
expect(getFilterId({ key: 'service.name', type: 'resource' })).toBe(
'resource:service.name',
);
expect(getFilterId({ key: 'service.name', type: 'tag' })).toBe(
'tag:service.name',
);
});
it('falls back to the bare name without a type', () => {
expect(getFilterId({ key: 'body', type: '' })).toBe('body');
expect(getFilterId({ key: 'body' })).toBe('body');
});
});
describe('mapMeterFieldKeysToFilters', () => {
it('returns an empty list when there are no keys', () => {
expect(mapMeterFieldKeysToFilters(undefined)).toStrictEqual([]);
});
it('keeps the raw field context, which carries the metric aggregation', () => {
const filters = mapMeterFieldKeysToFilters({
'service.name': [
{
name: 'service.name',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
'signoz.metric.name': [
{
name: 'signoz.metric.name',
fieldContext: 'metric',
fieldDataType: 'float64',
},
],
} as never);
expect(filters).toStrictEqual([
{ key: 'service.name', dataType: 'string', type: 'attribute' },
{ key: 'signoz.metric.name', dataType: 'float64', type: 'metric' },
]);
});
it('narrows the number data type so the save is not rejected', () => {
const filters = mapMeterFieldKeysToFilters({
'http.status_code': [
{
name: 'http.status_code',
fieldContext: 'attribute',
fieldDataType: 'number',
},
],
} as never);
expect(filters).toStrictEqual([
{ key: 'http.status_code', dataType: 'float64', type: 'attribute' },
]);
});
it('falls back to unspecified for missing context and data type', () => {
expect(
mapMeterFieldKeysToFilters({ custom: [{ name: 'custom' }] } as never),
).toStrictEqual([{ key: 'custom', dataType: '', type: '' }]);
});
});

View File

@@ -0,0 +1,60 @@
import { useMemo } from 'react';
import { TelemetrytypesSourceDTO } from 'api/generated/services/sigNoz.schemas';
import { useGetFieldsKeys } from 'api/generated/services/fields';
import { SignalType } from 'components/QuickFilters/types';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { SIGNAL_DATA_SOURCE_MAP } from '../constants';
import {
DATA_SOURCE_TO_SIGNAL,
mapFieldKeysToFilters,
mapMeterFieldKeysToFilters,
} from '../utils';
const FIELD_KEYS_LIMIT = 100;
interface UseFieldKeysProps {
signal: SignalType | undefined;
searchText: string;
enabled: boolean;
}
interface UseFieldKeysReturn {
filters: FilterType[];
isFetching: boolean;
}
/** Meter keys are a source-scoped subset of metrics, hence the `source` param and its own mapper. */
export function useFieldKeys({
signal,
searchText,
enabled,
}: UseFieldKeysProps): UseFieldKeysReturn {
const isMeterSignal = signal === SignalType.METER_EXPLORER;
const { data, isFetching } = useGetFieldsKeys(
{
signal: signal
? DATA_SOURCE_TO_SIGNAL[SIGNAL_DATA_SOURCE_MAP[signal]]
: undefined,
source: isMeterSignal ? TelemetrytypesSourceDTO.meter : undefined,
searchText,
limit: FIELD_KEYS_LIMIT,
},
{
query: {
enabled,
keepPreviousData: true,
},
},
);
const filters = useMemo(
() =>
isMeterSignal
? mapMeterFieldKeysToFilters(data?.data?.keys)
: mapFieldKeysToFilters(data?.data?.keys),
[data, isMeterSignal],
);
return { filters, isFetching };
}

View File

@@ -0,0 +1,68 @@
import {
TelemetrytypesFieldContextDTO,
TelemetrytypesGettableFieldKeysDTOKeys,
TelemetrytypesSignalDTO,
TelemetrytypesTelemetryFieldKeyDTO,
} from 'api/generated/services/sigNoz.schemas';
import { Filter as FilterType } from 'types/api/quickFilters/getCustomFilters';
import { DataSource } from 'types/common/queryBuilder';
import { fieldDataTypeToDataType } from 'utils/fieldDataType';
export const DATA_SOURCE_TO_SIGNAL: Record<
DataSource,
TelemetrytypesSignalDTO
> = {
[DataSource.LOGS]: TelemetrytypesSignalDTO.logs,
[DataSource.TRACES]: TelemetrytypesSignalDTO.traces,
[DataSource.METRICS]: TelemetrytypesSignalDTO.metrics,
};
const FIELD_CONTEXT_TO_ATTRIBUTE_TYPE: Partial<
Record<TelemetrytypesFieldContextDTO, string>
> = {
[TelemetrytypesFieldContextDTO.attribute]: 'tag',
[TelemetrytypesFieldContextDTO.resource]: 'resource',
};
const toAttributeType = (
fieldContext: TelemetrytypesFieldContextDTO | undefined,
): string =>
(fieldContext && FIELD_CONTEXT_TO_ATTRIBUTE_TYPE[fieldContext]) || '';
export const getFilterId = (filter: {
key: string;
type?: string | null;
}): string => (filter.type ? `${filter.type}:${filter.key}` : filter.key);
/** Keyed by id: distinct contexts can map to the same v3 type, and a repeated id collides as a React key. */
const mapEachKeyVariant = (
keys: TelemetrytypesGettableFieldKeysDTOKeys | undefined,
toFilter: (fieldKey: TelemetrytypesTelemetryFieldKeyDTO) => FilterType,
): FilterType[] => {
const filters = Object.values(keys ?? {})
.flat()
.filter((fieldKey) => !!fieldKey?.name)
.map(toFilter);
return [...new Map(filters.map((f) => [getFilterId(f), f])).values()];
};
/** Keys are grouped by name; each context under a name becomes its own filter. */
export const mapFieldKeysToFilters = (
keys: TelemetrytypesGettableFieldKeysDTOKeys | undefined,
): FilterType[] =>
mapEachKeyVariant(keys, (fieldKey) => ({
key: fieldKey.name,
dataType: fieldDataTypeToDataType(fieldKey.fieldDataType),
type: toAttributeType(fieldKey.fieldContext),
}));
/** Keeps the raw field context: for metrics `type` carries the aggregation, not the attribute scope. */
export const mapMeterFieldKeysToFilters = (
keys: TelemetrytypesGettableFieldKeysDTOKeys | undefined,
): FilterType[] =>
mapEachKeyVariant(keys, (fieldKey) => ({
key: fieldKey.name,
dataType: fieldDataTypeToDataType(fieldKey.fieldDataType),
type: fieldKey.fieldContext || '',
}));

View File

@@ -5,7 +5,7 @@ import {
} from 'container/ApiMonitoring/queryParams';
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
import {
otherFiltersResponse,
otherFiltersFieldKeysResponse,
quickFiltersAttributeValuesResponse,
quickFiltersListResponse,
} from 'mocks-server/__mockdata__/customQuickFilters';
@@ -36,7 +36,7 @@ const BASE_URL = ENVIRONMENT.baseURL;
const SIGNAL = SignalType.LOGS;
const quickFiltersListURL = `${BASE_URL}/api/v1/orgs/me/filters/${SIGNAL}`;
const saveQuickFiltersURL = `${BASE_URL}/api/v1/orgs/me/filters`;
const quickFiltersSuggestionsURL = `${BASE_URL}/api/v3/filter_suggestions`;
const fieldsKeysURL = `${BASE_URL}/api/v1/fields/keys`;
const quickFiltersAttributeValuesURL = `${BASE_URL}/api/v3/autocomplete/attribute_values`;
const fieldsValuesURL = `${BASE_URL}/api/v1/fields/values`;
@@ -55,8 +55,8 @@ const setupServer = (): void => {
rest.get(quickFiltersListURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(quickFiltersListResponse)),
),
rest.get(quickFiltersSuggestionsURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(otherFiltersResponse)),
rest.get(fieldsKeysURL, (_, res, ctx) =>
res(ctx.status(200), ctx.json(otherFiltersFieldKeysResponse)),
),
rest.put(saveQuickFiltersURL, async (req, res, ctx) => {
putHandler(await req.json());

View File

@@ -93,7 +93,6 @@ export const REACT_QUERY_KEY = {
// Quick Filters Query Keys
GET_CUSTOM_FILTERS: 'GET_CUSTOM_FILTERS',
GET_OTHER_FILTERS: 'GET_OTHER_FILTERS',
SPAN_LOGS: 'SPAN_LOGS',
SPAN_BEFORE_LOGS: 'SPAN_BEFORE_LOGS',
SPAN_AFTER_LOGS: 'SPAN_AFTER_LOGS',

View File

@@ -62,56 +62,91 @@ export const quickFiltersListResponse = {
},
};
export const otherFiltersResponse = {
export const meterFieldKeysResponse = {
status: 'success',
data: {
attributes: [
{
key: 'service.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.deployment.name',
dataType: 'string',
type: 'resource',
},
{
key: 'deployment.environment',
dataType: 'string',
type: 'resource',
},
{
key: 'service.namespace',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.namespace.name',
dataType: 'string',
type: 'resource',
},
{
key: 'service.instance.id',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.name',
dataType: 'string',
type: 'resource',
},
{
key: 'k8s.pod.uid',
dataType: 'string',
type: 'resource',
},
{
key: 'os.description',
dataType: 'string',
type: 'resource',
},
],
complete: true,
keys: {
'service.name': [
{
name: 'service.name',
signal: 'metrics',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
'http.status_code': [
{
name: 'http.status_code',
signal: 'metrics',
fieldContext: 'attribute',
fieldDataType: 'number',
},
],
},
},
};
export const otherFiltersFieldKeysResponse = {
status: 'success',
data: {
complete: true,
keys: {
'service.name': [
{
name: 'service.name',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
},
],
'k8s.deployment.name': [
{
name: 'k8s.deployment.name',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
},
],
'deployment.environment': [
{
name: 'deployment.environment',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
},
],
'http.status_code': [
{
name: 'http.status_code',
signal: 'logs',
fieldContext: 'attribute',
fieldDataType: 'number',
},
],
'os.description': [
{
name: 'os.description',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
},
],
'host.name': [
{
name: 'host.name',
signal: 'logs',
fieldContext: 'resource',
fieldDataType: 'string',
},
{
name: 'host.name',
signal: 'logs',
fieldContext: 'attribute',
fieldDataType: 'string',
},
],
},
},
};