Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: create delete user groups modal #1703

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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"]').click();
cy.get('[data-ouia-component-id="iam-user-groups-table-actions-dropdown"] 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 @@ -2214,6 +2214,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
136 changes: 109 additions & 27 deletions src/smart-components/access-management/UserGroupsTable.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import React, { useEffect, useCallback, useMemo } from 'react';
import React, { useEffect, useCallback, useMemo, 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 { Pagination, Tooltip } from '@patternfly/react-core';
import { ButtonVariant, Pagination, Tooltip } from '@patternfly/react-core';
import { ActionsColumn } 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 { useIntl } from 'react-intl';
import { FormattedMessage, useIntl } from 'react-intl';
import messages from '../../Messages';
import { Group } from '../../redux/reducers/group-reducer';
import { EventTypes, useDataViewEventsContext } from '@patternfly/react-data-view';
import { WarningModal } from '@patternfly/react-component-groups';

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

Expand Down Expand Up @@ -44,14 +45,16 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
onChange,
focusedGroup,
}) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false);
const [currentGroups, setCurrentGroups] = React.useState<Group[]>([]);
const dispatch = useDispatch();
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 } = useSelector((state: RBACStore) => ({
groups: state.groupReducer?.groups?.data || [],
Expand Down Expand Up @@ -135,7 +138,21 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
group.workspaces || '?', // not currently in API
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}
/>
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the group has either system=true or platform_default=true flag enabled we can't show the kebab menu at all. These groups can't be changed or removed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image
Actions are now disabled for system and platform_default rows, both individually and when one is included within a bulk selection.

props: { isActionCell: true },
},
],
Expand All @@ -150,6 +167,17 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
const pageSelected = rows.length > 0 && rows.every(isSelected);
const pagePartiallySelected = !pageSelected && rows.some(isSelected);

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
perPageOptions={PER_PAGE_OPTIONS}
Expand All @@ -162,25 +190,79 @@ const UserGroupsTable: React.FunctionComponent<UserGroupsTableProps> = ({
);

return (
<DataView ouiaId={ouiaId} selection={selection}>
<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={
<FormattedMessage
{...messages.deleteUserGroupModalTitle}
values={{
count: currentGroups.length,
plural: currentGroups.length > 1 ? intl.formatMessage(messages.groups) : intl.formatMessage(messages.group),
}}
/>
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like title props can only be string. You already have a message preconfigured to work with plural values

Suggested change
title={
<FormattedMessage
{...messages.deleteUserGroupModalTitle}
values={{
count: currentGroups.length,
plural: currentGroups.length > 1 ? intl.formatMessage(messages.groups) : intl.formatMessage(messages.group),
}}
/>
}
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 })}
/>
<DataViewTable variant="compact" aria-label="Users Table" ouiaId={`${ouiaId}-table`} columns={COLUMNS} rows={rows} />
<DataViewToolbar ouiaId={`${ouiaId}-footer-toolbar`} pagination={paginationComponent} />
</DataView>
</WarningModal>
)}
<DataView ouiaId={ouiaId} selection={selection}>
<DataViewToolbar
ouiaId={`${ouiaId}-header-toolbar`}
bulkSelect={
<BulkSelect
isDataPaginated
pageCount={groups.length}
selectedCount={selected.length}
totalCount={totalCount}
pageSelected={pageSelected}
pagePartiallySelected={pagePartiallySelected}
onSelect={handleBulkSelect}
/>
}
actions={
<div data-ouia-component-id={`${ouiaId}-actions-dropdown`}>
<ActionsColumn
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's use newer functionality from DataView https://react-data-view-pr-data-view-131.surge.sh/extensions/data-view/components#actions-example this would allow us to keep actions button visible all the times.

Copy link
Contributor Author

@CodyWMitchell CodyWMitchell Nov 15, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karelhala The <ResponsiveActions> component doesn't currently support disabling actions through any props like isDisabled or disabled. I'll switch to using it once that functionality is added. We need that here in the case that no rows are selected or any rows are system or platform_default (see #1703 (comment)).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All other feedback should be resolved ✅

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can disable each action by setting isDisabled on ResponsiveAction

<ResponsiveActions breakpoint="lg">
    <ResponsiveAction isPersistent isDisabled>
        Persistent Action
    </ResponsiveAction>
    <ResponsiveAction isDisabled isPinned variant='secondary'>
        Pinned Action
    </ResponsiveAction>
    <ResponsiveAction>
        Overflow Action
    </ResponsiveAction>
  </ResponsiveActions>

Copy link
Contributor Author

@CodyWMitchell CodyWMitchell Nov 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@karelhala It's working when the ResponsiveAction is marked isPersistent, but for actions within the dropdown (not isPersistent or isPinned), isDisabled isn't disabling them.
image
For example, both actions in the screenshot are isDisabled={true}, but only the Edit button is being disabled.

items={[
{
title: intl.formatMessage(messages.usersAndUserGroupsEditUserGroup),
onClick: () => console.log('EDIT USER GROUP'),
},
{
title: intl.formatMessage(messages.usersAndUserGroupsDeleteUserGroup),
onClick: () => {
handleDeleteModalToggle(groups.filter((group) => selected.some((selectedGroup) => selectedGroup.id === group.uuid)));
},
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

selectedGroup has implicit any type

Suggested change
onClick: () => {
handleDeleteModalToggle(groups.filter((group) => selected.some((selectedGroup) => selectedGroup.id === group.uuid)));
},
onClick: () => {
handleDeleteModalToggle(
groups.filter((group) => selected.some((selectedGroup: { id: string }) => selectedGroup.id === group.uuid))
);
},

},
]}
isDisabled={selected.length === 0}
/>
</div>
}
pagination={React.cloneElement(paginationComponent, { isCompact: true })}
/>
<DataViewTable variant="compact" aria-label="Users Table" ouiaId={`${ouiaId}-table`} columns={COLUMNS} rows={rows} />
<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