diff --git a/frontend/src/components/Logs/AddToQueryHOC.tsx b/frontend/src/components/Logs/AddToQueryHOC.tsx index d7e2c7156e7e..a41432b26839 100644 --- a/frontend/src/components/Logs/AddToQueryHOC.tsx +++ b/frontend/src/components/Logs/AddToQueryHOC.tsx @@ -5,17 +5,19 @@ import cx from 'classnames'; import { OPERATORS } from 'constants/queryBuilder'; import { FontSize } from 'container/OptionsMenu/types'; import { memo, MouseEvent, ReactNode, useMemo } from 'react'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; function AddToQueryHOC({ fieldKey, fieldValue, onAddToQuery, fontSize, + dataType = DataTypes.EMPTY, children, }: AddToQueryHOCProps): JSX.Element { const handleQueryAdd = (event: MouseEvent): void => { event.stopPropagation(); - onAddToQuery(fieldKey, fieldValue, OPERATORS['=']); + onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType); }; const popOverContent = useMemo(() => Add to query: {fieldKey}, [ @@ -35,9 +37,20 @@ function AddToQueryHOC({ export interface AddToQueryHOCProps { fieldKey: string; fieldValue: string; - onAddToQuery: (fieldKey: string, fieldValue: string, operator: string) => void; + onAddToQuery: ( + fieldKey: string, + fieldValue: string, + operator: string, + isJSON?: boolean, + dataType?: DataTypes, + ) => void; fontSize: FontSize; + dataType?: DataTypes; children: ReactNode; } +AddToQueryHOC.defaultProps = { + dataType: DataTypes.EMPTY, +}; + export default memo(AddToQueryHOC); diff --git a/frontend/src/container/LogDetailedView/ActionItem.tsx b/frontend/src/container/LogDetailedView/ActionItem.tsx index 73a662ea91a7..2860f90f0abd 100644 --- a/frontend/src/container/LogDetailedView/ActionItem.tsx +++ b/frontend/src/container/LogDetailedView/ActionItem.tsx @@ -3,6 +3,7 @@ import { Button, Col, Popover } from 'antd'; import { OPERATORS } from 'constants/queryBuilder'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; import { memo, useCallback, useMemo } from 'react'; +import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; function ActionItem({ fieldKey, @@ -55,6 +56,8 @@ export interface ActionItemProps { fieldKey: string, fieldValue: string, operator: string, + isJSON?: boolean, + dataType?: DataTypes, ) => void; } diff --git a/frontend/src/container/LogDetailedView/TableView.tsx b/frontend/src/container/LogDetailedView/TableView.tsx index d38b11e3c477..652b3c20e8cc 100644 --- a/frontend/src/container/LogDetailedView/TableView.tsx +++ b/frontend/src/container/LogDetailedView/TableView.tsx @@ -33,7 +33,12 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse'; import { ActionItemProps } from './ActionItem'; import FieldRenderer from './FieldRenderer'; import { TableViewActions } from './TableView/TableViewActions'; -import { filterKeyForField, findKeyPath, flattenObject } from './utils'; +import { + filterKeyForField, + findKeyPath, + flattenObject, + getFieldAttributes, +} from './utils'; interface TableViewProps { logData: ILog; @@ -107,10 +112,17 @@ function TableView({ operator: string, fieldKey: string, fieldValue: string, + dataType: string | undefined, ): void => { const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); if (onClickActionItem) { - onClickActionItem(fieldKey, validatedFieldValue, operator); + onClickActionItem( + fieldKey, + validatedFieldValue, + operator, + undefined, + dataType as DataTypes, + ); } }; @@ -118,8 +130,9 @@ function TableView({ operator: string, fieldKey: string, fieldValue: string, + dataType: string | undefined, ) => (): void => { - handleClick(operator, fieldKey, fieldValue); + handleClick(operator, fieldKey, fieldValue, dataType); if (operator === OPERATORS['=']) { setIsFilterInLoading(true); } @@ -247,6 +260,7 @@ function TableView({ } const fieldFilterKey = filterKeyForField(field); + const { dataType } = getFieldAttributes(field); if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) { return ( {renderedField} diff --git a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx index ad40981a3453..749a91172d13 100644 --- a/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx +++ b/frontend/src/container/LogDetailedView/TableView/TableViewActions.tsx @@ -23,6 +23,7 @@ import { DataType } from '../TableView'; import { escapeHtml, filterKeyForField, + getFieldAttributes, jsonToDataNodes, parseFieldValue, recursiveParseJSON, @@ -45,6 +46,7 @@ interface ITableViewActionsProps { operator: string, fieldKey: string, fieldValue: string, + dataType: string | undefined, ) => () => void; } @@ -64,6 +66,7 @@ export function TableViewActions( } = props; const { pathname } = useLocation(); + const { dataType } = getFieldAttributes(record.field); // there is no option for where clause in old logs explorer and live logs page const isOldLogsExplorerOrLiveLogsPage = useMemo( @@ -161,6 +164,7 @@ export function TableViewActions( OPERATORS['='], fieldFilterKey, parseFieldValue(fieldData.value), + dataType, )} /> @@ -178,6 +182,7 @@ export function TableViewActions( OPERATORS['!='], fieldFilterKey, parseFieldValue(fieldData.value), + dataType, )} /> diff --git a/frontend/src/lib/__tests__/chooseAutocompleteFromCustomValue.test.ts b/frontend/src/lib/__tests__/chooseAutocompleteFromCustomValue.test.ts new file mode 100644 index 000000000000..8ce0798f4e9b --- /dev/null +++ b/frontend/src/lib/__tests__/chooseAutocompleteFromCustomValue.test.ts @@ -0,0 +1,296 @@ +import { initialAutocompleteData } from 'constants/queryBuilder'; +import { + BaseAutocompleteData, + DataTypes, +} from 'types/api/queryBuilder/queryAutocompleteResponse'; + +import { chooseAutocompleteFromCustomValue } from '../newQueryBuilder/chooseAutocompleteFromCustomValue'; + +describe('chooseAutocompleteFromCustomValue', () => { + // Mock source list containing various data types and edge cases + const mockSourceList = [ + { + key: 'string_key', + dataType: DataTypes.String, + isJSON: false, + }, + { + key: 'number_key', + dataType: DataTypes.Float64, + isJSON: false, + }, + { + key: 'bool_key', + dataType: DataTypes.bool, + isJSON: false, + }, + { + key: 'float_key', + dataType: DataTypes.Float64, + isJSON: false, + }, + { + key: 'unknown_key', + dataType: DataTypes.EMPTY, + isJSON: false, + }, + { + key: 'duplicate_key', + dataType: DataTypes.String, + isJSON: false, + }, + { + key: 'duplicate_key', + dataType: DataTypes.Float64, + isJSON: false, + }, + ] as BaseAutocompleteData[]; + + describe('when element with same value and same data type found in sourceList', () => { + // Test case: Perfect match - both key and dataType match an existing element + it('should return matching string element', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'string_key', + false, + 'string' as DataTypes, + ); + + expect(result).toEqual(mockSourceList[0]); + }); + + // Test case: Perfect match for numeric data type + it('should return matching number element', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'number_key', + false, + 'number', + ); + + expect(result).toEqual(mockSourceList[1]); + }); + + // Test case: Perfect match for boolean data type + it('should return matching bool element', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'bool_key', + false, + 'bool' as DataTypes, + ); + + expect(result).toEqual(mockSourceList[2]); + }); + + // Test case: Perfect match for float data type (maps to Float64) + it('should return matching float element', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'float_key', + false, + 'number', + ); + + expect(result).toEqual(mockSourceList[3]); + }); + + // Test case: Perfect match for unknown data type + it('should return matching unknown element', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'unknown_key', + false, + 'unknown' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'unknown_key', + dataType: 'unknown', + isJSON: false, + }); + }); + + // Test case: Perfect match with isJSON true in sourceList + it('should return matching element with isJSON true', () => { + const jsonSourceList = [ + { key: 'json_key', dataType: DataTypes.String, isJSON: true }, + ]; + const result = chooseAutocompleteFromCustomValue( + jsonSourceList as BaseAutocompleteData[], + 'json_key', + true, + 'string' as DataTypes, + ); + expect(result).toEqual(jsonSourceList[0]); + }); + }); + + describe('when element with same value but different data type found in sourceList', () => { + // Test case: Key exists but dataType doesn't match - should create new object + it('should return new object for string value with number dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'string_key', + false, + 'number', + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'string_key', + dataType: DataTypes.Float64, + isJSON: false, + }); + }); + + // Test case: Key exists but dataType doesn't match - should create new object + it('should return new object for number value with string dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'number_key', + false, + 'string' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'number_key', + dataType: DataTypes.String, + isJSON: false, + }); + }); + + // Test case: Key exists but dataType doesn't match - should create new object + it('should return new object for bool value with string dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'bool_key', + false, + 'string' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'bool_key', + dataType: DataTypes.String, + isJSON: false, + }); + }); + + // Test case: Duplicate key with different dataType - should return the matching one + it('should return new object for duplicate key with different dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'duplicate_key', + false, + 'number' as DataTypes, + ); + + expect(result).toEqual(mockSourceList[6]); + }); + }); + + describe('when element not found in sourceList', () => { + // Test case: New key with string dataType - should create new object + it('should return new object for string dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'new_string_key', + false, + 'string' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'new_string_key', + dataType: DataTypes.String, + isJSON: false, + }); + }); + + // Test case: New key with number dataType - should create new object with Float64 + it('should return new object for number dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'new_number_key', + false, + 'number' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'new_number_key', + dataType: DataTypes.Float64, + isJSON: false, + }); + }); + + // Test case: New key with boolean dataType - should create new object + it('should return new object for bool dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'new_bool_key', + false, + 'bool' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'new_bool_key', + dataType: DataTypes.bool, + isJSON: false, + }); + }); + + // Test case: New key with unknown dataType - should create new object with 'unknown' string + it('should return new object for unknown dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'new_unknown_key', + false, + 'unknown' as DataTypes, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'new_unknown_key', + dataType: 'unknown', + isJSON: false, + }); + }); + + // Test case: New key with undefined dataType - should create new object with EMPTY + it('should return new object for undefined dataType', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'new_undefined_key', + false, + ); + + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'new_undefined_key', + dataType: DataTypes.EMPTY, + isJSON: false, + }); + }); + + // Test case: New key with isJSON true - should create new object with isJSON true + it('should return new object with isJSON true when not found', () => { + const result = chooseAutocompleteFromCustomValue( + mockSourceList, + 'json_not_found', + true, + 'string' as DataTypes, + ); + expect(result).toEqual({ + ...initialAutocompleteData, + key: 'json_not_found', + dataType: DataTypes.String, + isJSON: true, + }); + }); + }); +}); diff --git a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts index b65c7c3ca31e..4449359e4c48 100644 --- a/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts +++ b/frontend/src/lib/newQueryBuilder/chooseAutocompleteFromCustomValue.ts @@ -4,21 +4,40 @@ import { DataTypes, } from 'types/api/queryBuilder/queryAutocompleteResponse'; +const getDataTypeForCustomValue = (dataType?: string): DataTypes => { + if (dataType === 'number') { + return DataTypes.Float64; + } + + if (dataType === 'string') { + return DataTypes.String; + } + + if (dataType === 'bool') { + return DataTypes.bool; + } + + return (dataType as DataTypes) || DataTypes.EMPTY; +}; + export const chooseAutocompleteFromCustomValue = ( sourceList: BaseAutocompleteData[], value: string, isJSON?: boolean, - dataType?: DataTypes, + dataType?: DataTypes | 'number', ): BaseAutocompleteData => { + const dataTypeToUse = getDataTypeForCustomValue(dataType); const firstBaseAutoCompleteValue = sourceList.find( - (sourceAutoComplete) => value === sourceAutoComplete.key, + (sourceAutoComplete) => + value === sourceAutoComplete.key && + (dataType === undefined || dataTypeToUse === sourceAutoComplete.dataType), ); if (!firstBaseAutoCompleteValue) { return { ...initialAutocompleteData, key: value, - dataType: dataType || DataTypes.EMPTY, + dataType: dataTypeToUse, isJSON, }; }