diff --git a/__tests__/utils/filter-data.spec.ts b/__tests__/utils/filter-data.spec.ts index 768e5f9..1de3bac 100644 --- a/__tests__/utils/filter-data.spec.ts +++ b/__tests__/utils/filter-data.spec.ts @@ -89,10 +89,122 @@ describe('filterData function', () => { }); it('should return unfiltered data if no filters are provided', () => { - const filters = []; + const filters: CrudFilters = []; const filteredData = filterData(filters, unstructuredData); expect(filteredData).toEqual(unstructuredData); }); + + it('filters with nested structure', () => { + const filters: CrudFilters = [ + { + field: 'status.state', operator: 'eq', value: 'Normal', + }, + { + field: 'status.state', operator: 'in', value: [ + 'InUpdate', + 'InRollback', + ], + }, + { + operator: 'and', value: [ + { + field: 'metadata.deletionTimestamp', operator: 'nnull', value: true, + }, + { + field: 'status.state', operator: 'ne', value: 'DeleteFailed', + }, + ], + }, + ]; + const data: (Unstructured & { status: { state: string; } })[] = [ + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod A', namespace: 'Namespace A' }, + status: { + state: 'Normal', + }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod B', namespace: 'Namespace B' }, + status: { + state: 'InRollback', + }, + }, + { + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod C', namespace: 'Namespace A', deletionTimestamp: 'mock time' }, + status: { + state: 'InUpgrade', + }, + }, + ]; + + expect(filterData([ + { + operator: 'and', + value: [ + filters[0], + ], + }, + ], data)).toEqual([{ + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod A', namespace: 'Namespace A' }, + status: { + state: 'Normal', + }, + }]); + + expect(filterData([ + { + operator: 'and', + value: [ + filters[1], + ], + }, + ], data)).toEqual([{ + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod B', namespace: 'Namespace B' }, + status: { + state: 'InRollback', + }, + }]); + + expect(filterData([ + { + operator: 'and', + value: [ + filters[2], + ], + }, + ], data)).toEqual([{ + apiVersion: 'v1', + kind: 'Pod', + metadata: { name: 'Pod C', namespace: 'Namespace A', deletionTimestamp: 'mock time' }, + status: { + state: 'InUpgrade', + }, + },]); + + expect(filterData([ + { + operator: 'and', + value: filters, + }, + ], data)).toEqual([]); + + expect(filterData([ + { + operator: 'or', + value: filters, + }, + ], data)).toEqual(data); + }); }); describe('evaluateFilter function', () => { @@ -107,8 +219,8 @@ describe('evaluateFilter function', () => { total: 10, labels: ['label-1', 'label-2'], description: null, - type: 'type-1' - } + type: 'type-1', + }, } as Unstructured; test('handles "eq" operator', () => { @@ -190,13 +302,13 @@ describe('evaluateFilter function', () => { }); test('handles "null" operator', () => { - const result = evaluateFilter(mockItem, 'spec.description', 'null', null); - expect(result).toBeTruthy(); + expect(evaluateFilter(mockItem, 'spec.description', 'null', null)).toBeTruthy(); + expect(evaluateFilter(mockItem, 'spec.non_exist_path', 'null', null)).toBeFalsy(); }); test('handles "nnull" operator', () => { - const result = evaluateFilter(mockItem, 'spec.total', 'nnull', null); - expect(result).toBeTruthy(); + expect(evaluateFilter(mockItem, 'spec.total', 'nnull', null)).toBeTruthy(); + expect(evaluateFilter(mockItem, 'spec.non_exist_path', 'null', null)).toBeFalsy(); }); test('handles "startswith" operator', () => { @@ -238,4 +350,4 @@ describe('evaluateFilter function', () => { const result = evaluateFilter(mockItem, 'metadata.name', 'nendswiths', 'SERVICE'); expect(result).toBeTruthy(); }); -}) +}); diff --git a/src/utils/filter-data.ts b/src/utils/filter-data.ts index 5394c1b..de8b2aa 100644 --- a/src/utils/filter-data.ts +++ b/src/utils/filter-data.ts @@ -1,39 +1,33 @@ -import { CrudFilters, CrudOperators, LogicalFilter } from '@refinedev/core'; +import { CrudFilter, CrudFilters, CrudOperators } from '@refinedev/core'; import _ from 'lodash'; import { Unstructured } from '../kube-api'; +function deepFilter(item: Unstructured, filter: CrudFilter): boolean { + if ('field' in filter) { + // Logical filter + const { field, operator, value } = filter; + return evaluateFilter(item, field, operator, value); + } else { + // Conditional filter + const { operator, value } = filter; + if (operator === 'or') { + return value.some(subFilter => deepFilter(item, subFilter)); + } else if (operator === 'and') { + return value.every(subFilter => deepFilter(item, subFilter)); + } + } + return true; +} + export const filterData = ( filters: CrudFilters, - data: Unstructured[] + data: Unstructured[], ): Unstructured[] => { if (!filters || filters.length === 0) { return data; } - return data.filter(item => { - return filters.every(filter => { - if ('field' in filter) { - // Logical filter - const { field, operator, value } = filter; - return evaluateFilter(item, field, operator, value); - } else { - // Conditional filter - const { operator, value } = filter; - if (operator === 'or') { - return value.some(subFilter => { - const { field, operator, value } = subFilter as LogicalFilter; - return evaluateFilter(item, field, operator, value); - }); - } else if (operator === 'and') { - return value.every(subFilter => { - const { field, operator, value } = subFilter as LogicalFilter; - return evaluateFilter(item, field, operator, value); - }); - } - } - return true; - }); - }); + return data.filter(item => filters.every(filter => deepFilter(item, filter))); }; export function evaluateFilter( @@ -41,8 +35,12 @@ export function evaluateFilter( field: string, operator: Exclude, // eslint-disable-next-line @typescript-eslint/no-explicit-any - value: any + value: any, ): boolean { + if (!_.has(item, field)) { + return false; + } + const fieldValue = _.get(item, field); switch (operator) { @@ -66,7 +64,7 @@ export function evaluateFilter( if (Array.isArray(fieldValue)) { return _.includes(fieldValue, item); } - return item === fieldValue + return item === fieldValue; }); } case 'nin': { @@ -77,7 +75,7 @@ export function evaluateFilter( if (Array.isArray(fieldValue)) { return !_.includes(fieldValue, item); } - return item !== fieldValue + return item !== fieldValue; }); } case 'contains': @@ -92,10 +90,12 @@ export function evaluateFilter( return value[0] <= fieldValue && fieldValue <= value[1]; case 'nbetween': return value[0] > fieldValue || fieldValue > value[1]; - case 'null': - return fieldValue === null; - case 'nnull': - return fieldValue !== null; + case 'null': { + return _.isNil(fieldValue); + } + case 'nnull': { + return !_.isNil(fieldValue); + } case 'startswith': return fieldValue.startsWith(value); case 'nstartswith':