feat: tag filtering frontend changes (#2116)

feat: tag filtering frontend changes
This commit is contained in:
Vishal Sharma 2023-01-25 15:20:27 +05:30 committed by GitHub
parent ba6818f487
commit 05ce03e67d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 779 additions and 283 deletions

View File

@ -12,7 +12,9 @@ const getSpans = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const exclude: string[] = [];

View File

@ -30,7 +30,9 @@ const getSpanAggregate = async (
const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0],
Operator: e.Operator,
Values: e.Values,
StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
}));
const other = Object.fromEntries(props.selectedFilter);

View File

@ -11,9 +11,11 @@ const getTagValue = async (
const response = await axios.post<PayloadProps>(`/getTagValues`, {
start: props.start.toString(),
end: props.end.toString(),
tagKey: props.tagKey,
tagKey: {
Key: props.tagKey.Key,
Type: props.tagKey.Type,
},
});
return {
statusCode: 200,
error: null,

View File

@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
{
label: 'IN',
metricValue: '=~',
traceValue: 'in',
traceValue: 'In',
},
{
label: 'Not IN',
metricValue: '!~',
traceValue: 'not in',
traceValue: 'NotIn',
},
];

View File

@ -1,13 +1,15 @@
import { AutoComplete, AutoCompleteProps, Input, notification } from 'antd';
import { AutoComplete, Input } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { getTagKeyOptions, onTagKeySelect } from './utils';
function TagsKey(props: TagsKeysProps): JSX.Element {
const [selectLoading, setSelectLoading] = useState<boolean>(false);
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
@ -18,64 +20,48 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const [options, setOptions] = useState<AutoCompleteProps['options']>([]);
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
}),
cacheTime: 120000,
},
);
const onSearchHandler = useCallback(async () => {
try {
setSelectLoading(true);
const response = await getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
});
const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
if (response.statusCode === 200) {
if (response.payload === null) {
setOptions([
{
value: '',
label: 'No tags available',
},
]);
} else {
setOptions(
response.payload.map((e) => ({
value: e.tagKeys,
label: e.tagKeys,
})),
);
}
} else {
notification.error({
message: response.error || 'Something went wrong',
});
}
setSelectLoading(false);
} catch (error) {
notification.error({
message: 'Something went wrong',
});
setSelectLoading(false);
}
}, [globalTime, traces]);
const counter = useRef(0);
useEffect(() => {
if (counter.current === 0 && selectedKey.length === 0) {
counter.current = 1;
onSearchHandler();
}
}, [onSearchHandler, selectedKey.length]);
const onSelectHandler = useCallback(
(value: unknown) =>
onTagKeySelect(
value,
options,
setSelectedKey,
setLocalSelectedTags,
index,
tag,
),
[index, options, setLocalSelectedTags, tag],
);
return (
<AutoComplete
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: '100%' }}
value={selectedKey}
allowClear
disabled={isLoading}
notFoundContent="No tags available"
showSearch
options={options?.map((e) => ({
label: e.label?.toString(),
@ -85,27 +71,9 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
}
onChange={(e): void => setSelectedKey(e)}
onSelect={(value: unknown): void => {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: [value],
Operator: tag.Operator,
Values: tag.Values,
},
...tags.slice(index + 1, tags.length),
]);
}
}}
onSelect={onSelectHandler}
>
<Input disabled={selectLoading} placeholder="Please select" />
<Input placeholder="Please select" />
</AutoComplete>
);
}

View File

@ -1,81 +1,169 @@
import { Select } from 'antd';
import { BaseOptionType } from 'antd/es/select';
import getTagValue from 'api/trace/getTagValue';
import React, { useState } from 'react';
import React, { useCallback, useMemo, useState } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { AutoCompleteComponent } from './styles';
import { SelectComponent } from './styles';
import {
disableTagValue,
extractTagKey,
extractTagType,
getInitialLocalValue,
getTagValueOptions,
onTagValueChange,
selectOptions,
TagValueTypes,
} from './utils';
function TagValue(props: TagValueProps): JSX.Element {
const { tag, setLocalSelectedTags, index, tagKey } = props;
const {
Key: selectedKey,
Operator: selectedOperator,
Values: selectedValues,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const [localValue, setLocalValue] = useState<string>(selectedValues[0]);
const [localTagValue, setLocalTagValue] = useState<TagValueTypes[]>(
getInitialLocalValue(
selectedNumberValues,
selectedBoolValues,
selectedStringValues,
),
);
const globalReducer = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const tagType = useMemo(() => extractTagType(tagKey), [tagKey]);
const { isLoading, data } = useQuery(
['tagKey', globalReducer.minTime, globalReducer.maxTime, tagKey],
['tagKey', globalReducer.minTime, globalReducer.maxTime, tagKey, tagType],
{
queryFn: () =>
getTagValue({
end: globalReducer.maxTime,
start: globalReducer.minTime,
tagKey,
tagKey: {
Key: extractTagKey(tagKey),
Type: tagType,
},
}),
},
);
return (
<AutoCompleteComponent
options={data?.payload?.map((e) => ({
label: e.tagValues,
value: e.tagValues,
}))}
allowClear
defaultOpen
showSearch
filterOption={(inputValue, option): boolean =>
option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
const tagValueDisabled = useMemo(
() =>
disableTagValue(
selectedOperator,
setLocalTagValue,
selectedKey,
setLocalSelectedTags,
index,
),
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onSetLocalValue = useCallback(() => {
setLocalTagValue([]);
}, []);
const onSelectedHandler = useCallback(
(value: unknown) => {
if (
typeof value === 'number' ||
(typeof value === 'string' && !Number.isNaN(Number(value)) && value !== ' ')
) {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [],
NumberValues: [Number(value)],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
} else if (
typeof value === 'boolean' ||
value === 'true' ||
value === 'false'
) {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [value === 'true' || value === true],
},
...tags.slice(index + 1, tags.length),
]);
} else if (typeof value === 'string') {
setLocalTagValue([value]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
StringValues: [value],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
}
disabled={isLoading}
value={localValue}
onChange={(values): void => {
if (typeof values === 'string') {
setLocalValue(values);
}
}}
onSelect={(value: unknown): void => {
if (typeof value === 'string') {
setLocalValue(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKey,
Operator: selectedOperator,
Values: [value],
},
...tags.slice(index + 1, tags.length),
]);
}
}}
},
[index, selectedKey, selectedOperator, setLocalSelectedTags],
);
const onChangeHandler = useCallback(
(value: unknown) => onTagValueChange(value, setLocalTagValue),
[],
);
const getFilterOptions = useCallback(
(inputValue: string, option?: BaseOptionType): boolean => {
if (typeof option?.label === 'string') {
return option?.label.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1;
}
return false;
},
[],
);
return (
<SelectComponent
loading={isLoading}
options={getTagValueOptions(data?.payload, tagType)}
mode="tags"
allowClear
onClear={onSetLocalValue}
onDeselect={onSetLocalValue}
showSearch
filterOption={getFilterOptions}
disabled={isLoading || tagValueDisabled}
value={localTagValue}
onChange={onChangeHandler}
onSelect={onSelectedHandler}
>
{data &&
data.payload &&
data.payload.map((suggestion) => (
<Select.Option key={suggestion.tagValues} value={suggestion.tagValues}>
{suggestion.tagValues}
</Select.Option>
))}
</AutoCompleteComponent>
{selectOptions(data?.payload, tagType)?.map((suggestion) => (
<Select.Option key={suggestion.toString()} value={suggestion}>
{suggestion}
</Select.Option>
))}
</SelectComponent>
);
}

View File

@ -6,24 +6,90 @@ import { TraceReducer } from 'types/reducer/trace';
import { Container, IconContainer, SelectComponent } from './styles';
import TagsKey from './TagKey';
import TagValue from './TagValue';
import { mapOperators } from './utils';
const { Option } = Select;
type Tags = FlatArray<TraceReducer['selectedTags'], 1>['Operator'];
interface AllMenuProps {
const StringBoolNumber = ['string', 'number', 'bool'];
const Number = ['number'];
const String = ['string'];
export interface AllMenuProps {
key: Tags | '';
value: string;
supportedTypes: string[];
}
const AllMenu: AllMenuProps[] = [
export const AllMenu: AllMenuProps[] = [
{
key: 'in',
value: 'IN',
key: 'Equals',
value: 'EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'not in',
key: 'NotEquals',
value: 'NOT EQUALS',
supportedTypes: StringBoolNumber,
},
{
key: 'In',
value: 'IN',
supportedTypes: String,
},
{
key: 'NotIn',
value: 'NOT IN',
supportedTypes: String,
},
{
key: 'Exists',
value: 'EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'NotExists',
value: 'NOT EXISTS',
supportedTypes: StringBoolNumber,
},
{
key: 'GreaterThan',
value: 'GREATER THAN',
supportedTypes: Number,
},
{
key: 'LessThan',
value: 'LESS THAN',
supportedTypes: Number,
},
{
key: 'GreaterThanEquals',
value: 'GREATER THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'LessThanEquals',
value: 'LESS THAN OR EQUALS',
supportedTypes: Number,
},
{
key: 'StartsWith',
value: 'STARTS WITH',
supportedTypes: String,
},
{
key: 'NotStartsWith',
value: 'NOT STARTS WITH',
supportedTypes: String,
},
{
key: 'Contains',
value: 'CONTAINS',
supportedTypes: String,
},
{
key: 'NotContains',
value: 'NOT CONTAINS',
supportedTypes: String,
},
];
@ -38,7 +104,9 @@ function SingleTags(props: AllTagsProps): JSX.Element {
const {
Key: selectedKey,
Operator: selectedOperator,
Values: selectedValues,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
} = tag;
const onDeleteTagHandler = (index: number): void => {
@ -51,7 +119,9 @@ function SingleTags(props: AllTagsProps): JSX.Element {
...localSelectedTags.slice(0, index),
{
Key: selectedKey,
Values: selectedValues,
StringValues: selectedStringValues,
NumberValues: selectedNumberValues,
BoolValues: selectedBoolValues,
Operator: key as Tags,
},
...localSelectedTags.slice(index + 1, localSelectedTags.length),
@ -70,11 +140,14 @@ function SingleTags(props: AllTagsProps): JSX.Element {
onChange={onChangeOperatorHandler}
value={AllMenu.find((e) => e.key === selectedOperator)?.value || ''}
>
{AllMenu.map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))}
{
// filter out the operator that does not include supported type of the selected key
mapOperators(selectedKey).map((e) => (
<Option key={e.value} value={e.key}>
{e.value}
</Option>
))
}
</SelectComponent>
{selectedKey[0] ? (

View File

@ -1,4 +1,4 @@
import { AutoComplete, Select, Space } from 'antd';
import { Select, Space } from 'antd';
import styled from 'styled-components';
export const SpaceComponent = styled(Space)`
@ -7,12 +7,6 @@ export const SpaceComponent = styled(Space)`
}
`;
export const SelectComponent = styled(Select)`
&&& {
width: 100%;
}
`;
export const Container = styled(Space)`
&&& {
display: flex;
@ -37,7 +31,7 @@ export const IconContainer = styled.div`
margin-left: 1.125rem;
`;
export const AutoCompleteComponent = styled(AutoComplete)`
export const SelectComponent = styled(Select)`
&&& {
width: 100%;
}

View File

@ -0,0 +1,204 @@
import { AutoCompleteProps } from 'antd';
import { DefaultOptionType } from 'antd/es/select';
import { PayloadProps as TagKeyPayload } from 'types/api/trace/getTagFilters';
import { PayloadProps as TagValuePayload } from 'types/api/trace/getTagValue';
import { OperatorValues, Tags } from 'types/reducer/trace';
import { AllMenu, AllMenuProps } from '.';
export type TagValueTypes = string | number | boolean;
/**
* @description extract tag filters from payload
*/
export const extractTagFilters = (
payload: TagKeyPayload,
): DefaultOptionType[] => {
const tagFilters: string[] = [];
payload.stringTagKeys.forEach((element) => {
tagFilters.push(`${element}.(string)`);
});
payload.numberTagKeys.forEach((element) => {
tagFilters.push(`${element}.(number)`);
});
payload.boolTagKeys.forEach((element) => {
tagFilters.push(`${element}.(bool)`);
});
return tagFilters.map((e) => ({
value: e,
label: e,
}));
};
export const extractTagType = (tagKey: string): string => {
if (tagKey?.includes('.(string)')) {
return 'string';
}
if (tagKey?.includes('.(number)')) {
return 'number';
}
if (tagKey?.includes('.(bool)')) {
return 'bool';
}
return 'string';
};
export const extractTagKey = (tagKey: string): string => {
const tag = tagKey.split('.(');
if (tag && tag.length > 0) {
return tag[0];
}
return '';
};
export function onTagValueChange(
values: unknown,
setLocalValue: React.Dispatch<React.SetStateAction<TagValueTypes[]>>,
): void {
if (Array.isArray(values) && values.length > 0) {
if (typeof values[0] === 'number' || typeof values[0] === 'boolean') {
setLocalValue(values);
} else if (typeof values[0] === 'string') {
if (values[0] === 'true' || values[0] === 'false') {
setLocalValue([values[0] === 'true']);
} else if (values[0] !== ' ' && !Number.isNaN(Number(values[0]))) {
setLocalValue([Number(values[0])]);
} else {
setLocalValue([values[0]]);
}
}
}
}
export function disableTagValue(
selectedOperator: OperatorValues,
setLocalValue: React.Dispatch<React.SetStateAction<TagValueTypes[]>>,
selectedKeys: string[],
setLocalSelectedTags: React.Dispatch<React.SetStateAction<Tags[]>>,
index: number,
): boolean {
if (selectedOperator === 'Exists' || selectedOperator === 'NotExists') {
setLocalValue([]);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: selectedKeys,
Operator: selectedOperator,
StringValues: [],
NumberValues: [],
BoolValues: [],
},
...tags.slice(index + 1, tags.length),
]);
return true;
}
return false;
}
export function getInitialLocalValue(
selectedNumberValues: number[],
selectedBoolValues: boolean[],
selectedStringValues: string[],
): TagValueTypes[] {
if (selectedStringValues && selectedStringValues.length > 0) {
return selectedStringValues;
}
if (selectedNumberValues && selectedNumberValues.length > 0) {
return selectedNumberValues;
}
if (selectedBoolValues && selectedBoolValues.length > 0) {
return selectedBoolValues;
}
return selectedStringValues;
}
export function getTagValueOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): Array<{ label: string; value: TagValueTypes }> | undefined {
if (tagType === 'string') {
return payload?.stringTagValues?.map((e) => ({
label: e,
value: e,
}));
}
if (tagType === 'number') {
return payload?.numberTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
if (tagType === 'bool') {
return payload?.boolTagValues?.map((e) => ({
label: e.toString(),
value: e,
}));
}
return [];
}
export function getTagKeyOptions(
payload: TagKeyPayload | null | undefined,
): DefaultOptionType[] {
if (payload === null) {
return [
{
value: '',
label: 'No tags available',
},
];
}
if (payload != null) {
return extractTagFilters(payload);
}
return [];
}
export function selectOptions(
payload: TagValuePayload | null | undefined,
tagType: string,
): string[] | boolean[] | number[] | undefined {
if (tagType === 'string') {
return payload?.stringTagValues;
}
if (tagType === 'number') {
return payload?.numberTagValues;
}
if (tagType === 'bool') {
return payload?.boolTagValues;
}
return [];
}
export function mapOperators(selectedKey: string[]): AllMenuProps[] {
return AllMenu.filter((e) =>
e?.supportedTypes?.includes(extractTagType(selectedKey[0])),
);
}
export function onTagKeySelect(
value: unknown,
options: AutoCompleteProps['options'],
setSelectedKey: React.Dispatch<React.SetStateAction<string>>,
setLocalSelectedTags: React.Dispatch<React.SetStateAction<Tags[]>>,
index: number,
tag: Tags,
): void {
if (
typeof value === 'string' &&
options &&
options.find((option) => option.value === value)
) {
setSelectedKey(value);
setLocalSelectedTags((tags) => [
...tags.slice(0, index),
{
Key: [value],
Operator: tag.Operator,
StringValues: tag.StringValues,
NumberValues: tag.NumberValues,
BoolValues: tag.BoolValues,
},
...tags.slice(index + 1, tags.length),
]);
}
}

View File

@ -41,8 +41,10 @@ function AllTags({
...tags,
{
Key: [],
Operator: 'in',
Values: [],
Operator: 'Equals',
StringValues: [],
NumberValues: [],
BoolValues: [],
},
]);
};
@ -110,7 +112,7 @@ function AllTags({
</Button>
<Text ellipsis>
Results will include spans with ALL the specified tags ( Rows are `anded`
Results will include spans with ALL the specified tags ( Rows are `ANDed`
)
</Text>
</Space>

View File

@ -1,5 +1,8 @@
import { AllMenu } from 'container/Trace/Search/AllTags/Tag';
import { TraceReducer } from 'types/reducer/trace';
import { extractTagType, TagValueTypes } from './AllTags/Tag/utils';
type Tags = TraceReducer['selectedTags'];
interface PayloadProps<T> {
@ -7,79 +10,144 @@ interface PayloadProps<T> {
payload: T;
}
function extractValues(
tagType: string,
filters: string[],
isError: boolean,
): [number[], boolean[], string[], boolean] {
const StringValues: string[] = [];
const NumberValues: number[] = [];
const BoolValues: boolean[] = [];
let isErr = isError;
if (tagType === 'string') {
StringValues.push(...filters);
} else if (tagType === 'number') {
filters.forEach((element) => {
const num = Number(element);
isErr = Number.isNaN(num) ? true : isError;
NumberValues.push(num);
});
} else if (tagType === 'bool') {
filters.forEach((element) => {
if (element === 'true') {
BoolValues.push(true);
} else if (element === 'false') {
BoolValues.push(false);
} else {
isErr = true;
}
});
}
return [NumberValues, BoolValues, StringValues, isErr];
}
export const parseQueryToTags = (query: string): PayloadProps<Tags> => {
let isError = false;
// Split the query string by ' AND '
const noOfTags = query.split(' AND ');
// Map over each tag
const tags: Tags = noOfTags.map((filter) => {
const isInPresent = filter.includes('in');
const isNotInPresent = filter.includes('not in');
// Find the operator used in the filter
const operator =
AllMenu.find((e) => `${filter} `.includes(` ${e.key} `))?.key || '';
if (!isNotInPresent && !isInPresent) {
isError = true;
// Split the filter by the operator
const [tagName, tagValues] = filter.split(operator).map((e) => e.trim());
// If the operator is Exists or NotExists, then return the tag object without values
if (operator === 'Exists' || operator === 'NotExists') {
return {
Key: [tagName],
StringValues: [],
NumberValues: [],
BoolValues: [],
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
}
// Check for errors in the filter
isError = operator.length === 0 || !tagName || !tagValues ? true : isError;
const isPresentSplit = isInPresent ? 'in' : '';
// Remove the first and last brackets from the tagValues
const formattedTagValues = tagValues.slice(1, -1);
const splitBy = isNotInPresent ? 'not in' : isPresentSplit;
if (splitBy.length === 0) {
isError = true;
}
const filteredtags = filter.split(splitBy).map((e) => e.trim());
if (filteredtags.length !== 2) {
isError = true;
}
const filterForTags = filteredtags[1];
if (!filterForTags) {
isError = true;
}
const removingFirstAndLastBrackets = `${filterForTags?.slice(1, -1)}`;
const noofFilters = removingFirstAndLastBrackets
// Split the tagValues by ',' and remove any quotes
const filters = formattedTagValues
.split(',')
.map((e) => e.replaceAll(/"/g, ''));
noofFilters.forEach((e) => {
// Check for errors in the filters
filters.forEach((e) => {
const firstChar = e.charAt(0);
const lastChar = e.charAt(e.length - 1);
if (firstChar === '"' && lastChar === '"') {
isError = true;
}
isError = firstChar === '"' && lastChar === '"' ? true : isError;
});
// Extract the tag type
const tagType = extractTagType(tagName);
// Extract the values for the tag
const [NumberValues, BoolValues, StringValues, isErr] = extractValues(
tagType,
filters,
isError,
);
isError = isErr;
// Return the tag object
return {
Key: [filteredtags[0]],
Values: noofFilters,
Operator: splitBy as FlatArray<Tags, 1>['Operator'],
Key: [tagName],
StringValues,
NumberValues,
BoolValues,
Operator: operator as FlatArray<Tags, 1>['Operator'],
};
});
return {
isError,
payload: tags,
};
};
const formatValues = (values: TagValueTypes[]): string =>
values.map((e) => `"${e.toString().replaceAll(/"/g, '')}"`).join(',');
export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => {
let isError = false;
// Map over each tag
const payload = tags
.map(({ Values, Key, Operator }) => {
if (Key[0] === undefined) {
.map(({ StringValues, NumberValues, BoolValues, Key, Operator }) => {
// Check if the key of the tag is undefined
if (!Key[0]) {
isError = true;
}
if (Operator === 'Exists' || Operator === 'NotExists') {
return `${Key[0]} ${Operator}`;
}
// Check if the tag has string values
if (StringValues.length > 0) {
// Format the string values and join them with a ','
const formattedStringValues = formatValues(StringValues);
return `${Key[0]} ${Operator} (${formattedStringValues})`;
}
return `${Key[0]} ${Operator} (${Values.map(
(e) => `"${e.replaceAll(/"/g, '')}"`,
).join(',')})`;
// Check if the tag has number values
if (NumberValues.length > 0) {
// Format the number values and join them with a ','
const formattedNumberValues = formatValues(NumberValues);
return `${Key[0]} ${Operator} (${formattedNumberValues})`;
}
// Check if the tag has boolean values
if (BoolValues.length > 0) {
// Format the boolean values and join them with a ','
const formattedBoolValues = formatValues(BoolValues);
return `${Key[0]} ${Operator} (${formattedBoolValues})`;
}
return '';
})
.join(' AND ');

View File

@ -1,70 +1,72 @@
import { DefaultOptionType } from 'antd/es/select';
interface Dropdown {
key: string;
displayValue: string;
yAxisUnit?: string;
}
export const groupBy: Dropdown[] = [
export const groupBy: DefaultOptionType[] = [
{
key: '',
displayValue: 'None',
label: 'None',
value: '',
},
{
key: 'serviceName',
displayValue: 'Service Name',
label: 'Service Name',
value: 'serviceName',
},
{
displayValue: 'Operation',
key: 'operation',
label: 'Operation',
value: 'name',
},
{
displayValue: 'HTTP url',
key: 'httpUrl',
label: 'HTTP URL',
value: 'httpUrl',
},
{
displayValue: 'HTTP method',
key: 'httpMethod',
label: 'HTTP Method',
value: 'httpMethod',
},
{
displayValue: 'HTTP host',
key: 'httpHost',
label: 'HTTP Host',
value: 'httpHost',
},
{
displayValue: 'HTTP route',
key: 'httpRoute',
label: 'HTTP Route',
value: 'httpRoute',
},
{
displayValue: 'HTTP status code',
key: 'httpCode',
label: 'RPC Method',
value: 'rpcMethod',
},
{
displayValue: 'RPC Method',
key: 'rpcMethod',
label: 'Status Code',
value: 'responseStatusCode',
},
{
displayValue: 'Status Code',
key: 'responseStatusCode',
label: 'Database Name',
value: 'dbName',
},
{
displayValue: 'Database name',
key: 'dbName',
label: 'Database System',
value: 'dbSystem',
},
{
displayValue: 'Database operation',
key: 'dbSystem',
label: 'Database Operation',
value: 'dbOperation',
},
{
displayValue: 'Messaging System',
key: 'msgSystem',
label: 'Messaging System',
value: 'msgSystem',
},
{
displayValue: 'Messaging Operation',
key: 'msgOperation',
label: 'Messaging Operation',
value: 'msgOperation',
},
{
displayValue: 'Component',
key: 'component',
label: 'Component',
value: 'component',
},
];

View File

@ -1,17 +1,21 @@
import { Space } from 'antd';
import React from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { Dispatch } from 'redux';
import { AutoComplete, Input, Space } from 'antd';
import getTagFilters from 'api/trace/getTagFilter';
import React, { useMemo } from 'react';
import { useQuery } from 'react-query';
import { useSelector } from 'react-redux';
import { AppState } from 'store/reducers';
import AppActions from 'types/actions';
import {
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
} from 'types/actions/trace';
import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace';
import { functions, groupBy } from './config';
import { functions } from './config';
import { SelectComponent } from './styles';
import {
getSelectedValue,
initOptions,
onClickSelectedFunctionHandler,
onClickSelectedGroupByHandler,
selectedGroupByValue,
} from './utils';
const { Option } = SelectComponent;
@ -20,36 +24,32 @@ function TraceGraphFilter(): JSX.Element {
AppState,
TraceReducer
>((state) => state.traces);
const dispatch = useDispatch<Dispatch<AppActions>>();
const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime,
);
const traces = useSelector<AppState, TraceReducer>((state) => state.traces);
const onClickSelectedFunctionHandler = (ev: unknown): void => {
if (typeof ev === 'string') {
const selected = functions.find((e) => e.key === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_FUNCTION,
payload: {
selectedFunction: selected.key,
yAxisUnit: selected.yAxisUnit,
},
});
}
}
};
const { isLoading, data } = useQuery(
[
'getTagKeys',
globalTime.minTime,
globalTime.maxTime,
traces.selectedFilter,
traces.isFilterExclude,
],
{
queryFn: () =>
getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
}),
cacheTime: 120000,
},
);
const onClickSelectedGroupByHandler = (ev: unknown): void => {
if (typeof ev === 'string') {
const selected = groupBy.find((e) => e.key === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_GROUP_BY,
payload: {
selectedGroupBy: selected.key,
},
});
}
}
};
const options = useMemo(() => initOptions(data?.payload), [data?.payload]);
return (
<Space>
@ -59,7 +59,7 @@ function TraceGraphFilter(): JSX.Element {
dropdownMatchSelectWidth
data-testid="selectedFunction"
id="selectedFunction"
value={functions.find((e) => selectedFunction === e.key)?.displayValue}
value={getSelectedValue(selectedFunction)}
onChange={onClickSelectedFunctionHandler}
>
{functions.map((value) => (
@ -70,21 +70,16 @@ function TraceGraphFilter(): JSX.Element {
</SelectComponent>
<label htmlFor="selectedGroupBy">Group By</label>
<SelectComponent
<AutoComplete
dropdownMatchSelectWidth
id="selectedGroupBy"
data-testid="selectedGroupBy"
value={groupBy.find((e) => selectedGroupBy === e.key)?.displayValue}
onChange={onClickSelectedGroupByHandler}
options={options}
value={selectedGroupByValue(selectedGroupBy, options)}
onChange={onClickSelectedGroupByHandler(options)}
>
{groupBy.map(
(value): JSX.Element => (
<Option value={value.key} key={value.key}>
{value.displayValue}
</Option>
),
)}
</SelectComponent>
<Input disabled={isLoading} placeholder="Please select" />
</AutoComplete>
</Space>
);
}

View File

@ -0,0 +1,73 @@
import { DefaultOptionType } from 'antd/es/select';
import { ReactNode } from 'react';
import store from 'store';
import {
UPDATE_SELECTED_FUNCTION,
UPDATE_SELECTED_GROUP_BY,
} from 'types/actions/trace';
import { PayloadProps } from 'types/api/trace/getTagFilters';
import { extractTagFilters } from '../Search/AllTags/Tag/utils';
import { functions, groupBy } from './config';
export function groupByValues(
tagFilters: DefaultOptionType[],
): DefaultOptionType[] {
const result: DefaultOptionType[] = [...groupBy];
tagFilters.forEach((e) => {
result.push(e);
});
return result;
}
export function initOptions(
payload: PayloadProps | null | undefined,
): DefaultOptionType[] {
if (payload) {
return groupByValues(extractTagFilters(payload));
}
return groupBy;
}
export function onClickSelectedGroupByHandler(options: DefaultOptionType[]) {
return (ev: unknown): void => {
const { dispatch } = store;
if (typeof ev === 'string' && options) {
const selected = options.find((e) => e.value === ev);
if (selected) {
dispatch({
type: UPDATE_SELECTED_GROUP_BY,
payload: {
selectedGroupBy: selected.value ? selected.value.toString() : '',
},
});
}
}
};
}
export function onClickSelectedFunctionHandler(value: unknown): void {
const { dispatch } = store;
if (typeof value === 'string') {
const selected = functions.find((e) => e.key === value);
if (selected) {
dispatch({
type: UPDATE_SELECTED_FUNCTION,
payload: {
selectedFunction: selected.key,
yAxisUnit: selected.yAxisUnit,
},
});
}
}
}
export function selectedGroupByValue(
selectedGroupBy: string,
options: DefaultOptionType[],
): ReactNode {
return options.find((e) => selectedGroupBy === e.value)?.label;
}
export function getSelectedValue(selectedFunction: string): unknown {
return functions.find((e) => selectedFunction === e.key)?.displayValue;
}

View File

@ -43,7 +43,9 @@ export const convertRawQueriesToTraceSelectedTags = (
? [convertMetricKeyToTrace(query.tagKey)]
: (convertMetricKeyToTrace(query.tagKey) as never),
Operator: convertOperatorLabelToTraceOperator(query.operator),
Values: query.tagValue,
StringValues: query.tagValue,
NumberValues: [],
BoolValues: [],
}));
/**

View File

@ -9,8 +9,8 @@ export interface Props {
isFilterExclude: TraceReducer['isFilterExclude'];
}
interface TagsKeys {
tagKeys: string;
export interface PayloadProps {
stringTagKeys: string[];
numberTagKeys: string[];
boolTagKeys: string[];
}
export type PayloadProps = TagsKeys[];

View File

@ -3,11 +3,14 @@ import { GlobalReducer } from 'types/reducer/globalTime';
export interface Props {
start: GlobalReducer['minTime'];
end: GlobalReducer['maxTime'];
tagKey: string;
tagKey: {
Key: string;
Type: string;
};
}
interface Value {
tagValues: string;
export interface PayloadProps {
stringTagValues: string[];
numberTagValues: number[];
boolTagValues: boolean[];
}
export type PayloadProps = Value[];

View File

@ -49,15 +49,33 @@ interface SpansAggregateData {
export interface Tags {
Key: string[];
Operator: OperatorValues;
Values: string[];
StringValues: string[];
NumberValues: number[];
BoolValues: boolean[];
}
export interface TagsAPI {
Key: string;
Operator: OperatorValues;
Values: string[];
StringValues: string[];
NumberValues: number[];
BoolValues: boolean[];
}
export type OperatorValues = 'not in' | 'in';
export type OperatorValues =
| 'NotIn'
| 'In'
| 'Equals'
| 'NotEquals'
| 'Contains'
| 'NotContains'
| 'GreaterThan'
| 'Exists'
| 'NotExists'
| 'LessThan'
| 'GreaterThanEquals'
| 'LessThanEquals'
| 'StartsWith'
| 'NotStartsWith';
export type TraceFilterEnum =
| 'component'