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) => ({ const updatedSelectedTags = props.selectedTags.map((e) => ({
Key: e.Key[0], Key: e.Key[0],
Operator: e.Operator, Operator: e.Operator,
Values: e.Values, StringValues: e.StringValues,
NumberValues: e.NumberValues,
BoolValues: e.BoolValues,
})); }));
const exclude: string[] = []; const exclude: string[] = [];

View File

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

View File

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

View File

@ -8,11 +8,11 @@ export const OperatorConversions: Array<{
{ {
label: 'IN', label: 'IN',
metricValue: '=~', metricValue: '=~',
traceValue: 'in', traceValue: 'In',
}, },
{ {
label: 'Not IN', label: 'Not IN',
metricValue: '!~', 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 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 { useSelector } from 'react-redux';
import { AppState } from 'store/reducers'; import { AppState } from 'store/reducers';
import { GlobalReducer } from 'types/reducer/globalTime'; import { GlobalReducer } from 'types/reducer/globalTime';
import { TraceReducer } from 'types/reducer/trace'; import { TraceReducer } from 'types/reducer/trace';
import { getTagKeyOptions, onTagKeySelect } from './utils';
function TagsKey(props: TagsKeysProps): JSX.Element { function TagsKey(props: TagsKeysProps): JSX.Element {
const [selectLoading, setSelectLoading] = useState<boolean>(false);
const globalTime = useSelector<AppState, GlobalReducer>( const globalTime = useSelector<AppState, GlobalReducer>(
(state) => state.globalTime, (state) => state.globalTime,
); );
@ -18,64 +20,48 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
const traces = useSelector<AppState, TraceReducer>((state) => state.traces); 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 () => { const options = useMemo(() => getTagKeyOptions(data?.payload), [data]);
try {
setSelectLoading(true);
const response = await getTagFilters({
start: globalTime.minTime,
end: globalTime.maxTime,
other: Object.fromEntries(traces.selectedFilter),
isFilterExclude: traces.isFilterExclude,
});
if (response.statusCode === 200) { const onSelectHandler = useCallback(
if (response.payload === null) { (value: unknown) =>
setOptions([ onTagKeySelect(
{ value,
value: '', options,
label: 'No tags available', setSelectedKey,
}, setLocalSelectedTags,
]); index,
} else { tag,
setOptions( ),
response.payload.map((e) => ({ [index, options, setLocalSelectedTags, tag],
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]);
return ( return (
<AutoComplete <AutoComplete
dropdownClassName="certain-category-search-dropdown"
dropdownMatchSelectWidth={500}
style={{ width: '100%' }} style={{ width: '100%' }}
value={selectedKey} value={selectedKey}
allowClear allowClear
disabled={isLoading}
notFoundContent="No tags available"
showSearch showSearch
options={options?.map((e) => ({ options={options?.map((e) => ({
label: e.label?.toString(), label: e.label?.toString(),
@ -85,27 +71,9 @@ function TagsKey(props: TagsKeysProps): JSX.Element {
option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1 option?.label?.toUpperCase().indexOf(inputValue.toUpperCase()) !== -1
} }
onChange={(e): void => setSelectedKey(e)} onChange={(e): void => setSelectedKey(e)}
onSelect={(value: unknown): void => { onSelect={onSelectHandler}
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),
]);
}
}}
> >
<Input disabled={selectLoading} placeholder="Please select" /> <Input placeholder="Please select" />
</AutoComplete> </AutoComplete>
); );
} }

View File

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

View File

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

View File

@ -1,4 +1,4 @@
import { AutoComplete, Select, Space } from 'antd'; import { Select, Space } from 'antd';
import styled from 'styled-components'; import styled from 'styled-components';
export const SpaceComponent = styled(Space)` 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)` export const Container = styled(Space)`
&&& { &&& {
display: flex; display: flex;
@ -37,7 +31,7 @@ export const IconContainer = styled.div`
margin-left: 1.125rem; margin-left: 1.125rem;
`; `;
export const AutoCompleteComponent = styled(AutoComplete)` export const SelectComponent = styled(Select)`
&&& { &&& {
width: 100%; 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, ...tags,
{ {
Key: [], Key: [],
Operator: 'in', Operator: 'Equals',
Values: [], StringValues: [],
NumberValues: [],
BoolValues: [],
}, },
]); ]);
}; };
@ -110,7 +112,7 @@ function AllTags({
</Button> </Button>
<Text ellipsis> <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> </Text>
</Space> </Space>

View File

@ -1,5 +1,8 @@
import { AllMenu } from 'container/Trace/Search/AllTags/Tag';
import { TraceReducer } from 'types/reducer/trace'; import { TraceReducer } from 'types/reducer/trace';
import { extractTagType, TagValueTypes } from './AllTags/Tag/utils';
type Tags = TraceReducer['selectedTags']; type Tags = TraceReducer['selectedTags'];
interface PayloadProps<T> { interface PayloadProps<T> {
@ -7,79 +10,144 @@ interface PayloadProps<T> {
payload: 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> => { export const parseQueryToTags = (query: string): PayloadProps<Tags> => {
let isError = false; let isError = false;
// Split the query string by ' AND '
const noOfTags = query.split(' AND '); const noOfTags = query.split(' AND ');
// Map over each tag
const tags: Tags = noOfTags.map((filter) => { const tags: Tags = noOfTags.map((filter) => {
const isInPresent = filter.includes('in'); // Find the operator used in the filter
const isNotInPresent = filter.includes('not in'); const operator =
AllMenu.find((e) => `${filter} `.includes(` ${e.key} `))?.key || '';
if (!isNotInPresent && !isInPresent) { // Split the filter by the operator
isError = true; 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; // Split the tagValues by ',' and remove any quotes
const filters = formattedTagValues
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(',') .split(',')
.map((e) => e.replaceAll(/"/g, '')); .map((e) => e.replaceAll(/"/g, ''));
noofFilters.forEach((e) => { // Check for errors in the filters
filters.forEach((e) => {
const firstChar = e.charAt(0); const firstChar = e.charAt(0);
const lastChar = e.charAt(e.length - 1); const lastChar = e.charAt(e.length - 1);
isError = firstChar === '"' && lastChar === '"' ? true : isError;
if (firstChar === '"' && lastChar === '"') {
isError = true;
}
}); });
// 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 { return {
Key: [filteredtags[0]], Key: [tagName],
Values: noofFilters, StringValues,
Operator: splitBy as FlatArray<Tags, 1>['Operator'], NumberValues,
BoolValues,
Operator: operator as FlatArray<Tags, 1>['Operator'],
}; };
}); });
return { return {
isError, isError,
payload: tags, payload: tags,
}; };
}; };
const formatValues = (values: TagValueTypes[]): string =>
values.map((e) => `"${e.toString().replaceAll(/"/g, '')}"`).join(',');
export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => { export const parseTagsToQuery = (tags: Tags): PayloadProps<string> => {
let isError = false; let isError = false;
// Map over each tag
const payload = tags const payload = tags
.map(({ Values, Key, Operator }) => { .map(({ StringValues, NumberValues, BoolValues, Key, Operator }) => {
if (Key[0] === undefined) { // Check if the key of the tag is undefined
if (!Key[0]) {
isError = true; 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( // Check if the tag has number values
(e) => `"${e.replaceAll(/"/g, '')}"`, if (NumberValues.length > 0) {
).join(',')})`; // 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 '); .join(' AND ');

View File

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

View File

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

View File

@ -9,8 +9,8 @@ export interface Props {
isFilterExclude: TraceReducer['isFilterExclude']; isFilterExclude: TraceReducer['isFilterExclude'];
} }
interface TagsKeys { export interface PayloadProps {
tagKeys: string; 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 { export interface Props {
start: GlobalReducer['minTime']; start: GlobalReducer['minTime'];
end: GlobalReducer['maxTime']; end: GlobalReducer['maxTime'];
tagKey: string; tagKey: {
Key: string;
Type: string;
};
} }
interface Value { export interface PayloadProps {
tagValues: string; stringTagValues: string[];
numberTagValues: number[];
boolTagValues: boolean[];
} }
export type PayloadProps = Value[];

View File

@ -49,15 +49,33 @@ interface SpansAggregateData {
export interface Tags { export interface Tags {
Key: string[]; Key: string[];
Operator: OperatorValues; Operator: OperatorValues;
Values: string[]; StringValues: string[];
NumberValues: number[];
BoolValues: boolean[];
} }
export interface TagsAPI { export interface TagsAPI {
Key: string; Key: string;
Operator: OperatorValues; 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 = export type TraceFilterEnum =
| 'component' | 'component'