diff --git a/cypress/e2e/roles.cy.ts b/cypress/e2e/roles.cy.ts
index dc58e3339..893e7bd29 100644
--- a/cypress/e2e/roles.cy.ts
+++ b/cypress/e2e/roles.cy.ts
@@ -33,9 +33,109 @@ describe('Roles page', () => {
external_role_id: null,
external_tenant: null,
},
+ {
+ uuid: '00000-00000-1111-1111-111111',
+ name: 'A_Test_00000-00000',
+ display_name: 'A Test',
+ description: 'Test role A',
+ created: '2024-05-17T05:03:15.684013Z',
+ modified: '2024-05-17T05:03:15.709410Z',
+ policyCount: 2,
+ accessCount: 2,
+ applications: [],
+ system: false,
+ platform_default: false,
+ admin_default: false,
+ external_role_id: null,
+ external_tenant: null,
+ },
+ ],
+ meta: {
+ count: 3,
+ limit: 20,
+ offset: 0,
+ },
+ };
+
+ const sortedRoles = {
+ data: [
+ {
+ uuid: '00000-00000-1111-1111-111111',
+ name: 'A_Test_00000-00000',
+ display_name: 'A Test',
+ description: 'Test role A',
+ created: '2024-05-17T05:03:15.684013Z',
+ modified: '2024-05-17T05:03:15.709410Z',
+ policyCount: 2,
+ accessCount: 2,
+ applications: [],
+ system: false,
+ platform_default: false,
+ admin_default: false,
+ external_role_id: null,
+ external_tenant: null,
+ },
+ {
+ uuid: '00000-0000-0000-0000-00000000',
+ name: 'Test_1_00000-00000',
+ display_name: 'Test 1',
+ description: 'Test role1',
+ created: '2024-05-17T05:03:15.684013Z',
+ modified: '2024-05-17T05:03:15.709410Z',
+ policyCount: 2,
+ accessCount: 2,
+ applications: [],
+ system: false,
+ platform_default: false,
+ admin_default: false,
+ external_role_id: null,
+ external_tenant: null,
+ },
+ {
+ uuid: '00000-11111-1111-1111-111111',
+ name: 'Test_2_00000-00000',
+ display_name: 'Test 2',
+ description: 'Test role2',
+ created: '2024-05-17T05:03:15.684013Z',
+ modified: '2024-05-17T05:03:15.709410Z',
+ policyCount: 2,
+ accessCount: 2,
+ applications: [],
+ system: false,
+ platform_default: false,
+ admin_default: false,
+ external_role_id: null,
+ external_tenant: null,
+ },
],
meta: {
- count: 2,
+ count: 3,
+ limit: 20,
+ offset: 0,
+ },
+ };
+
+ const filteredRoles = {
+ data: [
+ {
+ uuid: '00000-00000-1111-1111-111111',
+ name: 'A_Test_00000-00000',
+ display_name: 'A Test',
+ description: 'Test role A',
+ created: '2024-05-17T05:03:15.684013Z',
+ modified: '2024-05-17T05:03:15.709410Z',
+ policyCount: 2,
+ accessCount: 2,
+ applications: [],
+ system: false,
+ platform_default: false,
+ admin_default: false,
+ external_role_id: null,
+ external_tenant: null,
+ },
+ ],
+ meta: {
+ count: 1,
limit: 20,
offset: 0,
},
@@ -85,4 +185,24 @@ describe('Roles page', () => {
cy.get('[data-ouia-component-id^="RolesTable-assigned-groups-tab"]').first().click();
cy.get('[data-ouia-component-id^="assigned-usergroups-table-stack"]').should('be.visible');
});
+
+ it('should sort roles', () => {
+ cy.intercept('GET', '**/api/rbac/v1/roles/?limit=20&display_name=&scope=org_id&order_by=display_name*', {
+ statusCode: 200,
+ body: sortedRoles,
+ }).as('sortRoles');
+ cy.get('[data-ouia-component-id="RolesTable-table-th-0"]').click();
+ cy.wait('@sortRoles', { timeout: 15000 });
+ cy.get('[data-ouia-component-id="RolesTable-table-td-0-0"]').should('contain.text', 'A Test');
+ });
+
+ it('should filter roles by name', () => {
+ cy.intercept('GET', '**/api/rbac/v1/roles/?limit=20&display_name=A*', {
+ statusCode: 200,
+ body: filteredRoles,
+ }).as('filterRoles');
+ cy.get('[data-ouia-component-id="RolesTable-name-filter-input"]').type('A');
+ cy.wait('@filterRoles', { timeout: 15000 });
+ cy.get('table tbody tr').should('have.length', 1);
+ });
});
diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts
index 9511a2497..8f3587232 100644
--- a/cypress/support/commands.ts
+++ b/cypress/support/commands.ts
@@ -47,6 +47,10 @@ Cypress.Commands.add('login', (enableWorkspaces = false) => {
// This JS file causes randomly an uncaught exception on login page which blocks the tests
// Cannot read properties of undefined (reading 'setAttribute')
cy.intercept({ url: 'https://sso.stage.redhat.com/auth/resources/0833r/login/rhd-theme/dist/pfelements/bundle.js' }, {});
+ Cypress.on('uncaught:exception', (err) => {
+ console.log(err);
+ return false;
+ });
cy.visit('/');
// disable analytics integrations
cy.setLocalStorage('chrome:analytics:disable', 'true');
diff --git a/package-lock.json b/package-lock.json
index dabf22968..9414c46a8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,7 +18,7 @@
"@patternfly/quickstarts": "^5.1.0",
"@patternfly/react-component-groups": "^5.5.5",
"@patternfly/react-core": "^5.1.1",
- "@patternfly/react-data-view": "^5.7.0",
+ "@patternfly/react-data-view": "^5.7.1",
"@patternfly/react-icons": "^5.1.1",
"@patternfly/react-table": "^5.1.1",
"@patternfly/react-tokens": "^5.1.1",
@@ -4372,10 +4372,9 @@
}
},
"node_modules/@patternfly/react-data-view": {
- "version": "5.7.0",
- "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-5.7.0.tgz",
- "integrity": "sha512-EcCy8+5xdD48AvZyONfO8pZ7LM53eIkvT23guQ1QkmI253tUQYk16TlfJQMBC8aBZztu7wd0fWjDNHKVvgJA9Q==",
- "license": "MIT",
+ "version": "5.8.0",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-data-view/-/react-data-view-5.8.0.tgz",
+ "integrity": "sha512-+GDaWmr19t71q83yiamFPnyxeak0I70gTxWmkD/tQEnE/m6+5MZy+jtJ1lsT5BFPXNvwweEwfaXwiFzPTxTV3w==",
"dependencies": {
"@patternfly/react-component-groups": "^5.5.2",
"@patternfly/react-core": "^5.4.1",
diff --git a/package.json b/package.json
index 5068f6263..8f2a776e4 100644
--- a/package.json
+++ b/package.json
@@ -16,7 +16,7 @@
"@patternfly/quickstarts": "^5.1.0",
"@patternfly/react-component-groups": "^5.5.5",
"@patternfly/react-core": "^5.1.1",
- "@patternfly/react-data-view": "^5.7.0",
+ "@patternfly/react-data-view": "^5.7.1",
"@patternfly/react-icons": "^5.1.1",
"@patternfly/react-table": "^5.1.1",
"@patternfly/react-tokens": "^5.1.1",
diff --git a/src/Messages.js b/src/Messages.js
index 48447be0d..9150d8a69 100644
--- a/src/Messages.js
+++ b/src/Messages.js
@@ -2518,6 +2518,11 @@ export default defineMessages({
description: 'confirm button for deleting role',
defaultMessage: 'Delete role',
},
+ nameFilterPlaceholder: {
+ id: 'nameFilterPlaceholder',
+ description: 'placeholder for name filter',
+ defaultMessage: 'Filter by name',
+ },
deleteRolesAction: {
id: 'deleteRolesAction',
description: 'delete roles',
diff --git a/src/smart-components/access-management/UserGroupsTable.tsx b/src/smart-components/access-management/UserGroupsTable.tsx
index 83a7650e8..ec383ade1 100644
--- a/src/smart-components/access-management/UserGroupsTable.tsx
+++ b/src/smart-components/access-management/UserGroupsTable.tsx
@@ -17,7 +17,7 @@ import messages from '../../Messages';
import { Group } from '../../redux/reducers/group-reducer';
import { DataViewTrObject, DataViewState, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
import { SearchIcon } from '@patternfly/react-icons';
-import { ResponsiveAction, ResponsiveActions, SkeletonTableBody, WarningModal } from '@patternfly/react-component-groups';
+import { ResponsiveAction, ResponsiveActions, SkeletonTableBody, SkeletonTableHead, WarningModal } from '@patternfly/react-component-groups';
import AddGroupWizard from '../group/add-group/add-group-wizard';
const COLUMNS: string[] = ['User group name', 'Description', 'Users', 'Service accounts', 'Roles', 'Workspaces', 'Last modified'];
@@ -30,6 +30,25 @@ const PER_PAGE_OPTIONS = [
{ title: '100', value: 100 },
];
+const EmptyTable: React.FunctionComponent<{ titleText: string }> = ({ titleText }) => {
+ return (
+
+ } />
+
+ ,
+ }}
+ />
+
+
+ );
+};
+
+const loadingHeader = ;
+const loadingBody = ;
+
interface UserGroupsTableProps {
defaultPerPage?: number;
useUrlParams?: boolean;
@@ -207,26 +226,6 @@ const UserGroupsTable: React.FunctionComponent = ({
/>
);
- const empty = (
-
- }
- />
-
- ,
- }}
- />
-
-
- );
-
- const loading = ;
-
return (
{isAddGroupWizardOpen && (
@@ -302,7 +301,8 @@ const UserGroupsTable: React.FunctionComponent = ({
ouiaId={`${ouiaId}-table`}
columns={COLUMNS}
rows={rows}
- bodyStates={{ empty, loading }}
+ headStates={{ loading: loadingHeader }}
+ bodyStates={{ loading: loadingBody, empty: }}
/>
diff --git a/src/smart-components/access-management/UsersTable.tsx b/src/smart-components/access-management/UsersTable.tsx
index ddfe1958b..857c43243 100644
--- a/src/smart-components/access-management/UsersTable.tsx
+++ b/src/smart-components/access-management/UsersTable.tsx
@@ -16,7 +16,7 @@ import { User } from '../../redux/reducers/user-reducer';
import { FormattedMessage, useIntl } from 'react-intl';
import messages from '../../Messages';
import { Outlet, useSearchParams } from 'react-router-dom';
-import { SkeletonTableBody, WarningModal } from '@patternfly/react-component-groups';
+import { SkeletonTableBody, SkeletonTableHead, WarningModal } from '@patternfly/react-component-groups';
import paths from '../../utilities/pathnames';
import useChrome from '@redhat-cloud-services/frontend-components/useChrome';
import useAppNavigate from '../../hooks/useAppNavigate';
@@ -33,6 +33,25 @@ const PER_PAGE_OPTIONS = [
{ title: '100', value: 100 },
];
+const EmptyTable: React.FunctionComponent<{ titleText: string }> = ({ titleText }) => {
+ return (
+
+ } />
+
+ ,
+ }}
+ />
+
+
+ );
+};
+
+const loadingHeader = ;
+const loadingBody = ;
+
const OUIA_ID = 'iam-users-table';
interface UsersTableProps {
@@ -159,22 +178,6 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick,
/>
);
- const empty = (
-
- } />
-
- ,
- }}
- />
-
-
- );
-
- const loading = ;
-
return (
{isDeleteModalOpen && (
@@ -239,7 +242,8 @@ const UsersTable: React.FunctionComponent = ({ onAddUserClick,
ouiaId={`${OUIA_ID}-table`}
columns={COLUMNS}
rows={rows}
- bodyStates={{ empty, loading }}
+ headStates={{ loading: loadingHeader }}
+ bodyStates={{ loading: loadingBody, empty: }}
/>
diff --git a/src/smart-components/role/RolesTable.tsx b/src/smart-components/role/RolesTable.tsx
index 257d675de..d6f152c2d 100644
--- a/src/smart-components/role/RolesTable.tsx
+++ b/src/smart-components/role/RolesTable.tsx
@@ -6,19 +6,20 @@ import { DataView } from '@patternfly/react-data-view/dist/dynamic/DataView';
import { DataViewToolbar } from '@patternfly/react-data-view/dist/dynamic/DataViewToolbar';
import { DataViewTable } from '@patternfly/react-data-view/dist/dynamic/DataViewTable';
import { DataViewEventsProvider, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view/dist/dynamic/DataViewEventsContext';
+import { useDataViewSort } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { ButtonVariant, Drawer, DrawerContent, DrawerContentBody, PageSection, Pagination } from '@patternfly/react-core';
-import { ActionsColumn } from '@patternfly/react-table';
+import { ActionsColumn, ThProps } from '@patternfly/react-table';
import ContentHeader from '@patternfly/react-component-groups/dist/esm/ContentHeader';
import { fetchRolesWithPolicies, removeRole } from '../../redux/actions/role-actions';
import { FormattedMessage, useIntl } from 'react-intl';
import messages from '../../Messages';
-import { mappedProps } from '../../helpers/shared/helpers';
+import { debouncedFetch, mappedProps } from '../../helpers/shared/helpers';
import { Role } from '../../redux/reducers/role-reducer';
import { RBACStore } from '../../redux/store';
import { useSearchParams } from 'react-router-dom';
import RolesDetails from './RolesTableDetails';
import { ResponsiveAction, ResponsiveActions, WarningModal } from '@patternfly/react-component-groups';
-import { DataViewTrObject } from '@patternfly/react-data-view';
+import { DataViewTextFilter, DataViewTh, DataViewTr, DataViewTrObject, useDataViewFilters } from '@patternfly/react-data-view';
const PER_PAGE = [
{ title: '5', value: 5 },
@@ -28,6 +29,10 @@ const PER_PAGE = [
{ title: '100', value: 100 },
];
+interface RoleFilters {
+ display_name: string;
+}
+
const ouiaId = 'RolesTable';
interface RolesTableProps {
@@ -51,17 +56,25 @@ const RolesTable: React.FunctionComponent = ({ selectedRole })
setIsDeleteModalOpen(!isDeleteModalOpen);
};
- const COLUMNS: string[] = [
- intl.formatMessage(messages.name),
- intl.formatMessage(messages.description),
- intl.formatMessage(messages.permissions),
- intl.formatMessage(messages.workspaces),
- intl.formatMessage(messages.userGroups),
- intl.formatMessage(messages.lastModified),
+ const COLUMNHEADERS = [
+ { label: intl.formatMessage(messages.name), key: 'display_name', index: 0, isSortable: true },
+ { label: intl.formatMessage(messages.description), key: 'description', index: 1, isSortable: false },
+ { label: intl.formatMessage(messages.permissions), key: 'accessCount', index: 2, isSortable: false },
+ { label: intl.formatMessage(messages.workspaces), key: 'workspaces', index: 3, isSortable: false },
+ { label: intl.formatMessage(messages.userGroups), key: 'user_groups', index: 4, isSortable: false },
+ { label: intl.formatMessage(messages.lastModified), key: 'modified', index: 5, isSortable: true },
];
const dispatch = useDispatch();
const [searchParams, setSearchParams] = useSearchParams();
+ const { filters, onSetFilters, clearAllFilters } = useDataViewFilters({
+ initialFilters: { display_name: '' },
+ searchParams,
+ setSearchParams,
+ });
+
+ const { sortBy, direction, onSort } = useDataViewSort({ searchParams, setSearchParams });
+ const sortByIndex = useMemo(() => COLUMNHEADERS.findIndex((item) => (item.isSortable ? item.key === sortBy : '')), [sortBy]);
const pagination = useDataViewPagination({ perPage: 20, searchParams, setSearchParams });
const { page, perPage, onSetPage, onPerPageSelect } = pagination;
@@ -70,9 +83,9 @@ const RolesTable: React.FunctionComponent = ({ selectedRole })
const { selected, onSelect, isSelected } = selection;
const fetchData = useCallback(
- (apiProps: { count: number; limit: number; offset: number; orderBy: string }) => {
- const { count, limit, offset, orderBy } = apiProps;
- dispatch(fetchRolesWithPolicies({ ...mappedProps({ count, limit, offset, orderBy }) }));
+ (apiProps: { limit: number; offset: number; orderBy: string; filters: RoleFilters }) => {
+ const { limit, offset, orderBy, filters } = apiProps;
+ dispatch(fetchRolesWithPolicies({ ...mappedProps({ limit, offset, orderBy, filters }) }));
},
[dispatch]
);
@@ -81,12 +94,43 @@ const RolesTable: React.FunctionComponent = ({ selectedRole })
fetchData({
limit: perPage,
offset: (page - 1) * perPage,
- orderBy: 'display_name',
- count: totalCount || 0,
+ orderBy: `${direction === 'desc' ? '-' : ''}${sortBy}`,
+ filters: filters,
});
- }, [fetchData, page, perPage]);
+ }, [fetchData, page, perPage, sortBy, direction]);
+
+ useEffect(() => {
+ debouncedFetch(
+ () =>
+ fetchData({
+ limit: perPage,
+ offset: (page - 1) * perPage,
+ orderBy: `${direction === 'desc' ? '-' : ''}${sortBy}`,
+ filters: filters,
+ }),
+ 800
+ );
+ }, [debouncedFetch, filters, onSetFilters]);
+
+ const getSortParams = (columnIndex: number): ThProps['sort'] => ({
+ sortBy: {
+ index: sortByIndex,
+ direction,
+ defaultDirection: 'asc',
+ },
+ onSort: (_event, index, direction) => {
+ onSort(_event, COLUMNHEADERS[index].key, direction);
+ onSetPage(undefined, 1);
+ },
+ columnIndex,
+ });
+
+ const columns: DataViewTh[] = COLUMNHEADERS.map((column, index) => ({
+ cell: column.label,
+ props: column.isSortable ? { sort: getSortParams(index) } : {},
+ }));
- const rows = useMemo(() => {
+ const rows: DataViewTr[] = useMemo(() => {
const handleRowClick = (event: any, role: Role | undefined) => {
(event.target.matches('td') || event.target.matches('tr')) && trigger(EventTypes.rowClick, role);
};
@@ -122,7 +166,7 @@ const RolesTable: React.FunctionComponent = ({ selectedRole })
isRowSelected: selectedRole?.name === role.name,
},
}));
- }, [roles, handleModalToggle, trigger, selectedRole, selectedRole?.display_name]);
+ }, [roles, handleModalToggle, trigger, selectedRole, selectedRole?.display_name, sortBy, onSort, direction, filters, onSetFilters]);
const handleBulkSelect = (value: BulkSelectValue) => {
value === BulkSelectValue.none && onSelect(false);
@@ -174,6 +218,7 @@ const RolesTable: React.FunctionComponent = ({ selectedRole })
= ({ selectedRole })
onPerPageSelect={onPerPageSelect}
/>
}
+ filters={
+ {
+ onSetFilters({ display_name: value });
+ onSetPage(undefined, 1);
+ }}
+ value={filters['display_name']}
+ />
+ }
/>
-
+