From 9d828916c36534c1609834b49051612685f4394a Mon Sep 17 00:00:00 2001 From: Dinika Saxena Date: Mon, 3 Jul 2023 19:11:05 +0200 Subject: [PATCH] US 5 // Allow user to filter resources if they have path Signed-off-by: Dinika Saxena --- .../dataExplorer/DataExplorer-utils.spec.tsx | 255 ++++++++++++++++-- .../dataExplorer/DataExplorer.spec.tsx | 35 ++- src/subapps/dataExplorer/DataExplorer.tsx | 48 +--- .../dataExplorer/PredicateSelector.tsx | 132 ++++++--- 4 files changed, 366 insertions(+), 104 deletions(-) diff --git a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx index d13a57358..d169c8d6a 100644 --- a/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer-utils.spec.tsx @@ -1,7 +1,7 @@ import { doesResourceContain, getAllPaths, - isPathMissing, + checkPathExistence, } from './PredicateSelector'; describe('DataExplorerSpec-Utils', () => { @@ -82,7 +82,7 @@ describe('DataExplorerSpec-Utils', () => { expect(receivedPaths).toEqual(expectedPaths); }); - it('returns true when top level property does not exist in resource', () => { + it('checks if path exists in resource', () => { const resource = { foo: 'some value', nullValue: null, @@ -121,45 +121,229 @@ describe('DataExplorerSpec-Utils', () => { }, ], }; - expect(isPathMissing(resource, 'bar')).toEqual(true); - expect(isPathMissing(resource, 'nullValue')).toEqual(false); - expect(isPathMissing(resource, 'undefinedValue')).toEqual(false); - expect(isPathMissing(resource, 'emptyString')).toEqual(false); - expect(isPathMissing(resource, 'emptyArray')).toEqual(false); - expect(isPathMissing(resource, 'emptyObject')).toEqual(false); + expect(checkPathExistence(resource, 'bar')).toEqual(false); + expect(checkPathExistence(resource, 'nullValue')).toEqual(true); + expect(checkPathExistence(resource, 'undefinedValue')).toEqual(true); + expect(checkPathExistence(resource, 'emptyString')).toEqual(true); + expect(checkPathExistence(resource, 'emptyArray')).toEqual(true); + expect(checkPathExistence(resource, 'emptyObject')).toEqual(true); - expect(isPathMissing(resource, 'foo')).toEqual(false); - expect(isPathMissing(resource, 'foo.xyz')).toEqual(true); - expect(isPathMissing(resource, 'foo.distribution')).toEqual(true); + expect(checkPathExistence(resource, 'foo')).toEqual(true); + expect(checkPathExistence(resource, 'foo.xyz')).toEqual(false); + expect(checkPathExistence(resource, 'foo.distribution')).toEqual(false); - expect(isPathMissing(resource, 'distribution')).toEqual(false); - expect(isPathMissing(resource, 'distribution.name')).toEqual(false); - expect(isPathMissing(resource, 'distribution.name.sillyname')).toEqual( - true + expect(checkPathExistence(resource, 'distribution')).toEqual(true); + expect(checkPathExistence(resource, 'distribution.name')).toEqual(true); + expect(checkPathExistence(resource, 'distribution.name.sillyname')).toEqual( + false ); expect( - isPathMissing(resource, 'distribution.name.sillyname.pancake') + checkPathExistence(resource, 'distribution.name.sillyname.pancake') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.name.label.pancake') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.label.unofficial') + ).toEqual(true); // TODO: Add opposite + expect( + checkPathExistence(resource, 'distribution.label.extended.prefix') ).toEqual(true); - expect(isPathMissing(resource, 'distribution.name.label.pancake')).toEqual( - true + expect( + checkPathExistence(resource, 'distribution.label.extended.suffix') + ).toEqual(true); // Add opposite + expect( + checkPathExistence(resource, 'distribution.label.extended.notexisting') + ).toEqual(false); // Add opposite + expect(checkPathExistence(resource, 'distribution.foo')).toEqual(false); + expect(checkPathExistence(resource, 'distribution.emptyArray')).toEqual( + false ); - expect(isPathMissing(resource, 'distribution.label.unofficial')).toEqual( + expect( + checkPathExistence(resource, 'distribution.label.emptyArray') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.label.emptyString') + ).toEqual(true); // Add opposite + }); + + it('check if path exists in resource with nested array', () => { + const resource = { + distribution: [ + { + foo: 'foovalue', + filename: ['filename1'], + }, + { + foo: 'foovalue', + }, + ], + objPath: { + filename: ['filename1'], + }, + }; + expect( + checkPathExistence(resource, 'distribution.filename', 'exists') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.filename', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence(resource, 'objPath.filename', 'does-not-exist') + ).toEqual(false); + expect(checkPathExistence(resource, 'objPath.filename', 'exists')).toEqual( true ); + }); + + it('checks if path is missing in resource', () => { + const resource = { + foo: 'some value', + nullValue: null, + undefinedValue: undefined, + emptyString: '', + emptyArray: [], + emptyObject: {}, + distribution: [ + { + name: 'sally', + label: { + official: 'official', + unofficial: 'unofficial', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + }, + { + name: 'sally', + sillyname: 'soliloquy', + label: [ + { + official: 'official', + emptyArray: [], + emptyString: '', + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + { + official: 'official', + unofficial: 'unofficial', + emptyArray: [1], + extended: [{ prefix: '1', suffix: 2 }, { prefix: '1' }], + }, + ], + }, + ], + }; + expect(checkPathExistence(resource, 'bar', 'does-not-exist')).toEqual(true); + expect(checkPathExistence(resource, 'nullValue', 'does-not-exist')).toEqual( + false + ); expect( - isPathMissing(resource, 'distribution.label.extended.prefix') + checkPathExistence(resource, 'undefinedValue', 'does-not-exist') ).toEqual(false); expect( - isPathMissing(resource, 'distribution.label.extended.suffix') - ).toEqual(true); - expect(isPathMissing(resource, 'distribution.foo')).toEqual(true); - expect(isPathMissing(resource, 'distribution.emptyArray')).toEqual(true); - expect(isPathMissing(resource, 'distribution.label.emptyArray')).toEqual( + checkPathExistence(resource, 'emptyString', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'emptyArray', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'emptyObject', 'does-not-exist') + ).toEqual(false); + + expect(checkPathExistence(resource, 'foo', 'does-not-exist')).toEqual( false ); - expect(isPathMissing(resource, 'distribution.label.emptyString')).toEqual( + expect(checkPathExistence(resource, 'foo.xyz', 'does-not-exist')).toEqual( true ); + expect( + checkPathExistence(resource, 'foo.distribution', 'does-not-exist') + ).toEqual(true); + + expect( + checkPathExistence(resource, 'distribution', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence(resource, 'distribution.name', 'does-not-exist') + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.name.sillyname', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.name.sillyname.pancake', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.name.label.pancake', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.unofficial', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.official', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.prefix', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.suffix', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.extended.notexisting', + 'does-not-exist' + ) + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.foo', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence(resource, 'distribution.emptyArray', 'does-not-exist') + ).toEqual(true); + expect( + checkPathExistence( + resource, + 'distribution.label.emptyArray', + 'does-not-exist' + ) + ).toEqual(false); + expect( + checkPathExistence( + resource, + 'distribution.label.emptyString', + 'does-not-exist' + ) + ).toEqual(true); }); it('checks if array strings can be checked for contains', () => { @@ -301,4 +485,23 @@ describe('DataExplorerSpec-Utils', () => { doesResourceContain(resource, 'distribution.filename', 'lly') ).toEqual(true); }); + + it('checks if path exists in resource', () => { + const resource = { + distribution: [ + { + name: 'sally', + filename: 'billy', + label: ['ChiPmunK'], + }, + { + name: 'sally', + sillyname: 'soliloquy', + filename: 'bolly', + label: { foo: 'foovalut', bar: 'barvalue' }, + }, + ], + }; + expect(checkPathExistence(resource, 'topLevelNotExisting')).toEqual(false); + }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.spec.tsx b/src/subapps/dataExplorer/DataExplorer.spec.tsx index 501a8a662..4456c1166 100644 --- a/src/subapps/dataExplorer/DataExplorer.spec.tsx +++ b/src/subapps/dataExplorer/DataExplorer.spec.tsx @@ -18,7 +18,13 @@ import { render, screen, waitFor } from '../../utils/testUtil'; import { DataExplorer } from './DataExplorer'; import { AllProjects } from './ProjectSelector'; import { getColumnTitle } from './DataExplorerTable'; -import { DEFAULT_OPTION, getAllPaths } from './PredicateSelector'; +import { + CONTAINS, + DEFAULT_OPTION, + DOES_NOT_EXIST, + EXISTS, + getAllPaths, +} from './PredicateSelector'; describe('DataExplorer', () => { const server = setupServer( @@ -73,7 +79,6 @@ describe('DataExplorer', () => { const expectRowCountToBe = async (expectedRowsCount: number) => { return await waitFor(() => { const rows = visibleTableRows(); - rows.forEach(row => console.log('Inner html', row.innerHTML)); expect(rows.length).toEqual(expectedRowsCount); return rows; }); @@ -176,7 +181,6 @@ describe('DataExplorer', () => { optionLabel: string ) => { await openMenuFor(menuAriaLabel); - console.log('Lookig for option ', optionLabel, ' in menu ', menuAriaLabel); const option = await getDropdownOption(optionLabel); await userEvent.click(option, { pointerEventsCheck: 0 }); }; @@ -398,11 +402,11 @@ describe('DataExplorer', () => { await expectRowCountToBe(3); await selectOptionFromMenu(PathMenuLabel, 'author'); - await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_EXIST); await expectRowCountToBe(1); await selectOptionFromMenu(PathMenuLabel, 'edition'); - await selectOptionFromMenu(PredicateMenuLabel, 'Empty value'); + await selectOptionFromMenu(PredicateMenuLabel, DOES_NOT_EXIST); await expectRowCountToBe(2); }); @@ -419,7 +423,7 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PathMenuLabel, 'author'); await userEvent.click(container); - await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + await selectOptionFromMenu(PredicateMenuLabel, CONTAINS); const valueInput = await screen.getByPlaceholderText('type the value...'); await userEvent.type(valueInput, 'iggy'); await expectRowCountToBe(2); @@ -443,7 +447,24 @@ describe('DataExplorer', () => { await selectOptionFromMenu(PathMenuLabel, 'author'); await userEvent.click(container); - await selectOptionFromMenu(PredicateMenuLabel, 'Contains'); + await selectOptionFromMenu(PredicateMenuLabel, CONTAINS); + await expectRowCountToBe(3); + }); + + it('shows resources that have a path when user selects exists predicate', async () => { + await expectRowCountToBe(10); + const mockResourcesForNextPage = [ + getMockResource('self1', { author: 'piggy', edition: 1 }), + getMockResource('self2', { author: ['iggy', 'twinky'] }), + getMockResource('self3', { year: 2013 }), + ]; + + await getRowsForNextPage(mockResourcesForNextPage); await expectRowCountToBe(3); + + await selectOptionFromMenu(PathMenuLabel, 'author'); + await userEvent.click(container); + await selectOptionFromMenu(PredicateMenuLabel, EXISTS); + await expectRowCountToBe(2); }); }); diff --git a/src/subapps/dataExplorer/DataExplorer.tsx b/src/subapps/dataExplorer/DataExplorer.tsx index 1ee2fce54..2280e1d64 100644 --- a/src/subapps/dataExplorer/DataExplorer.tsx +++ b/src/subapps/dataExplorer/DataExplorer.tsx @@ -4,41 +4,23 @@ import { notification } from 'antd'; import { isString } from 'lodash'; import React, { useReducer } from 'react'; import { useQuery } from 'react-query'; -import { getResourceLabel } from '../../shared/utils'; import { DataExplorerTable } from './DataExplorerTable'; import './styles.less'; import { ProjectSelector } from './ProjectSelector'; -import { - CONTAINS, - EMPTY_VALUE, - PredicateFilterT, - PredicateSelector, - doesResourceContain, - isPathMissing, -} from './PredicateSelector'; -import { normalizeString } from 'utils/stringUtils'; +import { PredicateSelector } from './PredicateSelector'; export interface DataExplorerConfiguration { pageSize: number; offset: number; orgAndProject?: [string, string]; - predicatePath: string | null; - predicateFilter: PredicateFilterT | null; - predicateValue: string | null; + predicateFilter: ((resource: Resource) => boolean) | null; } export const DataExplorer: React.FC<{}> = () => { const nexus = useNexusContext(); const [ - { - pageSize, - offset, - orgAndProject, - predicatePath, - predicateFilter, - predicateValue, - }, + { pageSize, offset, orgAndProject, predicateFilter }, updateTableConfiguration, ] = useReducer( ( @@ -49,9 +31,7 @@ export const DataExplorer: React.FC<{}> = () => { pageSize: 50, offset: 0, orgAndProject: undefined, - predicatePath: null, predicateFilter: null, - predicateValue: '', } ); @@ -83,23 +63,11 @@ export const DataExplorer: React.FC<{}> = () => { const currentPageDataSource: Resource[] = resources?._results || []; - const displayedDataSource = - predicatePath && predicateFilter - ? currentPageDataSource.filter(resource => { - switch (predicateFilter) { - case EMPTY_VALUE: - return isPathMissing(resource, predicatePath); - case CONTAINS: - return doesResourceContain( - resource, - predicatePath, - predicateValue ?? '' - ); - default: - return true; - } - }) - : currentPageDataSource; + const displayedDataSource = predicateFilter + ? currentPageDataSource.filter(resource => { + return predicateFilter(resource); + }) + : currentPageDataSource; return (
diff --git a/src/subapps/dataExplorer/PredicateSelector.tsx b/src/subapps/dataExplorer/PredicateSelector.tsx index 5e3d741bb..62d463675 100644 --- a/src/subapps/dataExplorer/PredicateSelector.tsx +++ b/src/subapps/dataExplorer/PredicateSelector.tsx @@ -15,9 +15,12 @@ export const PredicateSelector: React.FC = ({ dataSource, onPredicateChange, }: Props) => { - const [selectedPredicateFilter, setSeletectPredicateFilter] = useState< - string - >(DEFAULT_OPTION); + const [path, setPath] = useState(DEFAULT_OPTION); + + const [predicate, setPredicate] = useState( + DEFAULT_OPTION + ); + const [searchTerm, setSearchTerm] = useState(null); const pathOptions = [ { value: DEFAULT_OPTION }, @@ -25,10 +28,46 @@ export const PredicateSelector: React.FC = ({ ]; const predicateFilterOptions: PredicateFilterOptions[] = [ { value: DEFAULT_OPTION }, - { value: EMPTY_VALUE }, + { value: EXISTS }, + { value: DOES_NOT_EXIST }, { value: CONTAINS }, ]; + const predicateSelected = ( + path: string, + predicate: PredicateFilterOptions['value'], + searchTerm: string | null + ) => { + if (path === DEFAULT_OPTION || predicate === DEFAULT_OPTION) { + onPredicateChange({ predicateFilter: null }); + } + + switch (predicate) { + case EXISTS: + onPredicateChange({ + predicateFilter: (resource: Resource) => + checkPathExistence(resource, path, 'exists'), + }); + break; + case DOES_NOT_EXIST: + onPredicateChange({ + predicateFilter: (resource: Resource) => + checkPathExistence(resource, path, 'does-not-exist'), + }); + break; + case CONTAINS: + if (searchTerm) { + onPredicateChange({ + predicateFilter: (resource: Resource) => + doesResourceContain(resource, path, searchTerm), + }); + } + break; + default: + onPredicateChange({ predicateFilter: null }); + } + }; + return (
with @@ -36,9 +75,8 @@ export const PredicateSelector: React.FC = ({ { - setSeletectPredicateFilter(predicateFilterLabel); - if (predicateFilterLabel === CONTAINS) { - return; - } - onPredicateChange({ - predicateFilter: - predicateFilterLabel === DEFAULT_OPTION - ? null - : predicateFilterLabel, - }); + onSelect={(predicateLabel: PredicateFilterOptions['value']) => { + setPredicate(predicateLabel); + predicateSelected(path, predicateLabel, searchTerm); }} aria-label="predicate-selector" - className={clsx( - 'select-menu', - selectedPredicateFilter === CONTAINS && 'greyed-out' - )} + className={clsx('select-menu', path === CONTAINS && 'greyed-out')} popupClassName="search-menu" allowClear={true} - onClear={() => onPredicateChange({ predicateFilter: null })} + onClear={() => { + setPredicate(DEFAULT_OPTION); + predicateSelected(path, DEFAULT_OPTION, searchTerm); + }} /> - {selectedPredicateFilter === CONTAINS && ( + {predicate === CONTAINS && ( { - onPredicateChange({ - predicateFilter: CONTAINS, - predicateValue: event.target.value, - }); + setSearchTerm(event.target.value); + predicateSelected(path, predicate, event.target.value); }} /> )} @@ -91,9 +119,15 @@ export const PredicateSelector: React.FC = ({ }; export const DEFAULT_OPTION = '-'; -export const EMPTY_VALUE = 'Empty value'; +export const DOES_NOT_EXIST = 'Does not exist'; +export const EXISTS = 'Exists'; export const CONTAINS = 'Contains'; -export type PredicateFilterT = typeof EMPTY_VALUE | typeof CONTAINS | null; + +export type PredicateFilterT = + | typeof DOES_NOT_EXIST + | typeof EXISTS + | typeof CONTAINS + | null; type PredicateFilterOptions = { value: Exclude | typeof DEFAULT_OPTION; @@ -142,11 +176,47 @@ const getPathsForResource = ( return paths; }; +export const checkPathExistence = ( + resource: { [key: string]: any }, + path: string, + criteria: 'exists' | 'does-not-exist' = 'exists' +): boolean => { + if (path in resource) { + return criteria === 'exists' ? true : false; + } + + const subpaths = path.split('.'); + + for (const subpath of subpaths) { + const valueAtSubpath = resource[subpath]; + const remainingPath = subpaths.slice(1); + if (!(subpath in resource)) { + return criteria === 'exists' ? false : true; + } + + if (Array.isArray(valueAtSubpath)) { + return valueAtSubpath.some(value => + checkPathExistence(value, remainingPath.join('.'), criteria) + ); + } + if (isObject(valueAtSubpath)) { + return checkPathExistence( + valueAtSubpath, + remainingPath.join('.'), + criteria + ); + } + break; + } + + return criteria === 'exists' ? false : true; +}; + export const isPathMissing = ( resource: { [key: string]: any }, path: string ): boolean => { - if (path in resource) { + if (path in resource && path !== '') { return false; }