mirror of
https://github.com/SigNoz/signoz.git
synced 2026-09-16 00:10:42 +01:00
Compare commits
14 Commits
feat/sqlco
...
feat/migra
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a8ead070c2 | ||
|
|
fa385ea7d7 | ||
|
|
ae7346719d | ||
|
|
437484e5c1 | ||
|
|
019443099f | ||
|
|
8074044706 | ||
|
|
ea0e781367 | ||
|
|
d1f5c4e9cd | ||
|
|
985294ab2b | ||
|
|
18ad5419e9 | ||
|
|
b6262102b7 | ||
|
|
8be3d711dc | ||
|
|
2b9d140f5c | ||
|
|
182ae01d3b |
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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' },
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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: '' }]);
|
||||
});
|
||||
});
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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 || '',
|
||||
}));
|
||||
@@ -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());
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user