mirror of
https://github.com/SigNoz/signoz.git
synced 2026-06-19 15:00:24 +01:00
Compare commits
2 Commits
tvats-fat-
...
issue_5267
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d1682f2ab6 | ||
|
|
b8567664da |
@@ -6992,6 +6992,16 @@ components:
|
||||
required:
|
||||
- items
|
||||
type: object
|
||||
SpantypesGettableSpanMapperTest:
|
||||
properties:
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- spans
|
||||
type: object
|
||||
SpantypesGettableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -7079,6 +7089,39 @@ components:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTest:
|
||||
properties:
|
||||
groups:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTestGroup'
|
||||
nullable: true
|
||||
type: array
|
||||
spans:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperTestSpan'
|
||||
nullable: true
|
||||
type: array
|
||||
required:
|
||||
- spans
|
||||
- groups
|
||||
type: object
|
||||
SpantypesPostableSpanMapperTestGroup:
|
||||
properties:
|
||||
condition:
|
||||
$ref: '#/components/schemas/SpantypesSpanMapperGroupCondition'
|
||||
enabled:
|
||||
type: boolean
|
||||
mappers:
|
||||
items:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapper'
|
||||
nullable: true
|
||||
type: array
|
||||
name:
|
||||
type: string
|
||||
required:
|
||||
- name
|
||||
- condition
|
||||
type: object
|
||||
SpantypesPostableTraceAggregations:
|
||||
properties:
|
||||
aggregations:
|
||||
@@ -7240,6 +7283,17 @@ components:
|
||||
- operation
|
||||
- priority
|
||||
type: object
|
||||
SpantypesSpanMapperTestSpan:
|
||||
properties:
|
||||
attributes:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
resource:
|
||||
additionalProperties: {}
|
||||
nullable: true
|
||||
type: object
|
||||
type: object
|
||||
SpantypesUpdatableSpanMapper:
|
||||
properties:
|
||||
config:
|
||||
@@ -12797,6 +12851,69 @@ paths:
|
||||
summary: Update a span mapper
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/span_mapper_groups/test:
|
||||
post:
|
||||
deprecated: false
|
||||
description: Tests how span mappers would transform sample spans
|
||||
operationId: TestSpanMappers
|
||||
requestBody:
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/SpantypesPostableSpanMapperTest'
|
||||
responses:
|
||||
"200":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
properties:
|
||||
data:
|
||||
$ref: '#/components/schemas/SpantypesGettableSpanMapperTest'
|
||||
status:
|
||||
type: string
|
||||
required:
|
||||
- status
|
||||
- data
|
||||
type: object
|
||||
description: OK
|
||||
"400":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Bad Request
|
||||
"401":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Unauthorized
|
||||
"403":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Forbidden
|
||||
"404":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Not Found
|
||||
"500":
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/RenderErrorResponse'
|
||||
description: Internal Server Error
|
||||
security:
|
||||
- api_key:
|
||||
- VIEWER
|
||||
- tokenizer:
|
||||
- VIEWER
|
||||
summary: Test span mappers against sample spans
|
||||
tags:
|
||||
- spanmapper
|
||||
/api/v1/stats:
|
||||
get:
|
||||
deprecated: false
|
||||
|
||||
@@ -8135,6 +8135,44 @@ export interface SpantypesGettableSpanMapperGroupsDTO {
|
||||
items: SpantypesSpanMapperGroupDTO[];
|
||||
}
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributesAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOAttributes =
|
||||
SpantypesSpanMapperTestSpanDTOAttributesAnyOf | null;
|
||||
|
||||
export type SpantypesSpanMapperTestSpanDTOResourceAnyOf = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
/**
|
||||
* @nullable
|
||||
*/
|
||||
export type SpantypesSpanMapperTestSpanDTOResource =
|
||||
SpantypesSpanMapperTestSpanDTOResourceAnyOf | null;
|
||||
|
||||
export interface SpantypesSpanMapperTestSpanDTO {
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
attributes?: SpantypesSpanMapperTestSpanDTOAttributes;
|
||||
/**
|
||||
* @type object,null
|
||||
*/
|
||||
resource?: SpantypesSpanMapperTestSpanDTOResource;
|
||||
}
|
||||
|
||||
export interface SpantypesGettableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export enum SpantypesSpanAggregationTypeDTO {
|
||||
span_count = 'span_count',
|
||||
execution_time_percentage = 'execution_time_percentage',
|
||||
@@ -8430,6 +8468,33 @@ export interface SpantypesPostableSpanMapperGroupDTO {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestGroupDTO {
|
||||
condition: SpantypesSpanMapperGroupConditionDTO | null;
|
||||
/**
|
||||
* @type boolean
|
||||
*/
|
||||
enabled?: boolean;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
mappers?: SpantypesPostableSpanMapperDTO[] | null;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface SpantypesPostableSpanMapperTestDTO {
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
groups: SpantypesPostableSpanMapperTestGroupDTO[] | null;
|
||||
/**
|
||||
* @type array,null
|
||||
*/
|
||||
spans: SpantypesSpanMapperTestSpanDTO[] | null;
|
||||
}
|
||||
|
||||
export interface SpantypesSpanAggregationDTO {
|
||||
aggregation: SpantypesSpanAggregationTypeDTO;
|
||||
field: TelemetrytypesTelemetryFieldKeyDTO;
|
||||
@@ -9798,6 +9863,14 @@ export type UpdateSpanMapperPathParameters = {
|
||||
groupId: string;
|
||||
mapperId: string;
|
||||
};
|
||||
export type TestSpanMappers200 = {
|
||||
data: SpantypesGettableSpanMapperTestDTO;
|
||||
/**
|
||||
* @type string
|
||||
*/
|
||||
status: string;
|
||||
};
|
||||
|
||||
export type GetStats200Data = { [key: string]: unknown };
|
||||
|
||||
export type GetStats200 = {
|
||||
|
||||
@@ -30,8 +30,10 @@ import type {
|
||||
RenderErrorResponseDTO,
|
||||
SpantypesPostableSpanMapperDTO,
|
||||
SpantypesPostableSpanMapperGroupDTO,
|
||||
SpantypesPostableSpanMapperTestDTO,
|
||||
SpantypesUpdatableSpanMapperDTO,
|
||||
SpantypesUpdatableSpanMapperGroupDTO,
|
||||
TestSpanMappers200,
|
||||
UpdateSpanMapperGroupPathParameters,
|
||||
UpdateSpanMapperPathParameters,
|
||||
} from '../sigNoz.schemas';
|
||||
@@ -780,3 +782,86 @@ export const useUpdateSpanMapper = <
|
||||
> => {
|
||||
return useMutation(getUpdateSpanMapperMutationOptions(options));
|
||||
};
|
||||
/**
|
||||
* Tests how span mappers would transform sample spans
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const testSpanMappers = (
|
||||
spantypesPostableSpanMapperTestDTO?: BodyType<SpantypesPostableSpanMapperTestDTO>,
|
||||
signal?: AbortSignal,
|
||||
) => {
|
||||
return GeneratedAPIInstance<TestSpanMappers200>({
|
||||
url: `/api/v1/span_mapper_groups/test`,
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
data: spantypesPostableSpanMapperTestDTO,
|
||||
signal,
|
||||
});
|
||||
};
|
||||
|
||||
export const getTestSpanMappersMutationOptions = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
const mutationKey = ['testSpanMappers'];
|
||||
const { mutation: mutationOptions } = options
|
||||
? options.mutation &&
|
||||
'mutationKey' in options.mutation &&
|
||||
options.mutation.mutationKey
|
||||
? options
|
||||
: { ...options, mutation: { ...options.mutation, mutationKey } }
|
||||
: { mutation: { mutationKey } };
|
||||
|
||||
const mutationFn: MutationFunction<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> }
|
||||
> = (props) => {
|
||||
const { data } = props ?? {};
|
||||
|
||||
return testSpanMappers(data);
|
||||
};
|
||||
|
||||
return { mutationFn, ...mutationOptions };
|
||||
};
|
||||
|
||||
export type TestSpanMappersMutationResult = NonNullable<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>
|
||||
>;
|
||||
export type TestSpanMappersMutationBody =
|
||||
| BodyType<SpantypesPostableSpanMapperTestDTO>
|
||||
| undefined;
|
||||
export type TestSpanMappersMutationError = ErrorType<RenderErrorResponseDTO>;
|
||||
|
||||
/**
|
||||
* @summary Test span mappers against sample spans
|
||||
*/
|
||||
export const useTestSpanMappers = <
|
||||
TError = ErrorType<RenderErrorResponseDTO>,
|
||||
TContext = unknown,
|
||||
>(options?: {
|
||||
mutation?: UseMutationOptions<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
>;
|
||||
}): UseMutationResult<
|
||||
Awaited<ReturnType<typeof testSpanMappers>>,
|
||||
TError,
|
||||
{ data?: BodyType<SpantypesPostableSpanMapperTestDTO> },
|
||||
TContext
|
||||
> => {
|
||||
return useMutation(getTestSpanMappersMutationOptions(options));
|
||||
};
|
||||
|
||||
@@ -1,67 +1,29 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Input } from '@signozhq/ui/input';
|
||||
import { Button, Skeleton } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Skeleton } from 'antd';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import {
|
||||
DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY,
|
||||
PANEL_TYPES,
|
||||
} from 'constants/queryBuilder';
|
||||
import { DEBOUNCE_DELAY } from 'constants/queryBuilderFilterConfig';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import useDebouncedFn from 'hooks/useDebouncedFunction';
|
||||
import { cloneDeep, isArray, isFunction } from 'lodash-es';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import CheckboxFilterHeader from './CheckboxFilterHeader';
|
||||
import CheckboxValueRow from './CheckboxValueRow';
|
||||
import LogsQuickFilterEmptyState from './LogsQuickFilterEmptyState';
|
||||
import { isKeyMatch } from './utils';
|
||||
import useActiveQueryIndex from './useActiveQueryIndex';
|
||||
import useCheckboxDisclosure from './useCheckboxDisclosure';
|
||||
import useCheckboxFilterActions from './useCheckboxFilterActions';
|
||||
import useCheckboxFilterState from './useCheckboxFilterState';
|
||||
import useCheckboxFilterValues from './useCheckboxFilterValues';
|
||||
|
||||
import './Checkbox.styles.scss';
|
||||
|
||||
const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
const SOURCES_WITH_EMPTY_STATE_ENABLED = [QuickFiltersSource.LOGS_EXPLORER];
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
}
|
||||
|
||||
function setDefaultValues(
|
||||
values: string[],
|
||||
trueOrFalse: boolean,
|
||||
): Record<string, boolean> {
|
||||
const defaultState: Record<string, boolean> = {};
|
||||
values.forEach((val) => {
|
||||
defaultState[val] = trueOrFalse;
|
||||
});
|
||||
return defaultState;
|
||||
}
|
||||
interface ICheckboxProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
@@ -72,194 +34,39 @@ interface ICheckboxProps {
|
||||
export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
const { source, filter, onFilterChange } = props;
|
||||
const [searchText, setSearchText] = useState<string>('');
|
||||
// null = no user action, true = user opened, false = user closed
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(10);
|
||||
|
||||
const activeQueryIndex = useActiveQueryIndex(source);
|
||||
|
||||
const {
|
||||
lastUsedQuery,
|
||||
currentQuery,
|
||||
redirectWithQueryBuilderData,
|
||||
panelType,
|
||||
} = useQueryBuilder();
|
||||
|
||||
// Determine if we're in ListView mode
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
// In ListView mode, use index 0 for most sources; for TRACES_EXPLORER, use lastUsedQuery
|
||||
// Otherwise use lastUsedQuery for non-ListView modes
|
||||
const activeQueryIndex = useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
|
||||
// Check if this filter has active filters in the query
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
// Derive isOpen from filter state + user action
|
||||
const isOpen = useMemo(() => {
|
||||
// If user explicitly toggled, respect that
|
||||
if (userToggleState !== null) {
|
||||
return userToggleState;
|
||||
}
|
||||
|
||||
// Auto-open if this filter has active filters in the query
|
||||
if (isSomeFilterPresentForCurrentAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise use default behavior (first 2 filters open)
|
||||
return filter.defaultOpen;
|
||||
}, [
|
||||
userToggleState,
|
||||
isOpen,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
filter.defaultOpen,
|
||||
]);
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
} = useCheckboxDisclosure({ filter, activeQueryIndex });
|
||||
|
||||
const { data, isLoading } = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||
dataSource: filter.dataSource || DataSource.LOGS,
|
||||
aggregateAttribute: filter.aggregateAttribute || '',
|
||||
attributeKey: filter.attributeKey.key,
|
||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||
tagType: filter.attributeKey.type || '',
|
||||
searchText: searchText ?? '',
|
||||
},
|
||||
{
|
||||
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
const { attributeValues, isLoading } = useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
});
|
||||
|
||||
const { data: keyValueSuggestions, isLoading: isLoadingKeyValueSuggestions } =
|
||||
useGetQueryKeyValueSuggestions({
|
||||
key: filter.attributeKey.key,
|
||||
signal: filter.dataSource || DataSource.LOGS,
|
||||
signalSource: 'meter',
|
||||
options: {
|
||||
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
});
|
||||
const { currentFilterState, isFilterDisabled, isMultipleValuesTrueForTheKey } =
|
||||
useCheckboxFilterState({ filter, attributeValues, activeQueryIndex });
|
||||
|
||||
const attributeValues: string[] = useMemo(() => {
|
||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||
|
||||
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||
// Process the response data
|
||||
const responseData = keyValueSuggestions?.data as any;
|
||||
const values = responseData.data?.values || {};
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
.filter(
|
||||
(value: string | null | undefined): value is string =>
|
||||
value !== null && value !== undefined && value !== '',
|
||||
);
|
||||
|
||||
// Generate options from number values
|
||||
const numberOptions = numberValues
|
||||
.filter(
|
||||
(value: number | null | undefined): value is number =>
|
||||
value !== null && value !== undefined,
|
||||
)
|
||||
.map((value: number) => value.toString());
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
return [...stringOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
return (data?.payload?.[key] || []).filter(
|
||||
(val) => val !== undefined && val !== null,
|
||||
);
|
||||
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||
const { onChange, onClear } = useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
});
|
||||
|
||||
const setSearchTextDebounced = useDebouncedFn((...args) => {
|
||||
setSearchText(args[0] as string);
|
||||
}, DEBOUNCE_DELAY);
|
||||
|
||||
// derive the state of each filter key here in the renderer itself and keep it in sync with current query
|
||||
// also we need to keep a note of last focussed query.
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
const currentFilterState = useMemo(() => {
|
||||
let filterState: Record<string, boolean> = setDefaultValues(
|
||||
attributeValues,
|
||||
false,
|
||||
);
|
||||
const filterSync = currentQuery?.builder.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items.find((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (filterSync) {
|
||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = true;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = true;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
}
|
||||
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = false;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = false;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
}
|
||||
return filterState;
|
||||
}, [
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
]);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// variable to check if the current filter has multiple values to its name in the key op value section
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
// Sort checked items to the top, then unchecked items
|
||||
const currentAttributeKeys = useMemo(() => {
|
||||
const checkedValues = attributeValues.filter(
|
||||
@@ -277,293 +84,6 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
[currentAttributeKeys, currentFilterState],
|
||||
);
|
||||
|
||||
const handleClearFilterAttribute = (): void => {
|
||||
const preparedQuery: Query = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(preparedQuery);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(preparedQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
): void => {
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only';
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
} else if (query?.filters?.items) {
|
||||
if (
|
||||
query.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)
|
||||
) {
|
||||
// if there is already a running filter for the current attribute key then
|
||||
// we split the cases by which particular operator is present right now!
|
||||
const currentFilter = query.filters?.items?.find((q) =>
|
||||
isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (currentFilter) {
|
||||
const runningOperator = currentFilter?.op;
|
||||
switch (runningOperator) {
|
||||
case 'in':
|
||||
if (checked) {
|
||||
// if it's an IN operator then if we are checking another value it get's added to the
|
||||
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// if the current state wasn't an array we make it one and add our value
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (!checked) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// if not an array remove the whole thing altogether!
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'nin':
|
||||
case 'not in':
|
||||
// if the current running operator is NIN then when unchecking the value it gets
|
||||
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||
if (!checked) {
|
||||
// in case of array add the currentUnselectedValue to the list.
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// in case of not an array make it one!
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (checked) {
|
||||
// opposite of above!
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
if (checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (!checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// case - when there is no filter for the current key that means all are selected right now.
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
}
|
||||
const finalQuery = {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(finalQuery);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(finalQuery);
|
||||
}
|
||||
};
|
||||
|
||||
const isEmptyStateWithDocsEnabled =
|
||||
SOURCES_WITH_EMPTY_STATE_ENABLED.includes(source) &&
|
||||
!searchText &&
|
||||
@@ -571,48 +91,19 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
|
||||
return (
|
||||
<div className="checkbox-filter">
|
||||
<section
|
||||
className="filter-header-checkbox"
|
||||
onClick={(): void => {
|
||||
if (isOpen) {
|
||||
setUserToggleState(false);
|
||||
setVisibleItemsCount(10);
|
||||
} else {
|
||||
setUserToggleState(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<section className="left-action">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={13} cursor="pointer" />
|
||||
) : (
|
||||
<ChevronRight size={13} cursor="pointer" />
|
||||
)}
|
||||
<Typography.Text className="title">{filter.title}</Typography.Text>
|
||||
<CheckboxFilterHeader
|
||||
title={filter.title}
|
||||
isOpen={isOpen}
|
||||
showClearAll={!!attributeValues.length}
|
||||
onToggleOpen={onToggleOpen}
|
||||
onClear={onClear}
|
||||
/>
|
||||
{isOpen && isLoading && !attributeValues.length && (
|
||||
<section className="loading">
|
||||
<Skeleton paragraph={{ rows: 4 }} />
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && !!attributeValues.length && (
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
handleClearFilterAttribute();
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
{isOpen &&
|
||||
(isLoading || isLoadingKeyValueSuggestions) &&
|
||||
!attributeValues.length && (
|
||||
<section className="loading">
|
||||
<Skeleton paragraph={{ rows: 4 }} />
|
||||
</section>
|
||||
)}
|
||||
{isOpen && !isLoading && !isLoadingKeyValueSuggestions && (
|
||||
)}
|
||||
{isOpen && !isLoading && (
|
||||
<>
|
||||
{!isEmptyStateWithDocsEnabled && (
|
||||
<section className="search">
|
||||
@@ -634,48 +125,24 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
data-testid="filter-separator"
|
||||
/>
|
||||
)}
|
||||
<div className="value">
|
||||
<Checkbox
|
||||
onChange={(checked): void =>
|
||||
onChange(value, checked === true, false)
|
||||
}
|
||||
value={currentFilterState[value]}
|
||||
disabled={isFilterDisabled}
|
||||
className="check-box"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cx(
|
||||
'checkbox-value-section',
|
||||
isFilterDisabled ? 'filter-disabled' : '',
|
||||
)}
|
||||
onClick={(): void => {
|
||||
if (isFilterDisabled) {
|
||||
return;
|
||||
}
|
||||
onChange(value, currentFilterState[value], true);
|
||||
}}
|
||||
>
|
||||
<div className={`${filter.title} label-${value}`} />
|
||||
{filter.customRendererForValue ? (
|
||||
filter.customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only'}
|
||||
</Button>
|
||||
<Button type="text" className="toggle-btn">
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<CheckboxValueRow
|
||||
value={value}
|
||||
checked={currentFilterState[value]}
|
||||
disabled={isFilterDisabled}
|
||||
title={filter.title}
|
||||
onlyButtonLabel={
|
||||
isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only'
|
||||
}
|
||||
customRendererForValue={filter.customRendererForValue}
|
||||
onCheckboxChange={(checked): void => onChange(value, checked, false)}
|
||||
onOnlyOrAllClick={(): void =>
|
||||
onChange(value, currentFilterState[value], true)
|
||||
}
|
||||
/>
|
||||
</Fragment>
|
||||
))}
|
||||
</section>
|
||||
@@ -688,10 +155,7 @@ export default function CheckboxFilter(props: ICheckboxProps): JSX.Element {
|
||||
)}
|
||||
{visibleItemsCount < attributeValues?.length && (
|
||||
<section className="show-more">
|
||||
<Typography.Text
|
||||
className="show-more-text"
|
||||
onClick={(): void => setVisibleItemsCount((prev) => prev + 10)}
|
||||
>
|
||||
<Typography.Text className="show-more-text" onClick={onShowMore}>
|
||||
Show More...
|
||||
</Typography.Text>
|
||||
</section>
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import { ChevronDown, ChevronRight } from '@signozhq/icons';
|
||||
|
||||
interface CheckboxFilterHeaderProps {
|
||||
title: string;
|
||||
isOpen: boolean;
|
||||
showClearAll: boolean;
|
||||
onToggleOpen: () => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
function CheckboxFilterHeader({
|
||||
title,
|
||||
isOpen,
|
||||
showClearAll,
|
||||
onToggleOpen,
|
||||
onClear,
|
||||
}: CheckboxFilterHeaderProps): JSX.Element {
|
||||
return (
|
||||
<section className="filter-header-checkbox" onClick={onToggleOpen}>
|
||||
<section className="left-action">
|
||||
{isOpen ? (
|
||||
<ChevronDown size={13} cursor="pointer" />
|
||||
) : (
|
||||
<ChevronRight size={13} cursor="pointer" />
|
||||
)}
|
||||
<Typography.Text className="title">{title}</Typography.Text>
|
||||
</section>
|
||||
<section className="right-action">
|
||||
{isOpen && showClearAll && (
|
||||
<Typography.Text
|
||||
className="clear-all"
|
||||
onClick={(e): void => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
onClear();
|
||||
}}
|
||||
>
|
||||
Clear All
|
||||
</Typography.Text>
|
||||
)}
|
||||
</section>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
export default CheckboxFilterHeader;
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Button } from 'antd';
|
||||
import { Checkbox } from '@signozhq/ui/checkbox';
|
||||
import { Typography } from '@signozhq/ui/typography';
|
||||
import cx from 'classnames';
|
||||
|
||||
interface CheckboxValueRowProps {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
disabled: boolean;
|
||||
title: string;
|
||||
onlyButtonLabel: string;
|
||||
customRendererForValue?: (value: string) => JSX.Element;
|
||||
onCheckboxChange: (checked: boolean) => void;
|
||||
onOnlyOrAllClick: () => void;
|
||||
}
|
||||
|
||||
function CheckboxValueRow({
|
||||
value,
|
||||
checked,
|
||||
disabled,
|
||||
title,
|
||||
onlyButtonLabel,
|
||||
customRendererForValue,
|
||||
onCheckboxChange,
|
||||
onOnlyOrAllClick,
|
||||
}: CheckboxValueRowProps): JSX.Element {
|
||||
return (
|
||||
<div className="value">
|
||||
<Checkbox
|
||||
onChange={(isChecked): void => onCheckboxChange(isChecked === true)}
|
||||
value={checked}
|
||||
disabled={disabled}
|
||||
className="check-box"
|
||||
/>
|
||||
|
||||
<div
|
||||
className={cx('checkbox-value-section', disabled ? 'filter-disabled' : '')}
|
||||
onClick={(): void => {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
onOnlyOrAllClick();
|
||||
}}
|
||||
>
|
||||
<div className={`${title} label-${value}`} />
|
||||
{customRendererForValue ? (
|
||||
customRendererForValue(value)
|
||||
) : (
|
||||
<Typography.Text className="value-string" truncate={1}>
|
||||
{String(value)}
|
||||
</Typography.Text>
|
||||
)}
|
||||
<Button type="text" className="only-btn">
|
||||
{onlyButtonLabel}
|
||||
</Button>
|
||||
<Button type="text" className="toggle-btn">
|
||||
Toggle
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CheckboxValueRow.defaultProps = {
|
||||
customRendererForValue: undefined,
|
||||
};
|
||||
|
||||
export default CheckboxValueRow;
|
||||
@@ -0,0 +1,417 @@
|
||||
/* eslint-disable sonarjs/no-identical-functions */
|
||||
import { removeKeysFromExpression } from 'components/QueryBuilderV2/utils';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { OPERATORS } from 'constants/antlrQueryConstants';
|
||||
import { getOperatorValue } from 'container/QueryBuilder/filters/QueryBuilderSearch/utils';
|
||||
import { cloneDeep, isArray } from 'lodash-es';
|
||||
import { Query, TagFilterItem } from 'types/api/queryBuilder/queryBuilderData';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
export const SELECTED_OPERATORS = [OPERATORS['='], 'in'];
|
||||
export const NON_SELECTED_OPERATORS = [OPERATORS['!='], 'not in', 'nin'];
|
||||
|
||||
// Sources that use backend APIs expecting short operator format (e.g., 'nin' instead of 'not in')
|
||||
const SOURCES_WITH_SHORT_OPERATORS = [QuickFiltersSource.INFRA_MONITORING];
|
||||
|
||||
/**
|
||||
* Returns the correct NOT_IN operator value based on source.
|
||||
* InfraMonitoring backend expects 'nin', others expect 'not in'.
|
||||
*/
|
||||
export function getNotInOperator(source: QuickFiltersSource): string {
|
||||
if (SOURCES_WITH_SHORT_OPERATORS.includes(source)) {
|
||||
return 'nin';
|
||||
}
|
||||
return getOperatorValue('NOT_IN');
|
||||
}
|
||||
|
||||
function setDefaultValues(
|
||||
values: string[],
|
||||
trueOrFalse: boolean,
|
||||
): Record<string, boolean> {
|
||||
const defaultState: Record<string, boolean> = {};
|
||||
values.forEach((val) => {
|
||||
defaultState[val] = trueOrFalse;
|
||||
});
|
||||
return defaultState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the checked/unchecked state for each attribute value by reading the
|
||||
* active filter clause for this attribute key out of the query.
|
||||
*
|
||||
* - No matching clause -> every value is checked (all selected).
|
||||
* - IN / `=` clause -> only the listed values are checked.
|
||||
* - NOT IN / `!=` clause -> every value is checked except the excluded ones.
|
||||
*/
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems,
|
||||
filterKey,
|
||||
}: {
|
||||
attributeValues: string[];
|
||||
filterItems: TagFilterItem[] | undefined;
|
||||
filterKey: string;
|
||||
}): Record<string, boolean> {
|
||||
let filterState: Record<string, boolean> = setDefaultValues(
|
||||
attributeValues,
|
||||
false,
|
||||
);
|
||||
const filterSync = filterItems?.find((item) =>
|
||||
isKeyMatch(item.key?.key, filterKey),
|
||||
);
|
||||
|
||||
if (filterSync) {
|
||||
if (SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = true;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = true;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = true;
|
||||
}
|
||||
} else if (NON_SELECTED_OPERATORS.includes(filterSync.op)) {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
if (isArray(filterSync.value)) {
|
||||
filterSync.value.forEach((val) => {
|
||||
filterState[String(val)] = false;
|
||||
});
|
||||
} else if (typeof filterSync.value === 'string') {
|
||||
filterState[filterSync.value] = false;
|
||||
} else if (typeof filterSync.value === 'boolean') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
} else if (typeof filterSync.value === 'number') {
|
||||
filterState[String(filterSync.value)] = false;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
filterState = setDefaultValues(attributeValues, true);
|
||||
}
|
||||
return filterState;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a new query with every clause for this attribute key removed, both
|
||||
* from the structured filter items and the raw filter expression.
|
||||
*/
|
||||
export function clearFilterFromQuery({
|
||||
currentQuery,
|
||||
filter,
|
||||
activeQueryIndex,
|
||||
}: {
|
||||
currentQuery: Query;
|
||||
filter: IQuickFiltersConfig;
|
||||
activeQueryIndex: number;
|
||||
}): Query {
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: currentQuery.builder.queryData.map((item, idx) => ({
|
||||
...item,
|
||||
filter: {
|
||||
expression: removeKeysFromExpression(item.filter?.expression ?? '', [
|
||||
filter.attributeKey.key,
|
||||
]),
|
||||
},
|
||||
filters: {
|
||||
...item.filters,
|
||||
items:
|
||||
idx === activeQueryIndex
|
||||
? item.filters?.items?.filter(
|
||||
(fil) => !isKeyMatch(fil.key?.key, filter.attributeKey.key),
|
||||
) || []
|
||||
: [...(item.filters?.items || [])],
|
||||
op: item.filters?.op || 'AND',
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line sonarjs/cognitive-complexity
|
||||
export function applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
isOnlyOrAllClicked,
|
||||
}: {
|
||||
currentQuery: Query;
|
||||
activeQueryIndex: number;
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
value: string;
|
||||
checked: boolean;
|
||||
isOnlyOrAllClicked: boolean;
|
||||
}): Query {
|
||||
const activeItems =
|
||||
currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items;
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = !!activeItems?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
const currentFilterState = deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems: activeItems,
|
||||
filterKey: filter.attributeKey.key,
|
||||
});
|
||||
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
const query = cloneDeep(currentQuery.builder.queryData?.[activeQueryIndex]);
|
||||
|
||||
// if only or all are clicked we do not need to worry about anything just override whatever we have
|
||||
// by either adding a new IN operator value clause in case of ONLY or remove everything we have for ALL.
|
||||
if (isOnlyOrAllClicked && query?.filters?.items) {
|
||||
const isOnlyOrAll = isSomeFilterPresentForCurrentAttribute
|
||||
? currentFilterState[value] && !isMultipleValuesTrueForTheKey
|
||||
? 'All'
|
||||
: 'Only'
|
||||
: 'Only';
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(q) => !isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(query.filter.expression, [
|
||||
filter.attributeKey.key,
|
||||
]);
|
||||
}
|
||||
|
||||
if (isOnlyOrAll === 'Only') {
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
} else if (query?.filters?.items) {
|
||||
if (
|
||||
query.filters?.items?.some((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)
|
||||
) {
|
||||
// if there is already a running filter for the current attribute key then
|
||||
// we split the cases by which particular operator is present right now!
|
||||
const currentFilter = query.filters?.items?.find((q) =>
|
||||
isKeyMatch(q.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (currentFilter) {
|
||||
const runningOperator = currentFilter?.op;
|
||||
switch (runningOperator) {
|
||||
case 'in':
|
||||
if (checked) {
|
||||
// if it's an IN operator then if we are checking another value it get's added to the
|
||||
// filter clause. example - key IN [value1, currentSelectedValue]
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// if the current state wasn't an array we make it one and add our value
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (!checked) {
|
||||
// if we are removing some value when the running operator is IN we filter.
|
||||
// example - key IN [value1,currentSelectedValue] becomes key IN [value1] in case of array
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
// if not an array remove the whole thing altogether!
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case 'nin':
|
||||
case 'not in':
|
||||
// if the current running operator is NIN then when unchecking the value it gets
|
||||
// added to the clause like key NIN [value1 , currentUnselectedValue]
|
||||
if (!checked) {
|
||||
// in case of array add the currentUnselectedValue to the list.
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [...currentFilter.value, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else {
|
||||
// in case of not an array make it one!
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else if (checked) {
|
||||
// opposite of above!
|
||||
if (isArray(currentFilter.value)) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value.filter((val) => val !== value),
|
||||
};
|
||||
if (newFilter.value.length === 0) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
if (query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
} else {
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
value: currentFilter.value === value ? null : currentFilter.value,
|
||||
};
|
||||
if (newFilter.value === null && query.filter?.expression) {
|
||||
query.filter.expression = removeKeysFromExpression(
|
||||
query.filter.expression,
|
||||
[filter.attributeKey.key],
|
||||
);
|
||||
}
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case '=':
|
||||
if (checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getOperatorValue(OPERATORS.IN),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (!checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
case '!=':
|
||||
if (!checked) {
|
||||
const newFilter = {
|
||||
...currentFilter,
|
||||
op: getNotInOperator(source),
|
||||
value: [currentFilter.value as string, value],
|
||||
};
|
||||
query.filters.items = query.filters.items.map((item) => {
|
||||
if (isKeyMatch(item.key?.key, filter.attributeKey.key)) {
|
||||
return newFilter;
|
||||
}
|
||||
return item;
|
||||
});
|
||||
} else if (checked) {
|
||||
query.filters.items = query.filters.items.filter(
|
||||
(item) => !isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// case - when there is no filter for the current key that means all are selected right now.
|
||||
const newFilterItem: TagFilterItem = {
|
||||
id: uuid(),
|
||||
op: getNotInOperator(source),
|
||||
key: filter.attributeKey,
|
||||
value,
|
||||
};
|
||||
query.filters.items = [...query.filters.items, newFilterItem];
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
...currentQuery,
|
||||
builder: {
|
||||
...currentQuery.builder,
|
||||
queryData: [
|
||||
...currentQuery.builder.queryData.map((q, idx) => {
|
||||
if (idx === activeQueryIndex) {
|
||||
return query;
|
||||
}
|
||||
return q;
|
||||
}),
|
||||
],
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { useMemo } from 'react';
|
||||
import { QuickFiltersSource } from 'components/QuickFilters/types';
|
||||
import { PANEL_TYPES } from 'constants/queryBuilder';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
/**
|
||||
* Resolves which query-builder query index the checkbox filter reads from and
|
||||
* writes to.
|
||||
*
|
||||
* In ListView most sources use index 0; TRACES_EXPLORER and every non-ListView
|
||||
* mode track the last focused query.
|
||||
*/
|
||||
function useActiveQueryIndex(source: QuickFiltersSource): number {
|
||||
const { lastUsedQuery, panelType } = useQueryBuilder();
|
||||
const isListView = panelType === PANEL_TYPES.LIST;
|
||||
|
||||
return useMemo(() => {
|
||||
if (isListView) {
|
||||
return source === QuickFiltersSource.TRACES_EXPLORER
|
||||
? lastUsedQuery || 0
|
||||
: 0;
|
||||
}
|
||||
return lastUsedQuery || 0;
|
||||
}, [isListView, source, lastUsedQuery]);
|
||||
}
|
||||
|
||||
export default useActiveQueryIndex;
|
||||
@@ -0,0 +1,90 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
const DEFAULT_VISIBLE_ITEMS_COUNT = 10;
|
||||
|
||||
interface UseCheckboxDisclosureProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
activeQueryIndex: number;
|
||||
}
|
||||
|
||||
interface UseCheckboxDisclosureReturn {
|
||||
isOpen: boolean;
|
||||
isSomeFilterPresentForCurrentAttribute: boolean;
|
||||
visibleItemsCount: number;
|
||||
onToggleOpen: () => void;
|
||||
onShowMore: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owns the open/collapsed state of a checkbox filter section and how many
|
||||
* values are visible.
|
||||
*
|
||||
* Auto-opens when the query already has a clause for this attribute, otherwise
|
||||
* falls back to `filter.defaultOpen`. An explicit user toggle always wins.
|
||||
* Collapsing resets the visible count.
|
||||
*/
|
||||
function useCheckboxDisclosure({
|
||||
filter,
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxDisclosureProps): UseCheckboxDisclosureReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
// null = no user action, true = user opened, false = user closed
|
||||
const [userToggleState, setUserToggleState] = useState<boolean | null>(null);
|
||||
const [visibleItemsCount, setVisibleItemsCount] = useState<number>(
|
||||
DEFAULT_VISIBLE_ITEMS_COUNT,
|
||||
);
|
||||
|
||||
const isSomeFilterPresentForCurrentAttribute = useMemo(
|
||||
() =>
|
||||
!!currentQuery.builder.queryData?.[activeQueryIndex]?.filters?.items?.some(
|
||||
(item) => isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
),
|
||||
[currentQuery.builder.queryData, activeQueryIndex, filter.attributeKey.key],
|
||||
);
|
||||
|
||||
const isOpen = useMemo(() => {
|
||||
// If user explicitly toggled, respect that
|
||||
if (userToggleState !== null) {
|
||||
return userToggleState;
|
||||
}
|
||||
|
||||
// Auto-open if this filter has active filters in the query
|
||||
if (isSomeFilterPresentForCurrentAttribute) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Otherwise use default behavior (first 2 filters open)
|
||||
return filter.defaultOpen;
|
||||
}, [
|
||||
userToggleState,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
filter.defaultOpen,
|
||||
]);
|
||||
|
||||
const onToggleOpen = (): void => {
|
||||
if (isOpen) {
|
||||
setUserToggleState(false);
|
||||
setVisibleItemsCount(DEFAULT_VISIBLE_ITEMS_COUNT);
|
||||
} else {
|
||||
setUserToggleState(true);
|
||||
}
|
||||
};
|
||||
|
||||
const onShowMore = (): void => {
|
||||
setVisibleItemsCount((prev) => prev + DEFAULT_VISIBLE_ITEMS_COUNT);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isSomeFilterPresentForCurrentAttribute,
|
||||
visibleItemsCount,
|
||||
onToggleOpen,
|
||||
onShowMore,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxDisclosure;
|
||||
@@ -0,0 +1,78 @@
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
import { isFunction } from 'lodash-es';
|
||||
import { Query } from 'types/api/queryBuilder/queryBuilderData';
|
||||
|
||||
import {
|
||||
applyCheckboxToggle,
|
||||
clearFilterFromQuery,
|
||||
} from './checkboxFilterQuery';
|
||||
|
||||
interface UseCheckboxFilterActionsProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
onFilterChange?: ((query: Query) => void) | null;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterActionsReturn {
|
||||
onChange: (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
) => void;
|
||||
onClear: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires the pure checkbox query algebra to query-builder dispatch: the
|
||||
* caller-provided `onFilterChange` when present, otherwise a URL redirect.
|
||||
*/
|
||||
function useCheckboxFilterActions({
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
onFilterChange,
|
||||
}: UseCheckboxFilterActionsProps): UseCheckboxFilterActionsReturn {
|
||||
const { currentQuery, redirectWithQueryBuilderData } = useQueryBuilder();
|
||||
|
||||
const dispatch = (query: Query): void => {
|
||||
if (onFilterChange && isFunction(onFilterChange)) {
|
||||
onFilterChange(query);
|
||||
} else {
|
||||
redirectWithQueryBuilderData(query);
|
||||
}
|
||||
};
|
||||
|
||||
const onChange = (
|
||||
value: string,
|
||||
checked: boolean,
|
||||
isOnlyOrAllClicked: boolean,
|
||||
): void => {
|
||||
dispatch(
|
||||
applyCheckboxToggle({
|
||||
currentQuery,
|
||||
activeQueryIndex,
|
||||
filter,
|
||||
source,
|
||||
attributeValues,
|
||||
value,
|
||||
checked,
|
||||
isOnlyOrAllClicked,
|
||||
}),
|
||||
);
|
||||
};
|
||||
|
||||
const onClear = (): void => {
|
||||
dispatch(clearFilterFromQuery({ currentQuery, filter, activeQueryIndex }));
|
||||
};
|
||||
|
||||
return { onChange, onClear };
|
||||
}
|
||||
|
||||
export default useCheckboxFilterActions;
|
||||
@@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react';
|
||||
import { IQuickFiltersConfig } from 'components/QuickFilters/types';
|
||||
import { useQueryBuilder } from 'hooks/queryBuilder/useQueryBuilder';
|
||||
|
||||
import { deriveCheckboxState } from './checkboxFilterQuery';
|
||||
import { isKeyMatch } from './utils';
|
||||
|
||||
interface UseCheckboxFilterStateProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
attributeValues: string[];
|
||||
activeQueryIndex: number;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterStateReturn {
|
||||
currentFilterState: Record<string, boolean>;
|
||||
isFilterDisabled: boolean;
|
||||
isMultipleValuesTrueForTheKey: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the active query and derives the per-value checked state for this
|
||||
* attribute, whether the filter is disabled (same key used more than once in
|
||||
* the filter bar), and whether more than one value is currently selected.
|
||||
*/
|
||||
function useCheckboxFilterState({
|
||||
filter,
|
||||
attributeValues,
|
||||
activeQueryIndex,
|
||||
}: UseCheckboxFilterStateProps): UseCheckboxFilterStateReturn {
|
||||
const { currentQuery } = useQueryBuilder();
|
||||
|
||||
// derive the state of each filter key here and keep it in sync with current query
|
||||
const currentFilterState = useMemo(
|
||||
() =>
|
||||
deriveCheckboxState({
|
||||
attributeValues,
|
||||
filterItems:
|
||||
currentQuery?.builder.queryData?.[activeQueryIndex]?.filters?.items,
|
||||
filterKey: filter.attributeKey.key,
|
||||
}),
|
||||
[
|
||||
attributeValues,
|
||||
currentQuery?.builder.queryData,
|
||||
filter.attributeKey,
|
||||
activeQueryIndex,
|
||||
],
|
||||
);
|
||||
|
||||
// disable the filter when there are multiple entries of the same attribute key present in the filter bar
|
||||
const isFilterDisabled = useMemo(
|
||||
() =>
|
||||
(currentQuery?.builder?.queryData?.[
|
||||
activeQueryIndex
|
||||
]?.filters?.items?.filter((item) =>
|
||||
isKeyMatch(item.key?.key, filter.attributeKey.key),
|
||||
)?.length || 0) > 1,
|
||||
[currentQuery?.builder?.queryData, activeQueryIndex, filter.attributeKey],
|
||||
);
|
||||
|
||||
// whether the current filter has multiple values to its name in the key op value section
|
||||
const isMultipleValuesTrueForTheKey =
|
||||
Object.values(currentFilterState).filter((val) => val).length > 1;
|
||||
|
||||
return {
|
||||
currentFilterState,
|
||||
isFilterDisabled,
|
||||
isMultipleValuesTrueForTheKey,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxFilterState;
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useMemo } from 'react';
|
||||
import {
|
||||
IQuickFiltersConfig,
|
||||
QuickFiltersSource,
|
||||
} from 'components/QuickFilters/types';
|
||||
import { DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY } from 'constants/queryBuilder';
|
||||
import { useGetAggregateValues } from 'hooks/queryBuilder/useGetAggregateValues';
|
||||
import { useGetQueryKeyValueSuggestions } from 'hooks/querySuggestions/useGetQueryKeyValueSuggestions';
|
||||
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
|
||||
import { DataSource } from 'types/common/queryBuilder';
|
||||
|
||||
interface UseCheckboxFilterValuesProps {
|
||||
filter: IQuickFiltersConfig;
|
||||
source: QuickFiltersSource;
|
||||
searchText: string;
|
||||
isOpen: boolean;
|
||||
}
|
||||
|
||||
interface UseCheckboxFilterValuesReturn {
|
||||
attributeValues: string[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
function useCheckboxFilterValues({
|
||||
filter,
|
||||
source,
|
||||
searchText,
|
||||
isOpen,
|
||||
}: UseCheckboxFilterValuesProps): UseCheckboxFilterValuesReturn {
|
||||
const { data, isLoading } = useGetAggregateValues(
|
||||
{
|
||||
aggregateOperator: filter.aggregateOperator || 'noop',
|
||||
dataSource: filter.dataSource || DataSource.LOGS,
|
||||
aggregateAttribute: filter.aggregateAttribute || '',
|
||||
attributeKey: filter.attributeKey.key,
|
||||
filterAttributeKeyDataType: filter.attributeKey.dataType || DataTypes.EMPTY,
|
||||
tagType: filter.attributeKey.type || '',
|
||||
searchText: searchText ?? '',
|
||||
},
|
||||
{
|
||||
enabled: isOpen && source !== QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: keyValueSuggestions, isLoading: isLoadingKeyValueSuggestions } =
|
||||
useGetQueryKeyValueSuggestions({
|
||||
key: filter.attributeKey.key,
|
||||
signal: filter.dataSource || DataSource.LOGS,
|
||||
signalSource: 'meter',
|
||||
options: {
|
||||
enabled: isOpen && source === QuickFiltersSource.METER_EXPLORER,
|
||||
keepPreviousData: true,
|
||||
},
|
||||
});
|
||||
|
||||
const attributeValues: string[] = useMemo(() => {
|
||||
const dataType = filter.attributeKey.dataType || DataTypes.String;
|
||||
|
||||
if (source === QuickFiltersSource.METER_EXPLORER && keyValueSuggestions) {
|
||||
// Process the response data
|
||||
const responseData = keyValueSuggestions?.data as any;
|
||||
const values = responseData.data?.values || {};
|
||||
const stringValues = values.stringValues || [];
|
||||
const numberValues = values.numberValues || [];
|
||||
|
||||
// Generate options from string values - explicitly handle empty strings
|
||||
const stringOptions = stringValues
|
||||
// Strict filtering for empty string - we'll handle it as a special case if needed
|
||||
.filter(
|
||||
(value: string | null | undefined): value is string =>
|
||||
value !== null && value !== undefined && value !== '',
|
||||
);
|
||||
|
||||
// Generate options from number values
|
||||
const numberOptions = numberValues
|
||||
.filter(
|
||||
(value: number | null | undefined): value is number =>
|
||||
value !== null && value !== undefined,
|
||||
)
|
||||
.map((value: number) => value.toString());
|
||||
|
||||
// Combine all options and make sure we don't have duplicate labels
|
||||
return [...stringOptions, ...numberOptions];
|
||||
}
|
||||
|
||||
const key = DATA_TYPE_VS_ATTRIBUTE_VALUES_KEY[dataType];
|
||||
return (data?.payload?.[key] || []).filter(
|
||||
(val) => val !== undefined && val !== null,
|
||||
);
|
||||
}, [data?.payload, filter.attributeKey.dataType, keyValueSuggestions, source]);
|
||||
|
||||
return {
|
||||
attributeValues,
|
||||
isLoading: isLoading || isLoadingKeyValueSuggestions,
|
||||
};
|
||||
}
|
||||
|
||||
export default useCheckboxFilterValues;
|
||||
@@ -51,6 +51,26 @@ func (provider *provider) addSpanMapperRoutes(router *mux.Router) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/test", handler.New(
|
||||
provider.authzMiddleware.ViewAccess(provider.spanMapperHandler.TestMappers),
|
||||
handler.OpenAPIDef{
|
||||
ID: "TestSpanMappers",
|
||||
Tags: []string{"spanmapper"},
|
||||
Summary: "Test span mappers against sample spans",
|
||||
Description: "Tests how span mappers would transform sample spans",
|
||||
Request: new(spantypes.PostableSpanMapperTest),
|
||||
RequestContentType: "application/json",
|
||||
Response: new(spantypes.GettableSpanMapperTest),
|
||||
ResponseContentType: "application/json",
|
||||
SuccessStatusCode: http.StatusOK,
|
||||
ErrorStatusCodes: []int{http.StatusBadRequest, http.StatusNotFound},
|
||||
Deprecated: false,
|
||||
SecuritySchemes: newSecuritySchemes(types.RoleViewer),
|
||||
},
|
||||
)).Methods(http.MethodPost).GetError(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := router.Handle("/api/v1/span_mapper_groups/{groupId}", handler.New(
|
||||
provider.authzMiddleware.AdminAccess(provider.spanMapperHandler.UpdateGroup),
|
||||
handler.OpenAPIDef{
|
||||
|
||||
@@ -273,6 +273,35 @@ func (h *handler) DeleteMapper(rw http.ResponseWriter, r *http.Request) {
|
||||
render.Success(rw, http.StatusNoContent, nil)
|
||||
}
|
||||
|
||||
// TestMappers handles POST /api/v1/span_mapper_groups/test.
|
||||
func (h *handler) TestMappers(rw http.ResponseWriter, r *http.Request) {
|
||||
ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
claims, err := authtypes.ClaimsFromContext(ctx)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
orgID := valuer.MustNewUUID(claims.OrgID)
|
||||
|
||||
req := new(spantypes.PostableSpanMapperTest)
|
||||
if err := binding.JSON.BindBody(r.Body, req); err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
groups := spantypes.NewSpanMapperGroupsWithMappersFromPostable(orgID, req.Groups)
|
||||
out, err := h.module.TestMappers(ctx, orgID, req.Spans, groups)
|
||||
if err != nil {
|
||||
render.Error(rw, err)
|
||||
return
|
||||
}
|
||||
|
||||
render.Success(rw, http.StatusOK, &spantypes.GettableSpanMapperTest{Spans: out})
|
||||
}
|
||||
|
||||
// groupIDFromPath extracts and validates the {id} or {groupId} path variable.
|
||||
func groupIDFromPath(r *http.Request) (valuer.UUID, error) {
|
||||
vars := mux.Vars(r)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/modules/spanmapper"
|
||||
"github.com/SigNoz/signoz/pkg/query-service/agentConf"
|
||||
"github.com/SigNoz/signoz/pkg/types/opamptypes"
|
||||
@@ -102,6 +103,54 @@ func (module *module) DeleteMapper(ctx context.Context, orgID, groupID, id value
|
||||
return nil
|
||||
}
|
||||
|
||||
func (module *module) TestMappers(ctx context.Context, orgID valuer.UUID, spans []spantypes.SpanMapperTestSpan, groups []*spantypes.SpanMapperGroupWithMappers) ([]spantypes.SpanMapperTestSpan, error) {
|
||||
if len(spans) == 0 {
|
||||
return nil, errors.New(errors.TypeInvalidInput, spantypes.ErrCodeMappingInvalidInput, "'spans' must contain at least one span")
|
||||
}
|
||||
|
||||
_, err := module.backfillMappers(ctx, orgID, groups)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// out, _, err := spantypes.SimulateSpanMappersProcessing(ctx, resolved, spans)
|
||||
// if err != nil {
|
||||
// return nil, err
|
||||
// }
|
||||
// return out, nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// backfillMappers loads saved mappers for any group whose Mappers is nil.
|
||||
func (module *module) backfillMappers(ctx context.Context, orgID valuer.UUID, groups []*spantypes.SpanMapperGroupWithMappers) ([]*spantypes.SpanMapperGroupWithMappers, error) {
|
||||
// Load all the saved groups for this org, so we can look up by name.
|
||||
savedGroups, err := module.store.ListGroups(ctx, orgID, &spantypes.ListSpanMapperGroupsQuery{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
savedByName := make(map[string]*spantypes.SpanMapperGroup, len(savedGroups))
|
||||
for _, g := range savedGroups {
|
||||
savedByName[g.Name] = g
|
||||
}
|
||||
|
||||
// For each group in the request, if Mappers is nil, load the saved mappers for that group name.
|
||||
for _, g := range groups {
|
||||
if g.Mappers != nil {
|
||||
continue
|
||||
}
|
||||
saved, ok := savedByName[g.Group.Name]
|
||||
if !ok {
|
||||
return nil, errors.Newf(errors.TypeInvalidInput, spantypes.ErrCodeMappingGroupNotFound, "no saved group named %q to load mappers from; send 'mappers' for new or edited groups", g.Group.Name)
|
||||
}
|
||||
loaded, err := module.store.ListMappers(ctx, orgID, saved.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
g.Mappers = loaded
|
||||
}
|
||||
return groups, nil
|
||||
}
|
||||
|
||||
func (module *module) AgentFeatureType() agentConf.AgentFeatureType {
|
||||
return spantypes.SpanAttrMappingFeatureType
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@ type Module interface {
|
||||
CreateMapper(ctx context.Context, orgID, groupID valuer.UUID, mapper *spantypes.SpanMapper) error
|
||||
UpdateMapper(ctx context.Context, orgID, groupID, id valuer.UUID, fieldContext spantypes.FieldContext, config *spantypes.SpanMapperConfig, enabled *bool, updatedBy string) error
|
||||
DeleteMapper(ctx context.Context, orgID, groupID, id valuer.UUID) error
|
||||
TestMappers(ctx context.Context, orgID valuer.UUID, spans []spantypes.SpanMapperTestSpan, groups []*spantypes.SpanMapperGroupWithMappers) ([]spantypes.SpanMapperTestSpan, error)
|
||||
}
|
||||
|
||||
// Handler defines the HTTP handler interface for mapping group and mapper endpoints.
|
||||
@@ -42,4 +43,5 @@ type Handler interface {
|
||||
CreateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
UpdateMapper(rw http.ResponseWriter, r *http.Request)
|
||||
DeleteMapper(rw http.ResponseWriter, r *http.Request)
|
||||
TestMappers(rw http.ResponseWriter, r *http.Request)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,11 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
req.Start = querybuilder.ToMilliSecs(req.Start)
|
||||
req.End = querybuilder.ToMilliSecs(req.End)
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
event := &qbtypes.QBEvent{
|
||||
Version: "v5",
|
||||
NumberOfQueries: len(req.CompositeQuery.Queries),
|
||||
@@ -111,6 +116,9 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
// We need to set if it is unspecified or adjust it if value is not within recommended range
|
||||
intervalWarnings := q.adjustStepInterval(req.CompositeQuery.Queries, req.Start, req.End)
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
missingMetricQueries, metricWarnings, err := q.resolveMetricMetadata(ctx, req.CompositeQuery.Queries, req.Start, req.End)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -120,64 +128,6 @@ func (q *querier) QueryRange(ctx context.Context, orgID valuer.UUID, req *qbtype
|
||||
missingMetricQuerySet[name] = true
|
||||
}
|
||||
|
||||
queries, steps, err := q.buildQueries(req, dependencyQueries, missingMetricQuerySet, event)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) buildQueries(
|
||||
req *qbtypes.QueryRangeRequest,
|
||||
dependencyQueries map[string]bool,
|
||||
missingMetricQuerySet map[string]bool,
|
||||
event *qbtypes.QBEvent,
|
||||
) (map[string]qbtypes.Query, map[string]qbtypes.Step, error) {
|
||||
|
||||
tmplVars := req.Variables
|
||||
if tmplVars == nil {
|
||||
tmplVars = make(map[string]qbtypes.VariableItem)
|
||||
}
|
||||
|
||||
queries := make(map[string]qbtypes.Query)
|
||||
steps := make(map[string]qbtypes.Step)
|
||||
|
||||
for _, query := range req.CompositeQuery.Queries {
|
||||
queryName := query.GetQueryName()
|
||||
|
||||
@@ -190,7 +140,7 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypePromQL:
|
||||
promQuery, ok := query.Spec.(qbtypes.PromQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid promql query spec %T", query.Spec)
|
||||
}
|
||||
promqlQuery := newPromqlQuery(q.logger, q.promEngine, promQuery, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[promQuery.Name] = promqlQuery
|
||||
@@ -198,14 +148,14 @@ func (q *querier) buildQueries(
|
||||
case qbtypes.QueryTypeClickHouseSQL:
|
||||
chQuery, ok := query.Spec.(qbtypes.ClickHouseQuery)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid clickhouse query spec %T", query.Spec)
|
||||
}
|
||||
chSQLQuery := newchSQLQuery(q.logger, q.telemetryStore, chQuery, nil, qbtypes.TimeRange{From: req.Start, To: req.End}, req.RequestType, tmplVars)
|
||||
queries[chQuery.Name] = chSQLQuery
|
||||
case qbtypes.QueryTypeTraceOperator:
|
||||
traceOpQuery, ok := query.Spec.(qbtypes.QueryBuilderTraceOperator)
|
||||
if !ok {
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "invalid trace operator query spec %T", query.Spec)
|
||||
}
|
||||
toq := &traceOperatorQuery{
|
||||
telemetryStore: q.telemetryStore,
|
||||
@@ -258,12 +208,46 @@ func (q *querier) buildQueries(
|
||||
queries[spec.Name] = bq
|
||||
steps[spec.Name] = spec.StepInterval
|
||||
default:
|
||||
return nil, nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
return nil, errors.NewInvalidInputf(errors.CodeInvalidInput, "unsupported builder spec type %T", query.Spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return queries, steps, nil
|
||||
preseededResults := make(map[string]any)
|
||||
for _, name := range missingMetricQueries {
|
||||
switch req.RequestType {
|
||||
case qbtypes.RequestTypeTimeSeries:
|
||||
preseededResults[name] = &qbtypes.TimeSeriesData{QueryName: name}
|
||||
case qbtypes.RequestTypeScalar:
|
||||
preseededResults[name] = &qbtypes.ScalarData{QueryName: name}
|
||||
case qbtypes.RequestTypeRaw:
|
||||
preseededResults[name] = &qbtypes.RawData{QueryName: name}
|
||||
}
|
||||
}
|
||||
qbResp, qbErr := q.run(ctx, orgID, queries, req, steps, event, preseededResults)
|
||||
if qbResp != nil {
|
||||
qbResp.QBEvent = event
|
||||
if len(intervalWarnings) != 0 && req.RequestType == qbtypes.RequestTypeTimeSeries {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{
|
||||
Warnings: make([]qbtypes.QueryWarnDataAdditional, len(intervalWarnings)),
|
||||
}
|
||||
for idx := range intervalWarnings {
|
||||
qbResp.Warning.Warnings[idx] = qbtypes.QueryWarnDataAdditional{Message: intervalWarnings[idx]}
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(metricWarnings) > 0 {
|
||||
if qbResp.Warning == nil {
|
||||
qbResp.Warning = &qbtypes.QueryWarnData{}
|
||||
}
|
||||
for _, w := range metricWarnings {
|
||||
qbResp.Warning.Warnings = append(qbResp.Warning.Warnings, qbtypes.QueryWarnDataAdditional{
|
||||
Message: w,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return qbResp, qbErr
|
||||
}
|
||||
|
||||
func (q *querier) populateQBEvent(event *qbtypes.QBEvent, queries []qbtypes.QueryEnvelope) {
|
||||
|
||||
151
pkg/types/spantypes/spanmappersimulator.go
Normal file
151
pkg/types/spantypes/spanmappersimulator.go
Normal file
@@ -0,0 +1,151 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"go.opentelemetry.io/collector/pdata/ptrace"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrCodeProcessorFactoryMapFailed = errors.MustNewCode("processor_factory_map_failed")
|
||||
ErrCodeSpanMapperSimulationFailed = errors.MustNewCode("span_mapper_simulation_failed")
|
||||
)
|
||||
|
||||
// spanInputOrderAttr tags each input span with its index so the simulator
|
||||
// output can be sorted back into input order. The collector simulator does
|
||||
// not guarantee that traces come out in the order they went in.
|
||||
const spanInputOrderAttr = "__signoz_input_idx__"
|
||||
|
||||
// SimulateSpanMappersProcessing runs the given spans through an in-memory
|
||||
// collector pipeline that hosts signozspanmapperprocessor configured by the
|
||||
// supplied groups, and returns the transformed spans. Mirrors
|
||||
// SimulatePipelinesProcessing in pkg/query-service/app/logparsingpipeline.
|
||||
// func SimulateSpanMappersProcessing(
|
||||
// ctx context.Context,
|
||||
// groups []*SpanMapperGroupWithMappers,
|
||||
// spans []SpanMapperTestSpan,
|
||||
// ) (
|
||||
// []SpanMapperTestSpan, []string, error,
|
||||
// ) {
|
||||
// enabled := filterEnabledGroupsWithMappers(groups)
|
||||
// if len(enabled) < 1 {
|
||||
// return spans, nil, nil
|
||||
// }
|
||||
|
||||
// for i := range spans {
|
||||
// if spans[i].Attributes == nil {
|
||||
// spans[i].Attributes = map[string]any{}
|
||||
// }
|
||||
// spans[i].Attributes[spanInputOrderAttr] = int64(i)
|
||||
// }
|
||||
// simulatorInput := SpansToPTraces(spans)
|
||||
|
||||
// processorFactories, err := otelcol.MakeFactoryMap(signozspanmapperprocessor.NewFactory())
|
||||
// if err != nil {
|
||||
// return nil, nil, errors.WrapInternalf(err, ErrCodeProcessorFactoryMapFailed, "could not construct processor factory map")
|
||||
// }
|
||||
|
||||
// configGenerator := func(baseConf []byte) ([]byte, error) {
|
||||
// return GenerateCollectorConfigWithSpanMapperProcessor(baseConf, enabled)
|
||||
// }
|
||||
|
||||
// // signozspanmapperprocessor does no batching; spans flow through immediately.
|
||||
// timeout := 200 * time.Millisecond
|
||||
|
||||
// outputTraces, collectorErrs, simErr := collectorsimulator.SimulateTracesProcessing(
|
||||
// ctx,
|
||||
// processorFactories,
|
||||
// configGenerator,
|
||||
// simulatorInput,
|
||||
// timeout,
|
||||
// )
|
||||
// if simErr != nil {
|
||||
// if errors.Is(simErr, collectorsimulator.ErrInvalidConfig) {
|
||||
// return nil, nil, errors.WrapInvalidInputf(simErr, errors.CodeInvalidInput, "invalid config")
|
||||
// }
|
||||
// return nil, nil, errors.WrapInternalf(simErr, ErrCodeSpanMapperSimulationFailed, "could not simulate span mapper processing")
|
||||
// }
|
||||
|
||||
// outputSpans := PTracesToSpans(outputTraces)
|
||||
|
||||
// sort.Slice(outputSpans, func(i, j int) bool {
|
||||
// iIdx, _ := outputSpans[i].Attributes[spanInputOrderAttr].(int64)
|
||||
// jIdx, _ := outputSpans[j].Attributes[spanInputOrderAttr].(int64)
|
||||
// return iIdx < jIdx
|
||||
// })
|
||||
// for _, s := range outputSpans {
|
||||
// delete(s.Attributes, spanInputOrderAttr)
|
||||
// }
|
||||
|
||||
// collectorWarnAndErrorLogs := []string{}
|
||||
// for _, log := range collectorErrs {
|
||||
// if log == "" || strings.Contains(log, "featuregate.go") {
|
||||
// continue
|
||||
// }
|
||||
// collectorWarnAndErrorLogs = append(collectorWarnAndErrorLogs, log)
|
||||
// }
|
||||
|
||||
// return outputSpans, collectorWarnAndErrorLogs, nil
|
||||
// }
|
||||
|
||||
// SpansToPTraces packs each input span into its own ptrace.Traces with one
|
||||
// ResourceSpans / ScopeSpans / Span carrying its attribute and resource maps.
|
||||
func SpansToPTraces(spans []SpanMapperTestSpan) []ptrace.Traces {
|
||||
result := make([]ptrace.Traces, 0, len(spans))
|
||||
for _, s := range spans {
|
||||
td := ptrace.NewTraces()
|
||||
rs := td.ResourceSpans().AppendEmpty()
|
||||
if s.Resource != nil {
|
||||
_ = rs.Resource().Attributes().FromRaw(s.Resource)
|
||||
}
|
||||
sl := rs.ScopeSpans().AppendEmpty()
|
||||
span := sl.Spans().AppendEmpty()
|
||||
if s.Attributes != nil {
|
||||
_ = span.Attributes().FromRaw(s.Attributes)
|
||||
}
|
||||
result = append(result, td)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// PTracesToSpans flattens simulator output back into SpanMapperTestSpan: one
|
||||
// entry per individual Span across all ResourceSpans / ScopeSpans.
|
||||
func PTracesToSpans(traces []ptrace.Traces) []SpanMapperTestSpan {
|
||||
result := []SpanMapperTestSpan{}
|
||||
for _, td := range traces {
|
||||
rss := td.ResourceSpans()
|
||||
for i := 0; i < rss.Len(); i++ {
|
||||
rs := rss.At(i)
|
||||
resourceAttrs := rs.Resource().Attributes().AsRaw()
|
||||
ilss := rs.ScopeSpans()
|
||||
for j := 0; j < ilss.Len(); j++ {
|
||||
spans := ilss.At(j).Spans()
|
||||
for k := 0; k < spans.Len(); k++ {
|
||||
result = append(result, SpanMapperTestSpan{
|
||||
Attributes: spans.At(k).Attributes().AsRaw(),
|
||||
Resource: resourceAttrs,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func filterEnabledGroupsWithMappers(groups []*SpanMapperGroupWithMappers) []*SpanMapperGroupWithMappers {
|
||||
out := make([]*SpanMapperGroupWithMappers, 0, len(groups))
|
||||
for _, gm := range groups {
|
||||
if gm == nil || gm.Group == nil || !gm.Group.Enabled {
|
||||
continue
|
||||
}
|
||||
enabled := make([]*SpanMapper, 0, len(gm.Mappers))
|
||||
for _, m := range gm.Mappers {
|
||||
if m != nil && m.Enabled {
|
||||
enabled = append(enabled, m)
|
||||
}
|
||||
}
|
||||
if len(enabled) > 0 {
|
||||
out = append(out, &SpanMapperGroupWithMappers{Group: gm.Group, Mappers: enabled})
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
53
pkg/types/spantypes/spanmappertest.go
Normal file
53
pkg/types/spantypes/spanmappertest.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package spantypes
|
||||
|
||||
import (
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
type SpanMapperTestSpan struct {
|
||||
Attributes map[string]any `json:"attributes"`
|
||||
Resource map[string]any `json:"resource"`
|
||||
}
|
||||
|
||||
// Mappers is optional because the module can backfill from the store by Group.Name.
|
||||
type PostableSpanMapperTestGroup struct {
|
||||
PostableSpanMapperGroup
|
||||
Mappers []PostableSpanMapper `json:"mappers"`
|
||||
}
|
||||
|
||||
type PostableSpanMapperTest struct {
|
||||
Spans []SpanMapperTestSpan `json:"spans" required:"true"`
|
||||
Groups []PostableSpanMapperTestGroup `json:"groups" required:"true"`
|
||||
}
|
||||
|
||||
type GettableSpanMapperTest struct {
|
||||
Spans []SpanMapperTestSpan `json:"spans" required:"true"`
|
||||
}
|
||||
|
||||
func NewSpanMapperGroupsWithMappersFromPostable(orgID valuer.UUID, in []PostableSpanMapperTestGroup) []*SpanMapperGroupWithMappers {
|
||||
out := make([]*SpanMapperGroupWithMappers, 0, len(in))
|
||||
for _, pg := range in {
|
||||
var mappers []*SpanMapper
|
||||
if pg.Mappers != nil {
|
||||
mappers = make([]*SpanMapper, 0, len(pg.Mappers))
|
||||
for _, pm := range pg.Mappers {
|
||||
mappers = append(mappers, &SpanMapper{
|
||||
Name: pm.Name,
|
||||
FieldContext: pm.FieldContext,
|
||||
Config: pm.Config,
|
||||
Enabled: pm.Enabled,
|
||||
})
|
||||
}
|
||||
}
|
||||
out = append(out, &SpanMapperGroupWithMappers{
|
||||
Group: &SpanMapperGroup{
|
||||
OrgID: orgID,
|
||||
Name: pg.Name,
|
||||
Condition: pg.Condition,
|
||||
Enabled: pg.Enabled,
|
||||
},
|
||||
Mappers: mappers,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz-otel-collector/exporter/jsontypeexporter"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
|
||||
@@ -22,7 +21,7 @@ const (
|
||||
// BodyJSONStringSearchPrefix is the prefix used for body JSON search queries.
|
||||
// e.g., "body.status" where "body." is the prefix.
|
||||
BodyJSONStringSearchPrefix = "body."
|
||||
ArraySep = jsontypeexporter.ArraySeparator
|
||||
ArraySep = "[]."
|
||||
ArraySepSuffix = "[]"
|
||||
// TODO(Piyush): Remove once we've migrated to the new array syntax.
|
||||
ArrayAnyIndex = "[*]."
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/SigNoz/signoz-otel-collector/exporter/jsontypeexporter"
|
||||
"github.com/SigNoz/signoz/pkg/errors"
|
||||
"github.com/SigNoz/signoz/pkg/valuer"
|
||||
)
|
||||
@@ -76,7 +75,7 @@ func (n *JSONAccessNode) Alias() string {
|
||||
parentAlias := strings.TrimLeft(n.Parent.Alias(), "`")
|
||||
parentAlias = strings.TrimRight(parentAlias, "`")
|
||||
|
||||
sep := jsontypeexporter.ArraySeparator
|
||||
sep := "[]."
|
||||
if n.Parent.isRoot {
|
||||
sep = "."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user