diff --git a/frontend/src/api/trace/getSpans.ts b/frontend/src/api/trace/getSpans.ts index ffd8915563..c3bb5e0bf1 100644 --- a/frontend/src/api/trace/getSpans.ts +++ b/frontend/src/api/trace/getSpans.ts @@ -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[] = []; diff --git a/frontend/src/api/trace/getSpansAggregate.ts b/frontend/src/api/trace/getSpansAggregate.ts index 077577febf..d0cc04c2b4 100644 --- a/frontend/src/api/trace/getSpansAggregate.ts +++ b/frontend/src/api/trace/getSpansAggregate.ts @@ -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); diff --git a/frontend/src/api/trace/getTagValue.ts b/frontend/src/api/trace/getTagValue.ts index 25156d32ef..a330262049 100644 --- a/frontend/src/api/trace/getTagValue.ts +++ b/frontend/src/api/trace/getTagValue.ts @@ -11,9 +11,11 @@ const getTagValue = async ( const response = await axios.post(`/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, diff --git a/frontend/src/constants/resourceAttributes.ts b/frontend/src/constants/resourceAttributes.ts index 4e82cef590..ba61d4055a 100644 --- a/frontend/src/constants/resourceAttributes.ts +++ b/frontend/src/constants/resourceAttributes.ts @@ -8,11 +8,11 @@ export const OperatorConversions: Array<{ { label: 'IN', metricValue: '=~', - traceValue: 'in', + traceValue: 'In', }, { label: 'Not IN', metricValue: '!~', - traceValue: 'not in', + traceValue: 'NotIn', }, ]; diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx index bb9794d8e7..22c5788469 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx +++ b/frontend/src/container/Trace/Search/AllTags/Tag/TagKey.tsx @@ -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(false); const globalTime = useSelector( (state) => state.globalTime, ); @@ -18,64 +20,48 @@ function TagsKey(props: TagsKeysProps): JSX.Element { const traces = useSelector((state) => state.traces); - const [options, setOptions] = useState([]); + 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 ( ({ 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} > - + ); } diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx index 60b2d4118b..af5b51cfe2 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx +++ b/frontend/src/container/Trace/Search/AllTags/Tag/TagValue.tsx @@ -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(selectedValues[0]); + + const [localTagValue, setLocalTagValue] = useState( + getInitialLocalValue( + selectedNumberValues, + selectedBoolValues, + selectedStringValues, + ), + ); const globalReducer = useSelector( (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 ( - ({ - 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 ( + - {data && - data.payload && - data.payload.map((suggestion) => ( - - {suggestion.tagValues} - - ))} - + {selectOptions(data?.payload, tagType)?.map((suggestion) => ( + + {suggestion} + + ))} + ); } diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx b/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx index ea59972a23..f95afab615 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx +++ b/frontend/src/container/Trace/Search/AllTags/Tag/index.tsx @@ -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['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) => ( - - ))} + { + // filter out the operator that does not include supported type of the selected key + mapOperators(selectedKey).map((e) => ( + + )) + } {selectedKey[0] ? ( diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts b/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts index e604a444d7..fcc08ba38b 100644 --- a/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts +++ b/frontend/src/container/Trace/Search/AllTags/Tag/styles.ts @@ -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%; } diff --git a/frontend/src/container/Trace/Search/AllTags/Tag/utils.ts b/frontend/src/container/Trace/Search/AllTags/Tag/utils.ts new file mode 100644 index 0000000000..9faefef523 --- /dev/null +++ b/frontend/src/container/Trace/Search/AllTags/Tag/utils.ts @@ -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>, +): 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>, + selectedKeys: string[], + setLocalSelectedTags: React.Dispatch>, + 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>, + setLocalSelectedTags: React.Dispatch>, + 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), + ]); + } +} diff --git a/frontend/src/container/Trace/Search/AllTags/index.tsx b/frontend/src/container/Trace/Search/AllTags/index.tsx index 49ed2e28e8..a872fbfd1d 100644 --- a/frontend/src/container/Trace/Search/AllTags/index.tsx +++ b/frontend/src/container/Trace/Search/AllTags/index.tsx @@ -41,8 +41,10 @@ function AllTags({ ...tags, { Key: [], - Operator: 'in', - Values: [], + Operator: 'Equals', + StringValues: [], + NumberValues: [], + BoolValues: [], }, ]); }; @@ -110,7 +112,7 @@ function AllTags({ - Results will include spans with ALL the specified tags ( Rows are `anded` + Results will include spans with ALL the specified tags ( Rows are `ANDed` ) diff --git a/frontend/src/container/Trace/Search/util.ts b/frontend/src/container/Trace/Search/util.ts index 1e6dda96ca..e03a19e5bb 100644 --- a/frontend/src/container/Trace/Search/util.ts +++ b/frontend/src/container/Trace/Search/util.ts @@ -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 { @@ -7,79 +10,144 @@ interface PayloadProps { 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 => { 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['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['Operator'], + Key: [tagName], + StringValues, + NumberValues, + BoolValues, + Operator: operator as FlatArray['Operator'], }; }); - return { isError, payload: tags, }; }; +const formatValues = (values: TagValueTypes[]): string => + values.map((e) => `"${e.toString().replaceAll(/"/g, '')}"`).join(','); + export const parseTagsToQuery = (tags: Tags): PayloadProps => { 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 '); diff --git a/frontend/src/container/Trace/TraceGraphFilter/config.ts b/frontend/src/container/Trace/TraceGraphFilter/config.ts index 357f22a3ee..f887876864 100644 --- a/frontend/src/container/Trace/TraceGraphFilter/config.ts +++ b/frontend/src/container/Trace/TraceGraphFilter/config.ts @@ -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', }, ]; diff --git a/frontend/src/container/Trace/TraceGraphFilter/index.tsx b/frontend/src/container/Trace/TraceGraphFilter/index.tsx index 002fedee66..403bec1416 100644 --- a/frontend/src/container/Trace/TraceGraphFilter/index.tsx +++ b/frontend/src/container/Trace/TraceGraphFilter/index.tsx @@ -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>(); + const globalTime = useSelector( + (state) => state.globalTime, + ); + const traces = useSelector((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 ( @@ -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 { - selectedGroupBy === e.key)?.displayValue} - onChange={onClickSelectedGroupByHandler} + options={options} + value={selectedGroupByValue(selectedGroupBy, options)} + onChange={onClickSelectedGroupByHandler(options)} > - {groupBy.map( - (value): JSX.Element => ( - - ), - )} - + + ); } diff --git a/frontend/src/container/Trace/TraceGraphFilter/utils.ts b/frontend/src/container/Trace/TraceGraphFilter/utils.ts new file mode 100644 index 0000000000..72d3eb0808 --- /dev/null +++ b/frontend/src/container/Trace/TraceGraphFilter/utils.ts @@ -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; +} diff --git a/frontend/src/lib/resourceAttributes.ts b/frontend/src/lib/resourceAttributes.ts index 790877f74d..33b8fd5323 100644 --- a/frontend/src/lib/resourceAttributes.ts +++ b/frontend/src/lib/resourceAttributes.ts @@ -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: [], })); /** diff --git a/frontend/src/types/api/trace/getTagFilters.ts b/frontend/src/types/api/trace/getTagFilters.ts index 29506d1509..eeee055e75 100644 --- a/frontend/src/types/api/trace/getTagFilters.ts +++ b/frontend/src/types/api/trace/getTagFilters.ts @@ -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[]; diff --git a/frontend/src/types/api/trace/getTagValue.ts b/frontend/src/types/api/trace/getTagValue.ts index e90975d1d5..ee10041e56 100644 --- a/frontend/src/types/api/trace/getTagValue.ts +++ b/frontend/src/types/api/trace/getTagValue.ts @@ -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[]; diff --git a/frontend/src/types/reducer/trace.ts b/frontend/src/types/reducer/trace.ts index fa37924f5e..ac27a156e7 100644 --- a/frontend/src/types/reducer/trace.ts +++ b/frontend/src/types/reducer/trace.ts @@ -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'