diff --git a/playwright/test-utils/helpers/filters.ts b/playwright/test-utils/helpers/filters.ts
index d7d4e5ea1..082803f0d 100644
--- a/playwright/test-utils/helpers/filters.ts
+++ b/playwright/test-utils/helpers/filters.ts
@@ -332,6 +332,11 @@ export const removeFilter = async (page: Page, filter: FilterConfig) => {
export const resetFilters = async (page: Page) => {
// Close any open dropdowns first
await page.keyboard.press('Escape');
- await page.getByRole('button', { name: /Reset filters/i }).click();
+ try {
+ await page.getByRole('button', { name: /(Reset|Clear) filters/i }).waitFor({ timeout: 5000 });
+ } catch {
+ return;
+ }
+ await page.getByRole('button', { name: /(Reset|Clear) filters/i }).click();
await waitForTableLoad(page);
};
diff --git a/src/Messages.ts b/src/Messages.ts
index fd5b9eea7..543a5f58a 100644
--- a/src/Messages.ts
+++ b/src/Messages.ts
@@ -166,7 +166,7 @@ export default defineMessages({
labelsFiltersClear: {
id: 'labelsFiltersClear',
description: 'label for remove filter chips',
- defaultMessage: 'Reset filters',
+ defaultMessage: 'Clear filters',
},
labelsFiltersCvesSearchPlaceHolder: {
id: 'labelsFiltersCvesSearch',
diff --git a/src/PresentationalComponents/TableView/TableView.js b/src/PresentationalComponents/TableView/TableView.js
index 4eb76821f..988bb9657 100644
--- a/src/PresentationalComponents/TableView/TableView.js
+++ b/src/PresentationalComponents/TableView/TableView.js
@@ -4,11 +4,9 @@ import { PrimaryToolbar } from '@redhat-cloud-services/frontend-components/Prima
import { SkeletonTable } from '@redhat-cloud-services/frontend-components/SkeletonTable';
import PropTypes from 'prop-types';
import React from 'react';
-import messages from '../../Messages';
import AsyncRemediationButton from '../../SmartComponents/Remediation/AsyncRemediationButton';
-import { arrayFromObj, buildFilterChips, convertLimitOffset } from '../../Utilities/Helpers';
+import { arrayFromObj, buildActiveFilterConfig, convertLimitOffset } from '../../Utilities/Helpers';
import { useRemoveFilter, useBulkSelectConfig } from '../../Utilities/hooks';
-import { intl } from '../../Utilities/IntlProvider';
import TableFooter from './TableFooter';
import ErrorHandler from '../../PresentationalComponents/Snippets/ErrorHandler';
import { Skeleton, ToolbarItem } from '@patternfly/react-core';
@@ -50,6 +48,10 @@ const TableView = ({
const selectedCount = selectedRows && arrayFromObj(selectedRows).length;
const { code, hasError, isLoading } = status;
const bulkSelectConfig = useBulkSelectConfig(selectedCount, onSelect, metadata, rows, onCollapse);
+ const activeFiltersConfig = React.useMemo(
+ () => buildActiveFilterConfig(filter, search, deleteFilters, searchChipLabel, defaultFilters),
+ [defaultFilters, deleteFilters, filter, search, searchChipLabel],
+ );
const [isColumnMgmtModalOpen, setColumnMgmtModalOpen] = React.useState(false);
const [appliedColumns, setAppliedColumns] = React.useState(columns);
@@ -102,13 +104,7 @@ const TableView = ({
)
}
filterConfig={filterConfig}
- activeFiltersConfig={{
- filters: buildFilterChips(filter, search, searchChipLabel),
- onDelete: deleteFilters,
- deleteTitle: intl.formatMessage(
- (defaultFilters && messages.labelsFiltersReset) || messages.labelsFiltersClear,
- ),
- }}
+ activeFiltersConfig={activeFiltersConfig}
actionsConfig={{
actions: [
remediationProvider && (
diff --git a/src/PresentationalComponents/TableView/TableView.test.js b/src/PresentationalComponents/TableView/TableView.test.js
index e31574797..fe592fdcb 100644
--- a/src/PresentationalComponents/TableView/TableView.test.js
+++ b/src/PresentationalComponents/TableView/TableView.test.js
@@ -2,9 +2,10 @@ import TableView from './TableView';
import { render, screen, waitFor } from '@testing-library/react';
import { Provider, useSelector } from 'react-redux';
import configureStore from 'redux-mock-store';
-import { storeListDefaults } from '../../Utilities/constants';
+import { pageDefaultFilters, storeListDefaults } from '../../Utilities/constants';
import { systemPackages } from '../../Utilities/RawDataForTesting';
import userEvent from '@testing-library/user-event';
+import '@testing-library/jest-dom';
const testObj = {
columns: [],
@@ -134,6 +135,67 @@ describe('TableView', () => {
expect(asFragment()).toMatchSnapshot();
});
+ it('should keep default filter chips visible while hiding reset at the default state', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByText('Systems with patches available')).toBeInTheDocument();
+ expect(screen.queryByRole('button', { name: /reset filters/i })).not.toBeInTheDocument();
+ });
+
+ it('should show a reset button when active filters differ from defaults', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: /reset filters/i })).toBeInTheDocument();
+ });
+
+ it('should show a clear button for pages without defaults', () => {
+ render(
+
+
+ ,
+ );
+
+ expect(screen.getByRole('button', { name: /clear filters/i })).toBeInTheDocument();
+ });
+
it('Should unselect', async () => {
await render(
diff --git a/src/SmartComponents/Advisories/Advisories.js b/src/SmartComponents/Advisories/Advisories.js
index f3aa73163..0c0828075 100644
--- a/src/SmartComponents/Advisories/Advisories.js
+++ b/src/SmartComponents/Advisories/Advisories.js
@@ -16,6 +16,7 @@ import {
selectAdvisoryRow,
} from '../../store/Actions/Actions';
import { exportAdvisoriesCSV, exportAdvisoriesJSON } from '../../Utilities/api/api';
+import { pageDefaultFilters } from '../../Utilities/constants';
import { createAdvisoriesRows } from '../../Utilities/DataMappers';
import {
createSortBy,
@@ -200,6 +201,7 @@ const Advisories = () => {
rebootFilter(apply, queryParams?.filter),
],
}}
+ defaultFilters={pageDefaultFilters.advisories}
searchChipLabel={intl.formatMessage(messages.labelsFiltersSearchAdvisoriesTitle)}
isRemediationLoading={isRemediationLoading}
hasColumnManagement
diff --git a/src/SmartComponents/AdvisoryDetail/CvesModal.tsx b/src/SmartComponents/AdvisoryDetail/CvesModal.tsx
index 85580339d..590ae2bdd 100644
--- a/src/SmartComponents/AdvisoryDetail/CvesModal.tsx
+++ b/src/SmartComponents/AdvisoryDetail/CvesModal.tsx
@@ -6,6 +6,7 @@ import TableView from '../../PresentationalComponents/TableView/TableView';
import searchFilter from '../../PresentationalComponents/Filters/SearchFilter';
import { cvesTableColumns } from '../../PresentationalComponents/TableView/TableViewAssets';
import { fetchCves } from '../../store/Actions/VulnerabilityActions';
+import { pageDefaultFilters } from '../../Utilities/constants';
import { createCvesRows } from '../../Utilities/DataMappers';
import { sortCves } from '../../Utilities/Helpers';
import { SortByDirection } from '@patternfly/react-table';
@@ -108,6 +109,7 @@ const CvesModal = ({ cveIds }: CvesModalProps) => {
status,
queryParams: { filter: {}, search },
}}
+ defaultFilters={pageDefaultFilters.cves}
filterConfig={{
items: [
searchFilter(
diff --git a/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.js b/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.js
index d4dd37778..63caacabf 100644
--- a/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.js
+++ b/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.js
@@ -17,7 +17,7 @@ import {
exportAdvisorySystemsJSON,
fetchAdvisorySystems,
} from '../../Utilities/api/api';
-import { remediationIdentifiers } from '../../Utilities/constants';
+import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
import {
arrayFromObj,
persistantParams,
@@ -64,7 +64,11 @@ const AdvisorySystemsTable = ({
(newColumns) => setAppliedColumns(newColumns),
);
- const [deleteFilters] = useRemoveFilter({ search, ...filter }, apply);
+ const [deleteFilters] = useRemoveFilter(
+ { search, ...filter },
+ apply,
+ pageDefaultFilters.advisorySystems,
+ );
const filterConfig = {
items: [
@@ -78,7 +82,12 @@ const AdvisorySystemsTable = ({
],
};
- const activeFiltersConfig = buildActiveFiltersConfig(filter, search, deleteFilters);
+ const activeFiltersConfig = buildActiveFiltersConfig(
+ filter,
+ search,
+ deleteFilters,
+ pageDefaultFilters.advisorySystems,
+ );
const onSelect = useOnSelect(systems, selectedRows, {
endpoint: ID_API_ENDPOINTS.advisorySystems(advisoryName),
diff --git a/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.test.js b/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.test.js
index 266494f7d..5f974ec2f 100644
--- a/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.test.js
+++ b/src/SmartComponents/AdvisorySystems/AdvisorySystemsTable.test.js
@@ -31,6 +31,10 @@ const initStore = (state) => {
return mockStore(state);
};
+beforeEach(() => {
+ InventoryTable.mockClear();
+});
+
const renderComponent = async (mockedStore) => {
render(
@@ -179,6 +183,21 @@ describe('AdvisorySystemsTable.js', () => {
);
});
+ it('should keep active filters empty when the page is at its default state', async () => {
+ await renderComponent(mockState);
+
+ expect(InventoryTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: {
+ deleteTitle: 'Clear filters',
+ filters: [],
+ onDelete: expect.any(Function),
+ },
+ }),
+ {},
+ );
+ });
+
it('should provide activeFilters config', async () => {
const filteredState = {
...mockState,
@@ -194,7 +213,7 @@ describe('AdvisorySystemsTable.js', () => {
expect(InventoryTable).toHaveBeenCalledWith(
expect.objectContaining({
activeFiltersConfig: {
- deleteTitle: 'Reset filters',
+ deleteTitle: 'Clear filters',
filters: [
{
category: 'Status',
diff --git a/src/SmartComponents/PackageSystems/PackageSystems.js b/src/SmartComponents/PackageSystems/PackageSystems.js
index 5df63a000..f13fec26f 100644
--- a/src/SmartComponents/PackageSystems/PackageSystems.js
+++ b/src/SmartComponents/PackageSystems/PackageSystems.js
@@ -26,10 +26,10 @@ import {
fetchPackageSystems,
fetchPackageVersions,
} from '../../Utilities/api/api';
-import { remediationIdentifiers } from '../../Utilities/constants';
+import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
import {
arrayFromObj,
- buildFilterChips,
+ buildActiveFilterConfig,
decodeQueryparams,
filterRemediatablePackageSystems,
persistantParams,
@@ -91,7 +91,11 @@ const PackageSystems = ({ packageName }) => {
(newColumns) => setAppliedColumns(newColumns),
);
- const [deleteFilters] = useRemoveFilter({ ...filter, search }, apply);
+ const [deleteFilters] = useRemoveFilter(
+ { ...filter, search },
+ apply,
+ pageDefaultFilters.packageSystems,
+ );
const filterConfig = {
items: [
@@ -107,15 +111,15 @@ const PackageSystems = ({ packageName }) => {
};
const activeFiltersConfig = useMemo(
- () => ({
- filters: buildFilterChips(
+ () =>
+ buildActiveFilterConfig(
filter,
search,
+ deleteFilters,
intl.formatMessage(messages.labelsFiltersSystemsSearchTitle),
+ pageDefaultFilters.packageSystems,
),
- onDelete: deleteFilters,
- }),
- [filter, search],
+ [deleteFilters, filter, search],
);
const constructFilename = (system) => `${system.available_evra}`;
diff --git a/src/SmartComponents/PackageSystems/PackageSystems.test.js b/src/SmartComponents/PackageSystems/PackageSystems.test.js
index ead3c75ab..c7228ded0 100644
--- a/src/SmartComponents/PackageSystems/PackageSystems.test.js
+++ b/src/SmartComponents/PackageSystems/PackageSystems.test.js
@@ -3,7 +3,8 @@ import { systemRows } from '../../Utilities/RawDataForTesting';
import { initMocks } from '../../Utilities/unitTestingUtilities.js';
import PackageSystems from './PackageSystems';
import { ComponentWithContext } from '../../Utilities/TestingUtilities.js';
-import { render, screen } from '@testing-library/react';
+import { render, screen, waitFor } from '@testing-library/react';
+import { InventoryTable } from '@redhat-cloud-services/frontend-components/Inventory';
initMocks();
@@ -61,26 +62,82 @@ const mockState = {
},
};
-const initStore = () => {
+const initStore = (state = mockState) => {
const mockStore = configureStore([]);
- return mockStore(mockState);
+ return mockStore(state);
};
-const store = initStore(mockState);
-
-beforeEach(() => {
+const renderComponent = async (state = mockState) => {
render(
-
+
,
);
+
+ await waitFor(() => {
+ expect(screen.getByTestId('inventory-mock-component')).toBeVisible();
+ });
+};
+
+beforeEach(() => {
+ InventoryTable.mockClear();
});
// TODO: find a meaningful way of testing InventoryTable fed module
describe('PackageSystems.js', () => {
- it('Should render inventory table', () => {
+ it('Should render inventory table', async () => {
+ await renderComponent();
expect(screen.getByTestId('inventory-mock-component')).toBeVisible();
});
+
+ it('should keep active filters empty when there are no non-default filters', async () => {
+ await renderComponent();
+
+ expect(InventoryTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: {
+ deleteTitle: 'Clear filters',
+ filters: [],
+ onDelete: expect.any(Function),
+ },
+ }),
+ {},
+ );
+ });
+
+ it('should provide a clear filters action when filters are active', async () => {
+ await renderComponent({
+ ...mockState,
+ PackageSystemsStore: {
+ queryParams: {
+ filter: { status: ['Applicable'] },
+ },
+ },
+ });
+
+ expect(InventoryTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: {
+ deleteTitle: 'Clear filters',
+ filters: [
+ {
+ category: 'Status',
+ chips: [
+ {
+ id: 'status',
+ name: 'Applicable',
+ value: 'Applicable',
+ },
+ ],
+ id: 'status',
+ },
+ ],
+ onDelete: expect.any(Function),
+ },
+ }),
+ {},
+ );
+ });
// it('Should dispatch change package systems params action once only', () => {
// const dispatchedActions = store.getActions();
// expect(dispatchedActions.filter(item => item.type === 'CHANGE_PACKAGE_SYSTEMS_PARAMS')).toHaveLength(1);
diff --git a/src/SmartComponents/Packages/Packages.js b/src/SmartComponents/Packages/Packages.js
index 7bb7bdcfd..e3a90ca30 100644
--- a/src/SmartComponents/Packages/Packages.js
+++ b/src/SmartComponents/Packages/Packages.js
@@ -9,15 +9,10 @@ import TableView from '../../PresentationalComponents/TableView/TableView';
import { packagesColumns } from '../../PresentationalComponents/TableView/TableViewAssets';
import { changePackagesListParams, fetchPackagesAction } from '../../store/Actions/Actions';
import { exportPackagesCSV, exportPackagesJSON } from '../../Utilities/api/api';
-import { packagesListDefaultFilters } from '../../Utilities/constants';
+import { pageDefaultFilters } from '../../Utilities/constants';
import { createPackagesRows } from '../../Utilities/DataMappers';
import { createSortBy, decodeQueryparams, encodeURLParams } from '../../Utilities/Helpers';
-import {
- useOnExport,
- usePerPageSelect,
- useSetPage,
- useSortColumn,
-} from '../../Utilities/hooks';
+import { useOnExport, usePerPageSelect, useSetPage, useSortColumn } from '../../Utilities/hooks';
import { intl } from '../../Utilities/IntlProvider';
import { useChrome } from '@redhat-cloud-services/frontend-components/useChrome';
import { useSearchParams } from 'react-router-dom';
@@ -98,7 +93,7 @@ const Packages = () => {
remediationButtonOUIA='toolbar-remediation-button'
tableOUIA='package-details-table'
paginationOUIA='package-details-pagination'
- defaultFilters={packagesListDefaultFilters}
+ defaultFilters={pageDefaultFilters.packages}
searchChipLabel={intl.formatMessage(messages.labelsFiltersPackagesSearchTitle)}
hasColumnManagement
/>
diff --git a/src/SmartComponents/SystemAdvisories/SystemAdvisories.js b/src/SmartComponents/SystemAdvisories/SystemAdvisories.js
index 416238d5d..0aa96feb0 100644
--- a/src/SmartComponents/SystemAdvisories/SystemAdvisories.js
+++ b/src/SmartComponents/SystemAdvisories/SystemAdvisories.js
@@ -18,7 +18,7 @@ import {
selectSystemAdvisoryRow,
} from '../../store/Actions/Actions';
import { exportSystemAdvisoriesCSV, exportSystemAdvisoriesJSON } from '../../Utilities/api/api';
-import { remediationIdentifiers } from '../../Utilities/constants';
+import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
import { createSystemAdvisoriesRows } from '../../Utilities/DataMappers';
import {
arrayFromObj,
@@ -167,6 +167,7 @@ const SystemAdvisories = ({ handleNoSystemData, inventoryId, shouldRefresh }) =>
],
}}
errorState={errorState}
+ defaultFilters={pageDefaultFilters.systemAdvisories}
searchChipLabel={intl.formatMessage(messages.labelsFiltersSearchAdvisoriesTitle)}
hasColumnManagement
/>
diff --git a/src/SmartComponents/SystemPackages/SystemPackages.js b/src/SmartComponents/SystemPackages/SystemPackages.js
index c7de221ad..ce9e2b5fe 100644
--- a/src/SmartComponents/SystemPackages/SystemPackages.js
+++ b/src/SmartComponents/SystemPackages/SystemPackages.js
@@ -15,7 +15,7 @@ import {
selectSystemPackagesRow,
} from '../../store/Actions/Actions';
import { exportSystemPackagesCSV, exportSystemPackagesJSON } from '../../Utilities/api/api';
-import { remediationIdentifiers, systemPackagesDefaultFilters } from '../../Utilities/constants';
+import { pageDefaultFilters, remediationIdentifiers } from '../../Utilities/constants';
import { createSystemPackagesRows } from '../../Utilities/DataMappers';
import { arrayFromObj, createSortBy, remediationProvider } from '../../Utilities/Helpers';
import {
@@ -134,7 +134,7 @@ const SystemPackages = ({ handleNoSystemData, inventoryId, shouldRefresh }) => {
statusFilter(apply, queryParams.filter),
],
}}
- defaultFilters={systemPackagesDefaultFilters}
+ defaultFilters={pageDefaultFilters.systemPackages}
remediationButtonOUIA='toolbar-remediation-button'
tableOUIA='system-packages-table'
paginationOUIA='system-packages-pagination'
diff --git a/src/SmartComponents/Systems/SystemTable.test.js b/src/SmartComponents/Systems/SystemTable.test.js
index 3b4803f73..e76ad5880 100644
--- a/src/SmartComponents/Systems/SystemTable.test.js
+++ b/src/SmartComponents/Systems/SystemTable.test.js
@@ -2,9 +2,17 @@ import configureStore from 'redux-mock-store';
import { systemRows } from '../../Utilities/RawDataForTesting';
import { initMocks } from '../../Utilities/unitTestingUtilities';
import Systems from './SystemsTable';
-import { render, screen, waitFor } from '@testing-library/react';
+import { act, render, screen, waitFor } from '@testing-library/react';
import { ComponentWithContext } from '../../Utilities/TestingUtilities';
import { InventoryTable } from '@redhat-cloud-services/frontend-components/Inventory';
+import { fetchSystems } from '../../Utilities/api/api';
+
+jest.mock('../../Utilities/api/api', () => ({
+ exportSystemsCSV: jest.fn(),
+ exportSystemsJSON: jest.fn(),
+ fetchSystems: jest.fn(),
+}));
+
initMocks();
const mockState = {
@@ -27,10 +35,27 @@ const initStore = (state) => {
return mockStore(state);
};
-const renderComponent = async (mockedStore) => {
+beforeEach(() => {
+ InventoryTable.mockClear();
+ fetchSystems.mockReset();
+ fetchSystems.mockResolvedValue({
+ data: [],
+ meta: {
+ total_items: 0,
+ },
+ });
+});
+
+const renderComponent = async (mockedStore, props = {}) => {
render(
-
+
,
);
@@ -198,8 +223,59 @@ describe('SystemsTable', () => {
);
});
- it('should provide activeFilters config', async () => {
- await renderComponent(mockState);
+ it('should keep default systems filters visible while hiding reset at baseline', async () => {
+ const filteredState = {
+ ...mockState,
+ SystemsStore: {
+ queryParams: {
+ filter: { stale: [true, false] },
+ },
+ },
+ };
+
+ await renderComponent(filteredState);
+ expect(InventoryTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: {
+ deleteTitle: 'Reset filters',
+ filters: [
+ {
+ category: 'Status',
+ chips: [
+ {
+ id: true,
+ name: 'Stale',
+ value: true,
+ },
+ {
+ id: false,
+ name: 'Fresh',
+ value: false,
+ },
+ ],
+ id: 'stale',
+ },
+ ],
+ onDelete: expect.any(Function),
+ showDeleteButton: false,
+ },
+ }),
+ {},
+ );
+ });
+
+ it('should show reset only when at least one filter differs from defaults', async () => {
+ const filteredState = {
+ ...mockState,
+ SystemsStore: {
+ queryParams: {
+ filter: { packages_updatable: 'eq:0', stale: [true, false] },
+ },
+ },
+ };
+
+ await renderComponent(filteredState);
+
expect(InventoryTable).toHaveBeenCalledWith(
expect.objectContaining({
activeFiltersConfig: {
@@ -216,14 +292,123 @@ describe('SystemsTable', () => {
],
id: 'packages_updatable',
},
+ {
+ category: 'Status',
+ chips: [
+ {
+ id: true,
+ name: 'Stale',
+ value: true,
+ },
+ {
+ id: false,
+ name: 'Fresh',
+ value: false,
+ },
+ ],
+ id: 'stale',
+ },
],
onDelete: expect.any(Function),
+ showDeleteButton: true,
},
}),
{},
);
});
+ it('should show reset when inventory-based operating system filters are active', async () => {
+ const filteredState = {
+ ...mockState,
+ SystemsStore: {
+ queryParams: {
+ filter: { os: ['RHEL 8.8'], stale: [true, false] },
+ },
+ },
+ };
+
+ await renderComponent(filteredState);
+
+ expect(InventoryTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: expect.objectContaining({
+ deleteTitle: 'Reset filters',
+ showDeleteButton: true,
+ }),
+ customFilters: expect.objectContaining({
+ filters: [
+ {
+ osFilter: {
+ 'RHEL-8': {
+ 'RHEL-8-8.8': true,
+ },
+ },
+ },
+ ],
+ }),
+ }),
+ {},
+ );
+ });
+
+ it('should show reset before an inventory-backed fetch resolves', async () => {
+ let resolveFetch;
+ fetchSystems.mockImplementationOnce(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
+
+ await renderComponent(mockState);
+
+ const inventoryProps = InventoryTable.mock.calls[InventoryTable.mock.calls.length - 1][0];
+ let request;
+
+ act(() => {
+ request = inventoryProps.getEntities([], {
+ orderBy: 'display_name',
+ orderDirection: 'ASC',
+ page: 1,
+ per_page: 20,
+ patchParams: {
+ filter: { stale: [true, false] },
+ selectedTags: [],
+ systemProfile: {},
+ },
+ filters: {
+ osFilter: {
+ 'RHEL-8': {
+ 'RHEL-8-8.8': true,
+ },
+ },
+ },
+ });
+ });
+
+ await waitFor(() =>
+ expect(InventoryTable).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ activeFiltersConfig: expect.objectContaining({
+ deleteTitle: 'Reset filters',
+ showDeleteButton: true,
+ }),
+ }),
+ {},
+ ),
+ );
+
+ await act(async () => {
+ resolveFetch({
+ data: [],
+ meta: {
+ total_items: 0,
+ },
+ });
+ await request;
+ });
+ });
+
it('should provide bulkSelect config', async () => {
await renderComponent(mockState);
expect(InventoryTable).toHaveBeenCalledWith(
diff --git a/src/SmartComponents/Systems/SystemsMainContent.js b/src/SmartComponents/Systems/SystemsMainContent.js
index 5b01a164c..560c91c70 100644
--- a/src/SmartComponents/Systems/SystemsMainContent.js
+++ b/src/SmartComponents/Systems/SystemsMainContent.js
@@ -56,7 +56,7 @@ const SystemsMainContent = () => {
apply={apply}
setSearchParams={setSearchParams}
activateRemediationModal={activateRemediationModal}
- decodedParams={decodeQueryparams}
+ decodedParams={decodedParams}
/>
diff --git a/src/SmartComponents/Systems/SystemsMainContent.test.js b/src/SmartComponents/Systems/SystemsMainContent.test.js
index 222be9821..799859670 100644
--- a/src/SmartComponents/Systems/SystemsMainContent.test.js
+++ b/src/SmartComponents/Systems/SystemsMainContent.test.js
@@ -1,6 +1,7 @@
import configureStore from 'redux-mock-store';
import { initMocks } from '../../Utilities/unitTestingUtilities';
import Systems from './SystemsMainContent';
+import SystemsTable from './SystemsTable';
import { render, screen, waitFor } from '@testing-library/react';
import { ComponentWithContext } from '../../Utilities/TestingUtilities';
import '@testing-library/jest-dom';
@@ -75,9 +76,9 @@ const initStore = (state) => {
return mockStore(state);
};
-const renderComponent = async (mockedStore) => {
+const renderComponent = async (mockedStore, renderOptions = {}) => {
render(
-
+
,
);
@@ -85,6 +86,10 @@ const renderComponent = async (mockedStore) => {
const user = userEvent.setup();
describe('SystemsTable', () => {
+ beforeEach(() => {
+ SystemsTable.mockClear();
+ });
+
it('Should display systems table when there are no errors', async () => {
renderComponent(mockState);
expect(screen.getByTestId('systems-table-mock')).toBeVisible();
@@ -137,4 +142,19 @@ describe('SystemsTable', () => {
await user.click(screen.getByTestId('active-remediation-modal'));
expect(screen.getByTestId('remediation-wizard-mock')).toBeVisible();
});
+
+ it('should pass parsed decoded params into SystemsTable', async () => {
+ renderComponent(mockState, { initialEntries: ['/?search=test-search'] });
+
+ await waitFor(() => {
+ expect(SystemsTable).toHaveBeenCalledWith(
+ expect.objectContaining({
+ decodedParams: {
+ search: 'test-search',
+ },
+ }),
+ {},
+ );
+ });
+ });
});
diff --git a/src/SmartComponents/Systems/SystemsTable.js b/src/SmartComponents/Systems/SystemsTable.js
index 76f3e82d0..ed53f3c21 100644
--- a/src/SmartComponents/Systems/SystemsTable.js
+++ b/src/SmartComponents/Systems/SystemsTable.js
@@ -1,6 +1,7 @@
import React, { Fragment, useRef, useState } from 'react';
import { TableVariant } from '@patternfly/react-table';
import { InventoryTable } from '@redhat-cloud-services/frontend-components/Inventory';
+import isDeepEqualReact from 'fast-deep-equal/react';
import { shallowEqual, useDispatch, useSelector, useStore } from 'react-redux';
import { defaultReducers } from '../../store';
import { changeSystemsMetadata, changeTags, systemSelectAction } from '../../store/Actions/Actions';
@@ -9,8 +10,8 @@ import {
modifyInventory,
} from '../../store/Reducers/InventoryEntitiesReducer';
import { exportSystemsCSV, exportSystemsJSON, fetchSystems } from '../../Utilities/api/api';
-import { systemsListDefaultFilters, NO_ADVISORIES_TEXT } from '../../Utilities/constants';
-import { arrayFromObj, persistantParams } from '../../Utilities/Helpers';
+import { pageDefaultFilters, NO_ADVISORIES_TEXT } from '../../Utilities/constants';
+import { arrayFromObj, hasActiveInventoryFilters, persistantParams } from '../../Utilities/Helpers';
import {
useBulkSelectConfig,
useGetEntities,
@@ -31,6 +32,12 @@ import {
import { combineReducers } from 'redux';
import propTypes from 'prop-types';
+const buildInventorySnapshot = (filters = {}, selectedTags = [], systemProfile = {}) => ({
+ filters,
+ selectedTags: selectedTags || [],
+ systemProfile: systemProfile || {},
+});
+
const SystemsTable = ({ apply, setSearchParams, activateRemediationModal, decodedParams }) => {
const store = useStore();
const inventory = useRef(null);
@@ -76,6 +83,13 @@ const SystemsTable = ({ apply, setSearchParams, activateRemediationModal, decode
}, {}),
},
];
+ const [inventorySnapshot, setInventorySnapshot] = useState(() =>
+ buildInventorySnapshot(
+ operatingSystemFilter ? { osFilter: osFilter?.[0]?.osFilter || {} } : {},
+ selectedTags,
+ systemProfile,
+ ),
+ );
const applyMetadata = (metadata) => {
dispatch(changeSystemsMetadata(metadata));
@@ -85,10 +99,33 @@ const SystemsTable = ({ apply, setSearchParams, activateRemediationModal, decode
dispatch(changeTags(tags));
};
- const [deleteFilters] = useRemoveFilter({ search, ...filter }, apply, systemsListDefaultFilters);
+ const [deleteFilters] = useRemoveFilter({ search, ...filter }, apply, pageDefaultFilters.systems);
const filterConfig = buildFilterConfig(search, filter, apply);
+ const applyInventorySnapshot = React.useCallback((nextSnapshot) => {
+ setInventorySnapshot((previousSnapshot) =>
+ isDeepEqualReact(previousSnapshot, nextSnapshot) ? previousSnapshot : nextSnapshot,
+ );
+ }, []);
+ const hasInventoryFilterDeviation =
+ hasActiveInventoryFilters(inventorySnapshot.filters) ||
+ Boolean(inventorySnapshot.selectedTags?.length) ||
+ Boolean(
+ inventorySnapshot.systemProfile && Object.keys(inventorySnapshot.systemProfile).length > 0,
+ );
+
+ const activeFiltersConfig = React.useMemo(() => {
+ const config = buildActiveFiltersConfig(
+ filter,
+ search,
+ deleteFilters,
+ pageDefaultFilters.systems,
+ );
- const activeFiltersConfig = buildActiveFiltersConfig(filter, search, deleteFilters);
+ return {
+ ...config,
+ showDeleteButton: config.showDeleteButton || hasInventoryFilterDeviation,
+ };
+ }, [deleteFilters, filter, hasInventoryFilterDeviation, search]);
const onSelect = useOnSelect(systems, selectedRows, {
endpoint: ID_API_ENDPOINTS.systems,
@@ -114,6 +151,7 @@ const SystemsTable = ({ apply, setSearchParams, activateRemediationModal, decode
setSearchParams,
applyMetadata,
applyGlobalFilter,
+ applyInventorySnapshot,
);
const remediationDataProvider = useRemediationDataProvider(
@@ -216,6 +254,6 @@ SystemsTable.propTypes = {
apply: propTypes.func.isRequired,
setSearchParams: propTypes.func.isRequired,
activateRemediationModal: propTypes.func.isRequired,
- decodedParams: propTypes.func.isRequired,
+ decodedParams: propTypes.object.isRequired,
};
export default SystemsTable;
diff --git a/src/Utilities/Helpers.js b/src/Utilities/Helpers.js
index 26f1c1197..1bfd9ea4a 100644
--- a/src/Utilities/Helpers.js
+++ b/src/Utilities/Helpers.js
@@ -17,6 +17,7 @@ import messages from '../Messages';
import AdvisoriesIcon from '../PresentationalComponents/Snippets/AdvisoriesIcon';
import {
defaultCompoundSortValues,
+ emptyDefaultFilters,
filterCategories,
multiValueFilters,
SEVERITY_NONE,
@@ -356,6 +357,87 @@ export const decodeQueryparams = (queryString, parsers = {}) => {
const getFilterStringFromApi = (value) => (value === null ? 'null' : String(value));
+const compareNormalizedValues = (left, right) =>
+ getFilterStringFromApi(left).localeCompare(getFilterStringFromApi(right));
+
+const normalizeFilterComparisonValue = (category, value) => {
+ if (Array.isArray(value)) {
+ return [...value]
+ .map((item) => normalizeFilterComparisonValue(category, item))
+ .sort(compareNormalizedValues);
+ }
+
+ if (multiValueFilters.includes(category) && typeof value === 'string') {
+ return value.split(',').sort(compareNormalizedValues);
+ }
+
+ return value;
+};
+
+const areFilterValuesEqual = (category, currentValue, defaultValue) =>
+ JSON.stringify(normalizeFilterComparisonValue(category, currentValue)) ===
+ JSON.stringify(normalizeFilterComparisonValue(category, defaultValue));
+
+const sanitizeFilterState = (filters = {}) =>
+ Object.entries(filters).reduce((activeFilters, [category, value]) => {
+ if (value !== undefined && value !== '' && [].concat(value).length !== 0) {
+ activeFilters[category] = value;
+ }
+
+ return activeFilters;
+ }, {});
+
+const hasActiveNestedFilterValue = (value) => {
+ if (Array.isArray(value)) {
+ return value.some(hasActiveNestedFilterValue);
+ }
+
+ if (value && typeof value === 'object') {
+ return Object.values(value).some(hasActiveNestedFilterValue);
+ }
+
+ return ![undefined, null, '', false].includes(value);
+};
+
+export const getDefaultFilterState = (defaultFilters = emptyDefaultFilters) => ({
+ filter: sanitizeFilterState(defaultFilters?.filter ?? {}),
+ search: defaultFilters?.search ?? '',
+});
+
+export const hasActiveInventoryFilters = (filters = {}) => hasActiveNestedFilterValue(filters);
+
+export const hasDefaultFilterState = (defaultFilters = emptyDefaultFilters) => {
+ const { filter, search } = getDefaultFilterState(defaultFilters);
+
+ return Object.keys(filter).length > 0 || search !== '';
+};
+
+export const getActiveFilterState = (
+ filters = {},
+ search = '',
+ defaultFilters = emptyDefaultFilters,
+) => {
+ const sanitizedFilters = sanitizeFilterState(filters);
+ const { filter: defaultFilter, search: defaultSearch } = getDefaultFilterState(defaultFilters);
+
+ const activeFilters = Object.keys(sanitizedFilters).reduce((currentFilters, category) => {
+ const currentValue = sanitizedFilters[category];
+ const defaultValue = defaultFilter[category];
+
+ if (!areFilterValuesEqual(category, currentValue, defaultValue)) {
+ currentFilters[category] = currentValue;
+ }
+
+ return currentFilters;
+ }, {});
+
+ return {
+ filter: activeFilters,
+ hasDefaultFilterState: hasDefaultFilterState(defaultFilters),
+ search: (search ?? '') === defaultSearch ? '' : search,
+ };
+};
+
export const buildFilterChips = (filters, search, searchChipLabel = 'Search', parsers = {}) => {
let filterConfig = [];
const buildChips = (filters, category) => {
@@ -432,6 +514,32 @@ export const buildFilterChips = (filters, search, searchChipLabel = 'Search', pa
return filterConfig;
};
+export const buildActiveFilterConfig = (
+ filters,
+ search,
+ deleteFilters,
+ searchChipLabel = 'Search',
+ defaultFilters = emptyDefaultFilters,
+) => {
+ const visibleFilters = sanitizeFilterState(filters);
+ const visibleSearch = search ?? '';
+ const {
+ filter: deviatedFilters,
+ hasDefaultFilterState,
+ search: deviatedSearch,
+ } = getActiveFilterState(filters, search, defaultFilters);
+ const hasDeviation = Object.keys(deviatedFilters).length > 0 || Boolean(deviatedSearch);
+
+ return {
+ filters: buildFilterChips(visibleFilters, visibleSearch, searchChipLabel),
+ onDelete: deleteFilters,
+ deleteTitle: intl.formatMessage(
+ hasDefaultFilterState ? messages.labelsFiltersReset : messages.labelsFiltersClear,
+ ),
+ ...(hasDefaultFilterState && { showDeleteButton: hasDeviation }),
+ };
+};
+
export const buildOsFilter = (osFilter = {}) => {
const osVersions = Object.entries(osFilter).reduce(
(acc, [, osGroupValues]) => [
diff --git a/src/Utilities/Helpers.test.js b/src/Utilities/Helpers.test.js
index f6ce15e04..04af6b16c 100644
--- a/src/Utilities/Helpers.test.js
+++ b/src/Utilities/Helpers.test.js
@@ -1,10 +1,11 @@
/* eslint-disable */
import { SortByDirection } from '@patternfly/react-table';
-import { publicDateOptions, remediationIdentifiers } from '../Utilities/constants';
+import { pageDefaultFilters, publicDateOptions, remediationIdentifiers } from '../Utilities/constants';
import {
addOrRemoveItemFromSet,
arrayFromObj,
buildApiFilters,
+ buildActiveFilterConfig,
buildFilterChips,
changeListParams,
convertLimitOffset,
@@ -14,6 +15,7 @@ import {
encodeApiParams,
encodeParams,
encodeURLParams,
+ hasActiveInventoryFilters,
getFilterValue,
getLimitFromPageSize,
getNewSelectedItems,
@@ -311,6 +313,85 @@ describe('Helpers tests', () => {
},
);
+ it('buildActiveFilterConfig: should keep default filters visible while hiding reset at baseline', () => {
+ expect(
+ buildActiveFilterConfig(
+ { systems_applicable: ['gt:0'] },
+ '',
+ jest.fn(),
+ 'Package',
+ pageDefaultFilters.packages,
+ ),
+ ).toEqual({
+ deleteTitle: 'Reset filters',
+ filters: [
+ {
+ category: 'Status',
+ chips: [{ id: 'gt:0', name: 'Systems with patches available', value: 'gt:0' }],
+ id: 'systems_applicable',
+ },
+ ],
+ onDelete: expect.any(Function),
+ showDeleteButton: false,
+ });
+ });
+
+ it('buildActiveFilterConfig: should show reset when current state differs from defaults', () => {
+ expect(
+ buildActiveFilterConfig(
+ { systems_applicable: ['eq:0'] },
+ '',
+ jest.fn(),
+ 'Package',
+ pageDefaultFilters.packages,
+ ),
+ ).toEqual({
+ deleteTitle: 'Reset filters',
+ filters: [
+ {
+ category: 'Status',
+ chips: [{ id: 'eq:0', name: 'Systems up to date', value: 'eq:0' }],
+ id: 'systems_applicable',
+ },
+ ],
+ onDelete: expect.any(Function),
+ showDeleteButton: true,
+ });
+ });
+
+ it('buildActiveFilterConfig: should show clear filters when a page has no defaults', () => {
+ expect(
+ buildActiveFilterConfig(
+ { advisory_type_name: 'bugfix' },
+ '',
+ jest.fn(),
+ 'Advisory',
+ pageDefaultFilters.advisories,
+ ),
+ ).toEqual({
+ deleteTitle: 'Clear filters',
+ filters: [
+ {
+ category: 'Advisory type',
+ chips: [{ id: 'bugfix', name: 'Bugfix', value: 'bugfix' }],
+ id: 'advisory_type_name',
+ },
+ ],
+ onDelete: expect.any(Function),
+ });
+ });
+
+ it.each`
+ filters | result
+ ${{}} | ${false}
+ ${{ hostGroupFilter: [], osFilter: {}, tagFilters: [] }} | ${false}
+ ${{ osFilter: { 'RHEL-8': { 'RHEL-8-8.8': true } } }} | ${true}
+ ${{ tagFilters: [{ category: 'env', values: [{ tagKey: 'stage', value: 'prod' }] }] }} | ${true}
+ ${{ workspaceFilter: { workspaces: [{ id: 'workspace-1', name: 'Workspace 1' }] } }} | ${true}
+ `('hasActiveInventoryFilters: should detect inventory filter activity', ({ filters, result }) => {
+ expect(hasActiveInventoryFilters(filters)).toEqual(result);
+ });
+
it.each`
oldParams | newParams | result
${{ param: 'Hey!' }} | ${{ param: 'Yo!' }} | ${{ param: 'Yo!' }}
diff --git a/src/Utilities/SystemsHelpers.js b/src/Utilities/SystemsHelpers.js
index c0bad8547..3fae2b1ec 100644
--- a/src/Utilities/SystemsHelpers.js
+++ b/src/Utilities/SystemsHelpers.js
@@ -1,7 +1,7 @@
import searchFilter from '../PresentationalComponents/Filters/SearchFilter';
import staleFilter from '../PresentationalComponents/Filters/SystemStaleFilter';
import systemsUpdatableFilter from '../PresentationalComponents/Filters/SystemsUpdatableFilter';
-import { buildFilterChips } from './Helpers';
+import { buildActiveFilterConfig } from './Helpers';
import { intl } from './IntlProvider';
import messages from '../Messages';
import { defaultCompoundSortValues } from './constants';
@@ -19,21 +19,14 @@ export const buildFilterConfig = (search, filter, apply) => ({
],
});
-export const buildActiveFiltersConfig = (filter, search, deleteFilters) => {
- if (filter?.group_name?.length === 0) {
- delete filter.group_name;
- }
-
- return {
- filters: buildFilterChips(
- filter,
- search,
- intl.formatMessage(messages.labelsFiltersSystemsSearchTitle),
- ),
- onDelete: deleteFilters,
- deleteTitle: intl.formatMessage(messages.labelsFiltersReset),
- };
-};
+export const buildActiveFiltersConfig = (filter, search, deleteFilters, defaultFilters) =>
+ buildActiveFilterConfig(
+ filter,
+ search,
+ deleteFilters,
+ intl.formatMessage(messages.labelsFiltersSystemsSearchTitle),
+ defaultFilters,
+ );
export const mergeInventoryColumns = (patchmanColumns, inventoryColumns) =>
patchmanColumns.map((column) => ({
diff --git a/src/Utilities/constants.js b/src/Utilities/constants.js
index 624042816..dfd630940 100644
--- a/src/Utilities/constants.js
+++ b/src/Utilities/constants.js
@@ -57,6 +57,21 @@ export const systemsListDefaultFilters = {
filter: { stale: [true, false] },
};
+export const emptyDefaultFilters = {
+ filter: {},
+};
+
+export const pageDefaultFilters = {
+ advisories: emptyDefaultFilters,
+ advisorySystems: emptyDefaultFilters,
+ cves: emptyDefaultFilters,
+ packages: packagesListDefaultFilters,
+ packageSystems: emptyDefaultFilters,
+ systemAdvisories: emptyDefaultFilters,
+ systemPackages: systemPackagesDefaultFilters,
+ systems: systemsListDefaultFilters,
+};
+
export const publicDateOptions = [
{
apiValue: `gt:${subtractDate(7)}`,
diff --git a/src/Utilities/hooks/Hooks.js b/src/Utilities/hooks/Hooks.js
index 44e5963f7..f083ba6c3 100644
--- a/src/Utilities/hooks/Hooks.js
+++ b/src/Utilities/hooks/Hooks.js
@@ -13,6 +13,7 @@ import {
buildApiFilters,
getOffsetFromPageLimit,
encodeURLParams,
+ getDefaultFilterState,
mapGlobalFilters,
} from '../Helpers';
import { intl } from '../IntlProvider';
@@ -72,6 +73,9 @@ export const useSortColumn = (
};
export const useRemoveFilter = (filters, callback, defaultFilters = { filter: {} }) => {
+ const { filter: defaultFilterState, search: defaultSearch } =
+ getDefaultFilterState(defaultFilters);
+
const removeFilter = React.useCallback((selected, resetFilters, shouldReset) => {
let newParams = { filter: {} };
selected.forEach((selectedItem) => {
@@ -81,11 +85,11 @@ export const useRemoveFilter = (filters, callback, defaultFilters = { filter: {}
let activeFilter = filters[categoryId];
const toRemove = chips.map((item) => item.id?.toString());
if (Array.isArray(activeFilter)) {
- newParams.filter[categoryId] = activeFilter.filter(
- (item) => !toRemove.includes(item.toString()),
- );
+ const nextValue = activeFilter.filter((item) => !toRemove.includes(item.toString()));
+ newParams.filter[categoryId] =
+ nextValue.length > 0 ? nextValue : defaultFilterState[categoryId];
} else {
- newParams.filter[categoryId] = undefined;
+ newParams.filter[categoryId] = defaultFilterState[categoryId];
}
} else if (multiValueFilters.includes(categoryId)) {
const filterValues =
@@ -94,14 +98,16 @@ export const useRemoveFilter = (filters, callback, defaultFilters = { filter: {}
filters[categoryId])) ||
[];
- newParams.filter[categoryId] =
+ const nextValue =
(filterValues.length !== 1 &&
filterValues
.filter((filterValue) => !chips.find((chip) => chip.value === filterValue))
.join(',')) ||
undefined;
+
+ newParams.filter[categoryId] = nextValue ?? defaultFilterState[categoryId];
} else {
- newParams.search = '';
+ newParams.search = defaultSearch;
}
});
@@ -118,8 +124,8 @@ export const useRemoveFilter = (filters, callback, defaultFilters = { filter: {}
const deleteFilters = (__, selected, shouldReset) => {
const resetFilters = (currentFilters) => {
- if (Object.keys(defaultFilters.filter).length > 0) {
- currentFilters.filter = { ...currentFilters.filter, ...defaultFilters.filter };
+ if (Object.keys(defaultFilterState).length > 0) {
+ currentFilters.filter = { ...currentFilters.filter, ...defaultFilterState };
}
return currentFilters;
@@ -215,6 +221,7 @@ export const useGetEntities = (
setSearchParams,
applyMetadata,
applyGlobalFilter,
+ applyInventorySnapshot,
) => {
const { id, packageName } = config || {};
const mounted = useRef(true);
@@ -228,12 +235,22 @@ export const useGetEntities = (
const sort = createSystemsSortBy(orderBy, orderDirection, packageName);
const filter = buildApiFilters(patchParams.filter, filters);
+ const nextSelectedTags = [...activeTags, ...selectedTags];
+
+ applyInventorySnapshot &&
+ applyInventorySnapshot({
+ filter,
+ filters,
+ selectedTags: nextSelectedTags,
+ systemProfile: patchParams.systemProfile || {},
+ });
+
const items = await fetchApi({
page,
perPage,
...patchParams,
filter,
- selectedTags: [...activeTags, ...selectedTags],
+ selectedTags: nextSelectedTags,
sort,
...((id && { id }) || {}),
...((packageName && { package_name: packageName }) || {}),
diff --git a/src/Utilities/hooks/Hooks.test.js b/src/Utilities/hooks/Hooks.test.js
index 41321ccdc..69ea66515 100644
--- a/src/Utilities/hooks/Hooks.test.js
+++ b/src/Utilities/hooks/Hooks.test.js
@@ -1,6 +1,7 @@
import { SortByDirection } from '@patternfly/react-table';
import {
useEntitlements,
+ useGetEntities,
useHandleRefresh,
usePagePerPage,
usePerPageSelect,
@@ -127,6 +128,19 @@ describe('Custom hooks tests', () => {
},
);
+ it('useRemoveFilter: should restore category defaults when removing a deviated default filter', () => {
+ const apply = jest.fn();
+ const { result } = renderHook(() =>
+ useRemoveFilter({ systems_applicable: ['eq:0'] }, apply, packagesListDefaultFilters),
+ );
+
+ result.current[0]({}, [{ id: 'systems_applicable', chips: [{ id: 'eq:0' }] }]);
+
+ expect(apply).toHaveBeenCalledWith({
+ filter: { systems_applicable: ['gt:0'] },
+ });
+ });
+
it.each`
metadata | apply | input | finalResult
${{ limit: 10, offset: 0 }} | ${jest.fn()} | ${{ page: 2, per_page: 10 }} | ${{ offset: 10 }}
@@ -142,6 +156,76 @@ describe('Custom hooks tests', () => {
}
});
+ it('useGetEntities: should publish the inventory snapshot before the fetch resolves', async () => {
+ let resolveFetch;
+ const fetchApi = jest.fn(
+ () =>
+ new Promise((resolve) => {
+ resolveFetch = resolve;
+ }),
+ );
+ const apply = jest.fn();
+ const setSearchParams = jest.fn();
+ const applyMetadata = jest.fn();
+ const applyGlobalFilter = jest.fn();
+ const applyInventorySnapshot = jest.fn();
+ const params = {
+ orderBy: 'display_name',
+ orderDirection: 'ASC',
+ page: 1,
+ per_page: 20,
+ patchParams: {
+ filter: { stale: [true, false] },
+ selectedTags: ['tags=owner%2Fteam%3Dplatform'],
+ systemProfile: { ansible: { controller_version: 'not_nil' } },
+ },
+ filters: {
+ hostGroupFilter: ['web'],
+ osFilter: {
+ 'RHEL-8': {
+ 'RHEL-8-8.8': true,
+ },
+ },
+ tagFilters: [
+ {
+ category: 'env',
+ values: [{ tagKey: 'stage', value: 'prod' }],
+ },
+ ],
+ },
+ };
+ const { result } = renderHook(() =>
+ useGetEntities(
+ fetchApi,
+ apply,
+ {},
+ setSearchParams,
+ applyMetadata,
+ applyGlobalFilter,
+ applyInventorySnapshot,
+ ),
+ );
+
+ const request = result.current([], params);
+
+ expect(applyInventorySnapshot).toHaveBeenCalledWith({
+ filter: {
+ stale: [true, false],
+ group_name: ['web'],
+ os: 'RHEL 8.8',
+ },
+ filters: params.filters,
+ selectedTags: ['tags=owner%2Fteam%3Dplatform', ['tags=env%2Fstage%3Dprod']],
+ systemProfile: { ansible: { controller_version: 'not_nil' } },
+ });
+ expect(applyInventorySnapshot.mock.invocationCallOrder[0]).toBeLessThan(
+ fetchApi.mock.invocationCallOrder[0],
+ );
+
+ resolveFetch({ data: [], meta: { total_items: 0 } });
+ await request;
+ });
+
it('useEntitlements, should return correct entitlements', async () => {
const { result } = renderHook(() => useEntitlements());
const finalResult = await result.current();