fix: log details filters use data types from log data response as primary data type (#8278)

* fix: log details filters use data types from log data response as primary data type

* chore: added test cases

* test: add comprehensive unit tests for chooseAutocompleteFromCustomValue function

* fix: added datatypes to util and test cases

* fix: added new tests
This commit is contained in:
Sahil Khan 2025-06-22 16:28:43 +05:30 committed by GitHub
parent bbb21f608f
commit b11a4c0c21
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 359 additions and 8 deletions

View File

@ -5,17 +5,19 @@ import cx from 'classnames';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import { FontSize } from 'container/OptionsMenu/types'; import { FontSize } from 'container/OptionsMenu/types';
import { memo, MouseEvent, ReactNode, useMemo } from 'react'; import { memo, MouseEvent, ReactNode, useMemo } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
function AddToQueryHOC({ function AddToQueryHOC({
fieldKey, fieldKey,
fieldValue, fieldValue,
onAddToQuery, onAddToQuery,
fontSize, fontSize,
dataType = DataTypes.EMPTY,
children, children,
}: AddToQueryHOCProps): JSX.Element { }: AddToQueryHOCProps): JSX.Element {
const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => { const handleQueryAdd = (event: MouseEvent<HTMLDivElement>): void => {
event.stopPropagation(); event.stopPropagation();
onAddToQuery(fieldKey, fieldValue, OPERATORS['=']); onAddToQuery(fieldKey, fieldValue, OPERATORS['='], undefined, dataType);
}; };
const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [ const popOverContent = useMemo(() => <span>Add to query: {fieldKey}</span>, [
@ -35,9 +37,20 @@ function AddToQueryHOC({
export interface AddToQueryHOCProps { export interface AddToQueryHOCProps {
fieldKey: string; fieldKey: string;
fieldValue: 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; fontSize: FontSize;
dataType?: DataTypes;
children: ReactNode; children: ReactNode;
} }
AddToQueryHOC.defaultProps = {
dataType: DataTypes.EMPTY,
};
export default memo(AddToQueryHOC); export default memo(AddToQueryHOC);

View File

@ -3,6 +3,7 @@ import { Button, Col, Popover } from 'antd';
import { OPERATORS } from 'constants/queryBuilder'; import { OPERATORS } from 'constants/queryBuilder';
import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes'; import { removeJSONStringifyQuotes } from 'lib/removeJSONStringifyQuotes';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
function ActionItem({ function ActionItem({
fieldKey, fieldKey,
@ -55,6 +56,8 @@ export interface ActionItemProps {
fieldKey: string, fieldKey: string,
fieldValue: string, fieldValue: string,
operator: string, operator: string,
isJSON?: boolean,
dataType?: DataTypes,
) => void; ) => void;
} }

View File

@ -33,7 +33,12 @@ import { DataTypes } from 'types/api/queryBuilder/queryAutocompleteResponse';
import { ActionItemProps } from './ActionItem'; import { ActionItemProps } from './ActionItem';
import FieldRenderer from './FieldRenderer'; import FieldRenderer from './FieldRenderer';
import { TableViewActions } from './TableView/TableViewActions'; import { TableViewActions } from './TableView/TableViewActions';
import { filterKeyForField, findKeyPath, flattenObject } from './utils'; import {
filterKeyForField,
findKeyPath,
flattenObject,
getFieldAttributes,
} from './utils';
interface TableViewProps { interface TableViewProps {
logData: ILog; logData: ILog;
@ -107,10 +112,17 @@ function TableView({
operator: string, operator: string,
fieldKey: string, fieldKey: string,
fieldValue: string, fieldValue: string,
dataType: string | undefined,
): void => { ): void => {
const validatedFieldValue = removeJSONStringifyQuotes(fieldValue); const validatedFieldValue = removeJSONStringifyQuotes(fieldValue);
if (onClickActionItem) { if (onClickActionItem) {
onClickActionItem(fieldKey, validatedFieldValue, operator); onClickActionItem(
fieldKey,
validatedFieldValue,
operator,
undefined,
dataType as DataTypes,
);
} }
}; };
@ -118,8 +130,9 @@ function TableView({
operator: string, operator: string,
fieldKey: string, fieldKey: string,
fieldValue: string, fieldValue: string,
dataType: string | undefined,
) => (): void => { ) => (): void => {
handleClick(operator, fieldKey, fieldValue); handleClick(operator, fieldKey, fieldValue, dataType);
if (operator === OPERATORS['=']) { if (operator === OPERATORS['=']) {
setIsFilterInLoading(true); setIsFilterInLoading(true);
} }
@ -247,6 +260,7 @@ function TableView({
} }
const fieldFilterKey = filterKeyForField(field); const fieldFilterKey = filterKeyForField(field);
const { dataType } = getFieldAttributes(field);
if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) { if (!RESTRICTED_SELECTED_FIELDS.includes(fieldFilterKey)) {
return ( return (
<AddToQueryHOC <AddToQueryHOC
@ -254,6 +268,7 @@ function TableView({
fieldValue={flattenLogData[field]} fieldValue={flattenLogData[field]}
onAddToQuery={onAddToQuery} onAddToQuery={onAddToQuery}
fontSize={FontSize.SMALL} fontSize={FontSize.SMALL}
dataType={dataType as DataTypes}
> >
{renderedField} {renderedField}
</AddToQueryHOC> </AddToQueryHOC>

View File

@ -23,6 +23,7 @@ import { DataType } from '../TableView';
import { import {
escapeHtml, escapeHtml,
filterKeyForField, filterKeyForField,
getFieldAttributes,
jsonToDataNodes, jsonToDataNodes,
parseFieldValue, parseFieldValue,
recursiveParseJSON, recursiveParseJSON,
@ -45,6 +46,7 @@ interface ITableViewActionsProps {
operator: string, operator: string,
fieldKey: string, fieldKey: string,
fieldValue: string, fieldValue: string,
dataType: string | undefined,
) => () => void; ) => () => void;
} }
@ -64,6 +66,7 @@ export function TableViewActions(
} = props; } = props;
const { pathname } = useLocation(); const { pathname } = useLocation();
const { dataType } = getFieldAttributes(record.field);
// there is no option for where clause in old logs explorer and live logs page // there is no option for where clause in old logs explorer and live logs page
const isOldLogsExplorerOrLiveLogsPage = useMemo( const isOldLogsExplorerOrLiveLogsPage = useMemo(
@ -161,6 +164,7 @@ export function TableViewActions(
OPERATORS['='], OPERATORS['='],
fieldFilterKey, fieldFilterKey,
parseFieldValue(fieldData.value), parseFieldValue(fieldData.value),
dataType,
)} )}
/> />
</Tooltip> </Tooltip>
@ -178,6 +182,7 @@ export function TableViewActions(
OPERATORS['!='], OPERATORS['!='],
fieldFilterKey, fieldFilterKey,
parseFieldValue(fieldData.value), parseFieldValue(fieldData.value),
dataType,
)} )}
/> />
</Tooltip> </Tooltip>

View File

@ -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,
});
});
});
});

View File

@ -4,21 +4,40 @@ import {
DataTypes, DataTypes,
} from 'types/api/queryBuilder/queryAutocompleteResponse'; } 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 = ( export const chooseAutocompleteFromCustomValue = (
sourceList: BaseAutocompleteData[], sourceList: BaseAutocompleteData[],
value: string, value: string,
isJSON?: boolean, isJSON?: boolean,
dataType?: DataTypes, dataType?: DataTypes | 'number',
): BaseAutocompleteData => { ): BaseAutocompleteData => {
const dataTypeToUse = getDataTypeForCustomValue(dataType);
const firstBaseAutoCompleteValue = sourceList.find( const firstBaseAutoCompleteValue = sourceList.find(
(sourceAutoComplete) => value === sourceAutoComplete.key, (sourceAutoComplete) =>
value === sourceAutoComplete.key &&
(dataType === undefined || dataTypeToUse === sourceAutoComplete.dataType),
); );
if (!firstBaseAutoCompleteValue) { if (!firstBaseAutoCompleteValue) {
return { return {
...initialAutocompleteData, ...initialAutocompleteData,
key: value, key: value,
dataType: dataType || DataTypes.EMPTY, dataType: dataTypeToUse,
isJSON, isJSON,
}; };
} }