diff --git a/cypress/e2e/users-and-user-groups.cy.ts b/cypress/e2e/users-and-user-groups.cy.ts index 91eabe5cb..27bab2acc 100644 --- a/cypress/e2e/users-and-user-groups.cy.ts +++ b/cypress/e2e/users-and-user-groups.cy.ts @@ -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(); + }); }); diff --git a/src/Messages.js b/src/Messages.js index a3b9d94cd..de2005a1c 100644 --- a/src/Messages.js +++ b/src/Messages.js @@ -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 {name} user group} other {{count} user groups}} will impact user access configuration.', + }, addToUserGroup: { id: 'addToUserGroup', description: 'Action column option to add user to group', diff --git a/src/redux/reducers/group-reducer.ts b/src/redux/reducers/group-reducer.ts index 42170eddd..118fdfe72 100644 --- a/src/redux/reducers/group-reducer.ts +++ b/src/redux/reducers/group-reducer.ts @@ -26,6 +26,7 @@ export interface Group { modified?: string; admin_default?: boolean; platform_default?: boolean; + system?: boolean; } export interface GroupStore { diff --git a/src/smart-components/access-management/UserGroupsTable.tsx b/src/smart-components/access-management/UserGroupsTable.tsx index a4a8651c8..683f6a4e9 100644 --- a/src/smart-components/access-management/UserGroupsTable.tsx +++ b/src/smart-components/access-management/UserGroupsTable.tsx @@ -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']; @@ -46,15 +46,17 @@ const UserGroupsTable: React.FunctionComponent = ({ onChange, focusedGroup, }) => { + const [isDeleteModalOpen, setIsDeleteModalOpen] = React.useState(false); + const [currentGroups, setCurrentGroups] = React.useState([]); const dispatch = useDispatch(); const [activeState, setActiveState] = useState(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 || [], @@ -130,7 +132,7 @@ const UserGroupsTable: React.FunctionComponent = ({ (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, @@ -142,12 +144,27 @@ const UserGroupsTable: React.FunctionComponent = ({
{intl.formatMessage(messages['usersAndUserGroupsNoDescription'])}
), 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: , + cell: ( + console.log('EDIT USER GROUP'), + }, + { + title: intl.formatMessage(messages['usersAndUserGroupsDeleteUserGroup']), + onClick: () => handleDeleteModalToggle([group]), + }, + ]} + rowData={group} + isDisabled={group.platform_default || group.system} + /> + ), props: { isActionCell: true }, }, ], @@ -161,6 +178,21 @@ const UserGroupsTable: React.FunctionComponent = ({ 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 = ( = ({ ); return ( - - + {isDeleteModalOpen && ( + setIsDeleteModalOpen(false)} + onConfirm={() => { + handleDeleteGroups(currentGroups); + }} + > + {text}, + 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 ? ( - - ) : ( - + )} - - + + + } + actions={ + + console.log('EDIT USER GROUP')} + > + {intl.formatMessage(messages.usersAndUserGroupsEditUserGroup)} + + { + handleDeleteModalToggle(groups.filter((group) => selected.some((selectedRow: DataViewTrObject) => selectedRow.id === group.uuid))); + }} + > + {intl.formatMessage(messages.usersAndUserGroupsDeleteUserGroup)} + + + } + pagination={React.cloneElement(paginationComponent, { isCompact: true })} + /> + {isLoading ? ( + + ) : ( + + )} + + + ); }; diff --git a/src/smart-components/access-management/users-and-user-groups.tsx b/src/smart-components/access-management/users-and-user-groups.tsx index d510b6868..08ff15542 100644 --- a/src/smart-components/access-management/users-and-user-groups.tsx +++ b/src/smart-components/access-management/users-and-user-groups.tsx @@ -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'];