Skip to content

Commit

Permalink
feat: create delete user groups modal (#1703)
Browse files Browse the repository at this point in the history
* feat: create delete user groups modal

* fix: changes from pr feedback

* fix: use responsive actions for user group toolbar

* fix: update ouiaId for cypress test
  • Loading branch information
CodyWMitchell authored Nov 19, 2024
1 parent c3030aa commit ed28740
Show file tree
Hide file tree
Showing 5 changed files with 138 additions and 36 deletions.
14 changes: 14 additions & 0 deletions cypress/e2e/users-and-user-groups.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,18 @@ describe('Users and User Groups page', () => {
cy.get('[data-ouia-component-id="user-details-drawer"]').contains(mockUsers.data[0].last_name).should('exist');
cy.get('[data-ouia-component-id="user-details-drawer"]').contains(mockUsers.data[0].email).should('exist');
});

it('should be able to open Delete User Groups Modal from row actions', () => {
cy.get('[data-ouia-component-id="user-groups-tab-button"]').click();
cy.get('[data-ouia-component-id^="iam-user-groups-table-table-td-0-7"]').click();
cy.get('[data-ouia-component-id^="iam-user-groups-table-table-td-0-7"] button').contains('Delete user group').click();
cy.get('[data-ouia-component-id="iam-user-groups-table-remove-user-modal"]').should('be.visible');
});

it('should be able to open Delete User Groups modal from toolbar', () => {
cy.get('[data-ouia-component-id="user-groups-tab-button"]').click();
cy.get('[data-ouia-component-id^="iam-user-groups-table-table-tr-0"]').find('input[type="checkbox"]').click();
cy.get('[data-ouia-component-id="iam-user-groups-table-actions-dropdown-menu-control"]').click();
cy.get('[data-ouia-component-id="iam-user-groups-table-actions-dropdown-menu-control"] button').contains('Delete user group').click();
});
});
10 changes: 10 additions & 0 deletions src/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -2356,6 +2356,16 @@ export default defineMessages({
description: 'Modal body text for delete user',
defaultMessage: 'will lose all the roles associated with the user groups it belongs to.',
},
deleteUserGroupModalTitle: {
id: 'deleteUserGroupModalTitle',
description: 'Title for delete user group modal',
defaultMessage: 'Delete user {count, plural, one {group} other {groups}}?',
},
deleteUserGroupModalBody: {
id: 'deleteUserGroupModalBody',
description: 'Modal body text for delete user group',
defaultMessage: 'Deleting {count, plural, one {the <b>{name}</b> user group} other {{count} user groups}} will impact user access configuration.',
},
addToUserGroup: {
id: 'addToUserGroup',
description: 'Action column option to add user to group',
Expand Down
1 change: 1 addition & 0 deletions src/redux/reducers/group-reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface Group {
modified?: string;
admin_default?: boolean;
platform_default?: boolean;
system?: boolean;
}

export interface GroupStore {
Expand Down
147 changes: 112 additions & 35 deletions src/smart-components/access-management/UserGroupsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,23 @@
import React, { useEffect, useCallback, useMemo, useState } from 'react';
import React, { useEffect, useCallback, useMemo, useState, Fragment } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { useDataViewSelection, useDataViewPagination } from '@patternfly/react-data-view/dist/dynamic/Hooks';
import { BulkSelect, BulkSelectValue } from '@patternfly/react-component-groups/dist/dynamic/BulkSelect';
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 { EmptyState, EmptyStateBody, EmptyStateHeader, EmptyStateIcon, Pagination, Tooltip } from '@patternfly/react-core';
import { ButtonVariant, EmptyState, EmptyStateBody, EmptyStateHeader, EmptyStateIcon, Pagination, Tooltip } from '@patternfly/react-core';
import { ActionsColumn, TableVariant } from '@patternfly/react-table';
import { mappedProps } from '../../helpers/shared/helpers';
import { RBACStore } from '../../redux/store';
import { useSearchParams } from 'react-router-dom';
import { fetchGroups } from '../../redux/actions/group-actions';
import { fetchGroups, removeGroups } from '../../redux/actions/group-actions';
import { formatDistanceToNow } from 'date-fns';
import { FormattedMessage, useIntl } from 'react-intl';
import messages from '../../Messages';
import { Group } from '../../redux/reducers/group-reducer';
import { DataViewState, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
import { DataViewTrObject, DataViewState, EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
import { SearchIcon } from '@patternfly/react-icons';
import { SkeletonTable } from '@patternfly/react-component-groups';
import { ResponsiveAction, ResponsiveActions, SkeletonTable, WarningModal } from '@patternfly/react-component-groups';

const COLUMNS: string[] = ['User group name', 'Description', 'Users', 'Service accounts', 'Roles', 'Workspaces', 'Last modified'];

Expand Down Expand Up @@ -46,15 +46,17 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
onChange,
focusedGroup,
}) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false);
const [currentGroups, setCurrentGroups] = React.useState<Group[]>([]);
const dispatch = useDispatch();
const [activeState, setActiveState] = useState<DataViewState | undefined>(DataViewState.loading);
const intl = useIntl();
const { trigger } = useDataViewEventsContext();

const rowActions = [
{ title: intl.formatMessage(messages['usersAndUserGroupsEditUserGroup']), onClick: () => console.log('EDIT USER GROUP') },
{ title: intl.formatMessage(messages['usersAndUserGroupsDeleteUserGroup']), onClick: () => console.log('DELETE USER GROUP') },
];
const handleDeleteModalToggle = (groups: Group[]) => {
setCurrentGroups(groups);
setIsDeleteModalOpen(!isDeleteModalOpen);
};

const { groups, totalCount, isLoading } = useSelector((state: RBACStore) => ({
groups: state.groupReducer?.groups?.data || [],
Expand Down Expand Up @@ -130,7 +132,7 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
(event.target.matches('td') || event.target.matches('tr')) && trigger(EventTypes.rowClick, group);
};

return groups.map((group: any) => ({
return groups.map((group: Group) => ({
id: group.uuid,
row: [
group.name,
Expand All @@ -142,12 +144,27 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
<div className="pf-v5-u-color-400">{intl.formatMessage(messages['usersAndUserGroupsNoDescription'])}</div>
),
group.principalCount,
group.serviceAccounts || '?', // not currently in API
'?', // not currently in API
group.roleCount,
group.workspaces || '?', // not currently in API
formatDistanceToNow(new Date(group.modified), { addSuffix: true }),
'?', // not currently in API
group.modified ? formatDistanceToNow(new Date(group.modified), { addSuffix: true }) : '',
enableActions && {
cell: <ActionsColumn items={rowActions} />,
cell: (
<ActionsColumn
items={[
{
title: intl.formatMessage(messages['usersAndUserGroupsEditUserGroup']),
onClick: () => console.log('EDIT USER GROUP'),
},
{
title: intl.formatMessage(messages['usersAndUserGroupsDeleteUserGroup']),
onClick: () => handleDeleteModalToggle([group]),
},
]}
rowData={group}
isDisabled={group.platform_default || group.system}
/>
),
props: { isActionCell: true },
},
],
Expand All @@ -161,6 +178,21 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({

const pageSelected = rows.length > 0 && rows.every(isSelected);
const pagePartiallySelected = !pageSelected && rows.some(isSelected);
const isRowSystemOrPlatformDefault = (selectedRow: any) => {
const group = groups.find((group) => group.uuid === selectedRow.id);
return group?.platform_default || group?.system;
};

const handleDeleteGroups = async (groupsToDelete: Group[]) => {
await dispatch(removeGroups(groupsToDelete.map((group) => group.uuid)));
setIsDeleteModalOpen(false);
fetchData({
limit: perPage,
offset: (page - 1) * perPage,
orderBy: 'name',
count: totalCount || 0,
});
};

const paginationComponent = (
<Pagination
Expand Down Expand Up @@ -192,29 +224,74 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
);

return (
<DataView ouiaId={ouiaId} selection={selection} activeState={activeState}>
<DataViewToolbar
ouiaId={`${ouiaId}-header-toolbar`}
bulkSelect={
<BulkSelect
isDataPaginated
pageCount={groups.length}
selectedCount={selected.length}
totalCount={totalCount}
pageSelected={pageSelected}
pagePartiallySelected={pagePartiallySelected}
onSelect={handleBulkSelect}
<Fragment>
{isDeleteModalOpen && (
<WarningModal
ouiaId={`${ouiaId}-remove-user-modal`}
isOpen={isDeleteModalOpen}
title={intl.formatMessage(messages.deleteUserGroupModalTitle, { count: currentGroups.length })}
withCheckbox
checkboxLabel={intl.formatMessage(messages.understandActionIrreversible)}
confirmButtonLabel={intl.formatMessage(messages.remove)}
confirmButtonVariant={ButtonVariant.danger}
onClose={() => setIsDeleteModalOpen(false)}
onConfirm={() => {
handleDeleteGroups(currentGroups);
}}
>
<FormattedMessage
{...messages.deleteUserGroupModalBody}
values={{
b: (text) => <b>{text}</b>,
count: currentGroups.length,
plural: currentGroups.length > 1 ? intl.formatMessage(messages.groups) : intl.formatMessage(messages.group),
name: currentGroups[0]?.name,
}}
/>
}
pagination={React.cloneElement(paginationComponent, { isCompact: true })}
/>
{isLoading ? (
<SkeletonTable rowsCount={10} columns={COLUMNS} variant={TableVariant.compact} />
) : (
<DataViewTable variant="compact" aria-label="Users Table" ouiaId={`${ouiaId}-table`} columns={COLUMNS} rows={rows} states={{ empty }} />
</WarningModal>
)}
<DataViewToolbar ouiaId={`${ouiaId}-footer-toolbar`} pagination={paginationComponent} />
</DataView>
<DataView ouiaId={ouiaId} selection={selection} activeState={activeState}>
<DataViewToolbar
ouiaId={`${ouiaId}-header-toolbar`}
bulkSelect={
<BulkSelect
isDataPaginated
pageCount={groups.length}
selectedCount={selected.length}
totalCount={totalCount}
pageSelected={pageSelected}
pagePartiallySelected={pagePartiallySelected}
onSelect={handleBulkSelect}
/>
}
actions={
<ResponsiveActions breakpoint="lg" ouiaId={`${ouiaId}-actions-dropdown`}>
<ResponsiveAction
isDisabled={selected.length === 0 || selected.some(isRowSystemOrPlatformDefault)}
onClick={() => console.log('EDIT USER GROUP')}
>
{intl.formatMessage(messages.usersAndUserGroupsEditUserGroup)}
</ResponsiveAction>
<ResponsiveAction
isDisabled={selected.length === 0 || selected.some(isRowSystemOrPlatformDefault)}
onClick={() => {
handleDeleteModalToggle(groups.filter((group) => selected.some((selectedRow: DataViewTrObject) => selectedRow.id === group.uuid)));
}}
>
{intl.formatMessage(messages.usersAndUserGroupsDeleteUserGroup)}
</ResponsiveAction>
</ResponsiveActions>
}
pagination={React.cloneElement(paginationComponent, { isCompact: true })}
/>
{isLoading ? (
<SkeletonTable rowsCount={10} columns={COLUMNS} variant={TableVariant.compact} />
) : (
<DataViewTable variant="compact" aria-label="Users Table" ouiaId={`${ouiaId}-table`} columns={COLUMNS} rows={rows} states={{ empty }} />
)}
<DataViewToolbar ouiaId={`${ouiaId}-footer-toolbar`} pagination={paginationComponent} />
</DataView>
</Fragment>
);
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@ import UserGroupsTable from './UserGroupsTable';
import { useLocation, useNavigate } from 'react-router-dom';
import AddUserGroupModal from './AddUserGroupModal';
import { User } from '../../redux/reducers/user-reducer';
import UserDetailsDrawer from './UserDetailsDrawer';
import { Group } from '../../redux/reducers/group-reducer';
import GroupDetailsDrawer from './GroupDetailsDrawer';
import { DataViewEventsProvider } from '@patternfly/react-data-view';
import UserDetailsDrawer from './UserDetailsDrawer';

const TAB_NAMES = ['users', 'user-groups'];

Expand Down

0 comments on commit ed28740

Please sign in to comment.