From 364501dd33f4161c5e2281208130ae97d6c3a496 Mon Sep 17 00:00:00 2001 From: Georgy Karataev Date: Mon, 28 Mar 2022 16:15:56 +0200 Subject: [PATCH] feat(Recommendations list table): filters edition modifies search params (#195) * RecsList: filters edition modifies search params * RecsList: add first search params tests --- src/AppConstants.js | 1 + src/Components/Common/Tables.js | 29 ++++++- src/Components/RecsListTable/RecsListTable.js | 36 ++++++-- .../RecsListTable/RecsListTable.spec.ct.js | 86 +++++++++++++++---- 4 files changed, 127 insertions(+), 25 deletions(-) diff --git a/src/AppConstants.js b/src/AppConstants.js index d718c94b..d47a9163 100644 --- a/src/AppConstants.js +++ b/src/AppConstants.js @@ -247,6 +247,7 @@ export const CLUSTERS_LIST_COLUMNS = [ export const CLUSTER_NAME_CELL = 0; export const CLUSTER_LAST_CHECKED_CELL = 6; export const RECS_LIST_COLUMNS_KEYS = [ + '', // reserved for expand button 'description', 'publish_date', 'tags', diff --git a/src/Components/Common/Tables.js b/src/Components/Common/Tables.js index 62b9d445..054d201c 100644 --- a/src/Components/Common/Tables.js +++ b/src/Components/Common/Tables.js @@ -182,7 +182,7 @@ export const paramParser = (search) => { return Array.from(searchParams).reduce( (acc, [key, value]) => ({ ...acc, - [key]: ['text', 'first'].includes(key) + [key]: ['text', 'first', 'rule_status', 'sort'].includes(key) ? value // just copy the full value : value === 'true' || value === 'false' ? JSON.parse(value) // parse boolean @@ -198,6 +198,13 @@ export const translateSortParams = (value) => ({ direction: value.startsWith('-') ? 'desc' : 'asc', }); +export const translateSortValue = (index, indexMapping, direction) => { + if (!['desc', 'asc'].includes(direction)) { + console.error('Invalid sort parameters (is not asc nor desc)'); + } + return `${direction === 'asc' ? '' : '-'}${indexMapping[index]}`; +}; + export const debounce = (value, delay) => { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { @@ -211,3 +218,23 @@ export const debounce = (value, delay) => { return debouncedValue; }; + +export const updateSearchParams = (filters = {}, columnMapping) => { + const url = new URL(window.location.origin + window.location.pathname); + // separately check the sort param + url.searchParams.set( + 'sort', + translateSortValue(filters.sortIndex, columnMapping, filters.sortDirection) + ); + // check the rest of filters + Object.entries(filters).forEach(([key, value]) => { + return ( + key !== 'sortIndex' && + key !== 'sortDirection' && + key !== 'sort' && + value !== '' && + url.searchParams.set(key, value) + ); + }); + window.history.replaceState(null, null, url.href); +}; diff --git a/src/Components/RecsListTable/RecsListTable.js b/src/Components/RecsListTable/RecsListTable.js index 4f739102..98d46870 100644 --- a/src/Components/RecsListTable/RecsListTable.js +++ b/src/Components/RecsListTable/RecsListTable.js @@ -50,6 +50,7 @@ import { passFilters, paramParser, translateSortParams, + updateSearchParams, } from '../Common/Tables'; import DisableRule from '../Modals/DisableRule'; import { Delete } from '../../Utilities/Api'; @@ -72,8 +73,13 @@ const RecsListTable = ({ query }) => { const notify = (data) => dispatch(addNotification(data)); const { search } = useLocation(); const [filterBuilding, setFilterBuilding] = useState(true); + // helps to distinguish the state when the API data received but not yet filtered + const [rowsFiltered, setRowsFiltered] = useState(false); const updateFilters = (filters) => dispatch(updateRecsListFilters(filters)); const searchText = filters?.text || ''; + const loadingState = isUninitialized || isFetching || !rowsFiltered; + const errorState = isError || (isSuccess && recs.length === 0); + const successState = isSuccess && recs.length > 0; useEffect(() => { setDisplayedRows( @@ -89,6 +95,9 @@ const RecsListTable = ({ query }) => { useEffect(() => { setFilteredRows(buildFilteredRows(recs, filters)); + if (isSuccess && !rowsFiltered) { + setRowsFiltered(true); + } }, [data, filters]); useEffect(() => { @@ -96,7 +105,7 @@ const RecsListTable = ({ query }) => { const paramsObject = paramParser(search); if (paramsObject.sort) { - const sortObj = translateSortParams(paramsObject.sort[0]); + const sortObj = translateSortParams(paramsObject.sort); paramsObject.sortIndex = RECS_LIST_COLUMNS_KEYS.indexOf(sortObj.name); paramsObject.sortDirection = sortObj.direction; } @@ -112,6 +121,12 @@ const RecsListTable = ({ query }) => { setFilterBuilding(false); }, []); + useEffect(() => { + if (!filterBuilding) { + updateSearchParams(filters, RECS_LIST_COLUMNS_KEYS); + } + }, [filters, filterBuilding]); + // constructs array of rows (from the initial data) checking currently applied filters const buildFilteredRows = (allRows, filters) => { return allRows @@ -219,8 +234,8 @@ const RecsListTable = ({ query }) => { const buildDisplayedRows = (rows, index, direction) => { const sortingRows = [...rows].sort((firstItem, secondItem) => { const d = direction === SortByDirection.asc ? 1 : -1; - const fst = firstItem[0].rule[RECS_LIST_COLUMNS_KEYS[index - 1]]; - const snd = secondItem[0].rule[RECS_LIST_COLUMNS_KEYS[index - 1]]; + const fst = firstItem[0].rule[RECS_LIST_COLUMNS_KEYS[index]]; + const snd = secondItem[0].rule[RECS_LIST_COLUMNS_KEYS[index]]; if (index === 3) { return extractCategories(fst)[0].localeCompare( extractCategories(snd)[0] @@ -556,18 +571,23 @@ const RecsListTable = ({ query }) => { isCompact: true, ouiaId: 'pager', }} - filterConfig={{ items: filterConfigItems }} - activeFiltersConfig={activeFiltersConfig} + filterConfig={{ + items: filterConfigItems, + isDisabled: loadingState || errorState, + }} + activeFiltersConfig={ + loadingState || errorState ? undefined : activeFiltersConfig + } /> - {(isUninitialized || isFetching) && } - {(isError || (isSuccess && recs.length === 0)) && ( + {loadingState && } + {errorState && ( )} - {!(isUninitialized || isFetching) && isSuccess && recs.length > 0 && ( + {!loadingState && !errorState && successState && ( { // TODO make sorting tests data independent it('sort the data by Name', () => { - cy.sortByCol(0); + cy.sortByCol(0).then(() => { + expect(window.location.search).to.contain('sort=description'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Name]') .should('contain', '1Lorem'); - cy.sortByCol(0); + cy.sortByCol(0).then(() => { + expect(window.location.search).to.contain('sort=-description'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Name]') @@ -219,12 +223,16 @@ describe('successful non-empty recommendations list table', () => { }); it('sort the data by Modified', () => { - cy.sortByCol(1); + cy.sortByCol(1).then(() => { + expect(window.location.search).to.contain('sort=publish_date'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Name]') .should('contain', '1Lorem'); - cy.sortByCol(1); + cy.sortByCol(1).then(() => { + expect(window.location.search).to.contain('sort=-publish_date'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Name]') @@ -236,12 +244,16 @@ describe('successful non-empty recommendations list table', () => { //had to add \\ \\ to the Total risk, otherwise jQuery engine would throw an error it('sort the data by Total Risk', () => { - cy.sortByCol(3); + cy.sortByCol(3).then(() => { + expect(window.location.search).to.contain('sort=total_risk'); + }); cy.getAllRows() .eq(0) .find('td[data-label="Total risk"]') .should('contain', 'Moderate'); - cy.sortByCol(3); + cy.sortByCol(3).then(() => { + expect(window.location.search).to.contain('sort=-total_risk'); + }); cy.getAllRows() .eq(0) .find('td[data-label="Total risk"]') @@ -249,12 +261,18 @@ describe('successful non-empty recommendations list table', () => { }); it('sort the data by Clusters', () => { - cy.sortByCol(4); + cy.sortByCol(4).then(() => { + expect(window.location.search).to.contain('sort=impacted_clusters_count'); + }); cy.getAllRows() .eq(0) .find('td[data-label="Clusters"]') .should('contain', '1'); - cy.sortByCol(4); + cy.sortByCol(4).then(() => { + expect(window.location.search).to.contain( + 'sort=-impacted_clusters_count' + ); + }); cy.getAllRows() .eq(0) .find('td[data-label="Clusters"]') @@ -262,7 +280,9 @@ describe('successful non-empty recommendations list table', () => { }); it('include disabled rules', () => { - cy.removeStatusFilter(); + cy.removeStatusFilter().then(() => { + expect(window.location.search).to.not.contain('rule_status'); + }); cy.getAllRows() .should('have.length', 5) .find('td[data-label="Name"]') @@ -314,11 +334,14 @@ describe('successful non-empty recommendations list table', () => { .eq(0) .find('td[data-label="Total risk"]') .contains('Critical'); + expect(window.location.search).to.contain('sort=-total_risk'); }); // all tables must preserve original ordering it('can sort by category', () => { - cy.sortByCol(2); + cy.sortByCol(2).then(() => { + expect(window.location.search).to.contain('sort=tags'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Name]') @@ -327,7 +350,9 @@ describe('successful non-empty recommendations list table', () => { .eq(0) .find('td[data-label=Category]') .should('contain', 'Performance'); - cy.sortByCol(2); + cy.sortByCol(2).then(() => { + expect(window.location.search).to.contain('sort=-tags'); + }); cy.getAllRows() .eq(0) .find('td[data-label=Category]') @@ -341,29 +366,58 @@ describe('successful non-empty recommendations list table', () => { cy.wrap(element); element[0].click(); }); - cy.get('.pf-c-select__menu').find('label > input').eq(1).check(); + cy.get('.pf-c-select__menu') + .find('label > input') + .eq(1) + .check() + .then(() => { + expect(window.location.search).to.contain('impacting=true%2Cfalse'); + }); cy.get('.pf-c-chip-group__list-item').contains('1 or more'); cy.get(RECS_LIST_TABLE).find('button[class=pf-c-dropdown__toggle]').click(); cy.get(FILTERS_DROPDOWN).contains('Status').click(); cy.get(FILTER_TOGGLE).click({ force: true }); - cy.get('button[class=pf-c-select__menu-item]').contains('All').click(); + cy.get('button[class=pf-c-select__menu-item]') + .contains('All') + .click() + .then(() => { + expect(window.location.search).to.contain('rule_status=all'); + }); cy.get('.pf-c-chip-group__list-item').contains('1 or more'); }); it('clears text input after Name filter chip removal', () => { - cy.get(TOOLBAR_FILTER).find('.pf-c-form-control').type('cc'); + cy.get(TOOLBAR_FILTER) + .find('.pf-c-form-control') + .type('cc') + .then(() => { + expect(window.location.search).to.contain('text=cc'); + }); // remove the chip - getChipGroup('Name').find('button').click(); + getChipGroup('Name') + .find('button') + .click() + .then(() => { + expect(window.location.search).to.not.contain('text='); + }); cy.get(TOOLBAR_FILTER).find('.pf-c-form-control').should('be.empty'); }); it('clears text input after resetting all filters', () => { cy.get(TOOLBAR_FILTER).find('.pf-c-form-control').type('cc'); // reset all filters - cy.get(TOOLBAR).find('button').contains('Reset filters').click(); + cy.get(TOOLBAR) + .find('button') + .contains('Reset filters') + .click() + .then(() => { + expect(window.location.search).to.not.contain('text='); + }); cy.get(TOOLBAR_FILTER).find('.pf-c-form-control').should('be.empty'); }); + + // TODO: test search parameters with likelihood, impact, category filters }); describe('empty recommendations list table', () => {