Skip to content

Commit

Permalink
feat(Recommendations list table): filters edition modifies search par…
Browse files Browse the repository at this point in the history
…ams (#195)

* RecsList: filters edition modifies search params

* RecsList: add first search params tests
  • Loading branch information
gkarat authored Mar 28, 2022
1 parent bf48a5c commit 364501d
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 25 deletions.
1 change: 1 addition & 0 deletions src/AppConstants.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
29 changes: 28 additions & 1 deletion src/Components/Common/Tables.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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(() => {
Expand All @@ -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);
};
36 changes: 28 additions & 8 deletions src/Components/RecsListTable/RecsListTable.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import {
passFilters,
paramParser,
translateSortParams,
updateSearchParams,
} from '../Common/Tables';
import DisableRule from '../Modals/DisableRule';
import { Delete } from '../../Utilities/Api';
Expand All @@ -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(
Expand All @@ -89,14 +95,17 @@ const RecsListTable = ({ query }) => {

useEffect(() => {
setFilteredRows(buildFilteredRows(recs, filters));
if (isSuccess && !rowsFiltered) {
setRowsFiltered(true);
}
}, [data, filters]);

useEffect(() => {
if (search && filterBuilding) {
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;
}
Expand All @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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) && <Loading />}
{(isError || (isSuccess && recs.length === 0)) && (
{loadingState && <Loading />}
{errorState && (
<Card id="error-state-message" ouiaId="error-state">
<CardBody>
<ErrorState />
</CardBody>
</Card>
)}
{!(isUninitialized || isFetching) && isSuccess && recs.length > 0 && (
{!loadingState && !errorState && successState && (
<React.Fragment>
<Table
aria-label="Table of recommendations"
Expand Down
86 changes: 70 additions & 16 deletions src/Components/RecsListTable/RecsListTable.spec.ct.js
Original file line number Diff line number Diff line change
Expand Up @@ -203,12 +203,16 @@ describe('successful non-empty recommendations list table', () => {

// 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]')
Expand All @@ -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]')
Expand All @@ -236,33 +244,45 @@ 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"]')
.should('contain', 'Critical');
});

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"]')
.should('contain', '2,003');
});

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"]')
Expand Down Expand Up @@ -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]')
Expand All @@ -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]')
Expand All @@ -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', () => {
Expand Down

0 comments on commit 364501d

Please sign in to comment.