Skip to content

Commit 5ae6534

Browse files
feat: delete role functionality added to user table
1 parent e314172 commit 5ae6534

8 files changed

Lines changed: 272 additions & 26 deletions

File tree

src/authz-module/audit-user/CustomCells.tsx

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,21 @@
11
import { useIntl } from '@edx/frontend-platform/i18n';
22
import ViewMoreLink from '@src/authz-module/components/ViewMoreLink';
3-
import { Delete, ExpandMore } from '@openedx/paragon/icons';
4-
import { IconButton } from '@openedx/paragon';
5-
import { TableCellValue, UserRole } from 'types';
3+
import {
4+
Delete, ExpandMore, Info,
5+
} from '@openedx/paragon/icons';
6+
import {
7+
Icon,
8+
IconButton, OverlayTrigger, Tooltip,
9+
} from '@openedx/paragon';
10+
import { Role, TableCellValue, UserRole } from 'types';
11+
import { ADMIN_ROLES, DJANGO_ROLES } from 'authz-module/constants';
612
import messages from './messages';
713
import { getPermissionsCountByRole } from './utils';
814

915
type CellProps = TableCellValue<UserRole>;
16+
type ActionsCellProps = CellProps & {
17+
onClickDeleteButton: (role: Role) => void;
18+
};
1019

1120
export const ViewAllPermissionsCell = ({ row }: CellProps) => {
1221
const { formatMessage } = useIntl();
@@ -20,13 +29,48 @@ export const ViewAllPermissionsCell = ({ row }: CellProps) => {
2029
);
2130
};
2231

23-
export const ActionsCell = ({ row }: CellProps) => {
32+
export const ActionsCell = ({ row, onClickDeleteButton }: ActionsCellProps) => {
2433
const { formatMessage } = useIntl();
34+
const { role } = row.original;
2535
const handleDelete = () => {
26-
// TODO: Implement delete functionality
27-
console.log('Delete clicked for row:', row);
36+
const roleToDelete = {
37+
name: role,
38+
scope: row.original.scope,
39+
} as Role;
40+
onClickDeleteButton(roleToDelete);
2841
};
2942

43+
if (DJANGO_ROLES.includes(role)) {
44+
return (
45+
<OverlayTrigger
46+
placement="left"
47+
overlay={(
48+
<Tooltip variant="light" id="tooltip-left">
49+
{formatMessage(messages['authz.user.table.delete.action.djangorole.tooltip'])}
50+
</Tooltip>
51+
)}
52+
>
53+
<Icon
54+
className="mx-2 pl-1"
55+
src={Info}
56+
/>
57+
</OverlayTrigger>
58+
);
59+
}
60+
61+
if (ADMIN_ROLES.includes(role)) {
62+
return (
63+
<IconButton
64+
// @ts-ignore
65+
disabled
66+
isActive={false}
67+
variant="light"
68+
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
69+
src={Delete}
70+
/>
71+
);
72+
}
73+
3074
return (
3175
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
3276
);

src/authz-module/audit-user/index.tsx

Lines changed: 103 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import React, { useMemo } from 'react';
1+
import React, { useMemo, useState } from 'react';
22
import { useIntl } from '@edx/frontend-platform/i18n';
33
import debounce from 'lodash.debounce';
44
import {
@@ -12,13 +12,16 @@ import baseMessages from '@src/authz-module/messages';
1212
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
1313
import { RoleCell } from '@src/authz-module/components/TableCells';
1414
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
15-
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
15+
import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data/hooks';
16+
import { Role } from 'types';
17+
import { useToastManager } from 'authz-module/libraries-manager/ToastManagerContext';
1618
import messages from './messages';
1719
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
20+
import ConfirmDeletionModal from '../components/ConfirmDeletionModal';
1821

1922
const dummyData = [
2023
{
21-
role: 'Course Admin',
24+
role: 'Super Admin',
2225
organization: 'edX',
2326
scope: 'Course: Demo Course',
2427
permissions: ['View', 'Edit', 'Delete'],
@@ -107,6 +110,12 @@ const AuditUserPage = () => {
107110
const { formatMessage } = useIntl();
108111
const { username } = useParams();
109112
const navigate = useNavigate();
113+
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
114+
const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false);
115+
const {
116+
showToast, showErrorToast, Bold, Br,
117+
} = useToastManager();
118+
const { mutate: revokeUserRoles, isPending: isRevokingUserRole } = useRevokeUserRoles();
110119
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
111120
const { querySettings, handleTableFetch } = useQuerySettings();
112121
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
@@ -121,18 +130,7 @@ const AuditUserPage = () => {
121130
to: authzHomePath,
122131
},
123132
];
124-
const additionalColumns = [
125-
{
126-
id: 'view_permissions',
127-
Header: '',
128-
Cell: ViewAllPermissionsCell,
129-
},
130-
{
131-
id: 'action',
132-
Header: formatMessage(messages['authz.user.table.action.column.header']),
133-
Cell: ActionsCell,
134-
},
135-
];
133+
136134
const columns = [
137135
{
138136
Header: formatMessage(messages['authz.user.table.role.column.header']),
@@ -159,8 +157,98 @@ const AuditUserPage = () => {
159157

160158
const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]);
161159

160+
const handleShowConfirmDeletionModal = (role: Role) => {
161+
if (isRevokingUserRole) { return; }
162+
163+
setRoleToDelete(role);
164+
setShowConfirmDeletionModal(true);
165+
};
166+
167+
const handleCloseConfirmDeletionModal = () => {
168+
setRoleToDelete(null);
169+
setShowConfirmDeletionModal(false);
170+
};
171+
172+
const handleRevokeUserRole = () => {
173+
if (!user || !roleToDelete) { return; }
174+
175+
const data = {
176+
users: user.username,
177+
role: roleToDelete.role,
178+
scope: roleToDelete.scope,
179+
};
180+
181+
const runRevokeRole = (variables = { data }) => {
182+
revokeUserRoles(variables, {
183+
onSuccess: (response) => {
184+
const { errors } = response;
185+
186+
if (errors.length) {
187+
showToast({
188+
type: 'error',
189+
message: formatMessage(
190+
messages['library.authz.team.toast.default.error.message'],
191+
{ Bold, Br },
192+
),
193+
});
194+
return;
195+
}
196+
197+
const remainingRolesCount = dummyData.length - 1;
198+
showToast({
199+
message: formatMessage(
200+
messages['library.authz.team.remove.user.toast.success.description'],
201+
{
202+
role: roleToDelete.name,
203+
rolesCount: remainingRolesCount,
204+
},
205+
),
206+
type: 'success',
207+
});
208+
},
209+
onError: (error, retryVariables) => {
210+
showErrorToast(error, () => runRevokeRole(retryVariables));
211+
},
212+
});
213+
};
214+
215+
handleCloseConfirmDeletionModal();
216+
runRevokeRole();
217+
};
218+
219+
// TODO:
220+
// eslint-disable-next-line func-names, react/no-unstable-nested-components
221+
const createActionsCell = (extraProps) => function (cellProps) {
222+
return <ActionsCell {...cellProps} {...extraProps} />;
223+
};
224+
225+
const additionalColumns = [
226+
{
227+
id: 'view_permissions',
228+
Header: '',
229+
Cell: ViewAllPermissionsCell,
230+
},
231+
{
232+
id: 'action',
233+
Header: formatMessage(messages['authz.user.table.action.column.header']),
234+
Cell: createActionsCell({ onClickDeleteButton: handleShowConfirmDeletionModal }),
235+
},
236+
];
237+
162238
return (
163239
<div className="authz-module">
240+
<ConfirmDeletionModal
241+
isOpen={showConfirmDeletionModal}
242+
close={handleCloseConfirmDeletionModal}
243+
onSave={handleRevokeUserRole}
244+
isDeleting={isRevokingUserRole}
245+
context={{
246+
userName: user?.username || '',
247+
scope: roleToDelete?.scope || '',
248+
role: roleToDelete?.name || '',
249+
rolesCount: dummyData.length,
250+
}}
251+
/>
164252
<AuthZLayout
165253
context={{
166254
id: '',

src/authz-module/audit-user/messages.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,11 @@ const messages = defineMessages(
4242
defaultMessage: '{count, plural, one {# permission available} other {# permissions available}}',
4343
description: 'Text showing the number of permissions available, with proper pluralization',
4444
},
45+
'authz.user.table.delete.action.djangorole.tooltip': {
46+
id: 'authz.user.table.delete.action.djangorole.tooltip',
47+
defaultMessage: 'You can’t remove this role here. Please go to Django Admin to manage it.',
48+
description: 'Tooltip for delete button when hovering over Django roles',
49+
},
4550
},
4651
);
4752

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import {
2+
ActionRow, AlertModal, Icon, ModalDialog, Stack,
3+
StatefulButton,
4+
} from '@openedx/paragon';
5+
import { useIntl } from '@edx/frontend-platform/i18n';
6+
7+
import { SpinnerSimple } from '@openedx/paragon/icons';
8+
import messages from './messages';
9+
10+
interface ConfirmDeletionModalProps {
11+
isOpen: boolean;
12+
close: () => void;
13+
onSave: () => void;
14+
isDeleting?: boolean;
15+
context: {
16+
userName: string;
17+
scope: string;
18+
role: string;
19+
rolesCount: number;
20+
}
21+
}
22+
23+
const ConfirmDeletionModal = ({
24+
isOpen, close, onSave, isDeleting, context,
25+
}: ConfirmDeletionModalProps) => {
26+
const intl = useIntl();
27+
return (
28+
<AlertModal
29+
title={intl.formatMessage(messages['authz.team.remove.user.modal.title'])}
30+
isOpen={isOpen}
31+
onClose={close}
32+
size="lg"
33+
footerNode={(
34+
<ActionRow>
35+
<ModalDialog.CloseButton variant="tertiary">
36+
{intl.formatMessage(messages['authz.manage.cancel.button'])}
37+
</ModalDialog.CloseButton>
38+
<StatefulButton
39+
className="px-4"
40+
variant="danger"
41+
labels={{
42+
default: intl.formatMessage(messages['authz.manage.remove.button']),
43+
pending: intl.formatMessage(messages['authz.manage.removing.button']),
44+
}}
45+
icons={{
46+
pending: <Icon src={SpinnerSimple} />,
47+
}}
48+
state={isDeleting ? 'pending' : 'default'}
49+
onClick={() => onSave()}
50+
disabledStates={['pending']}
51+
/>
52+
</ActionRow>
53+
)}
54+
isOverflowVisible={false}
55+
>
56+
<Stack gap={3}>
57+
<p>{intl.formatMessage(messages['authz.team.remove.user.modal.body.1'], {
58+
userName: context.userName,
59+
scope: context.scope,
60+
role: context.role,
61+
})}
62+
</p>
63+
{context.rolesCount === 1 && (
64+
<p>{intl.formatMessage(messages['authz.team.remove.user.modal.body.2'])}</p>
65+
)}
66+
<p>{intl.formatMessage(messages['authz.team.remove.user.modal.body.3'])}</p>
67+
</Stack>
68+
69+
</AlertModal>
70+
);
71+
};
72+
73+
export default ConfirmDeletionModal;

src/authz-module/components/messages.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,41 @@ const messages = defineMessages({
1616
defaultMessage: 'Showing {pageSize} of {itemCount} users.',
1717
description: 'Message displayed when the user reaches the applied filters limit',
1818
},
19+
'authz.team.remove.user.modal.title': {
20+
id: 'authz.team.remove.user.modal.title',
21+
defaultMessage: 'Remove role?',
22+
description: 'AuthZ team management remove user modal title',
23+
},
24+
'authz.manage.cancel.button': {
25+
id: 'authz.manage.cancel.button',
26+
defaultMessage: 'Cancel',
27+
description: 'AuthZ cancel button title',
28+
},
29+
'authz.manage.remove.button': {
30+
id: 'authz.manage.remove.button',
31+
defaultMessage: 'Remove',
32+
description: 'AuthZ remove button title',
33+
},
34+
'authz.manage.removing.button': {
35+
id: 'authz.manage.removing.button',
36+
defaultMessage: 'Removing...',
37+
description: 'AuthZ removing button title',
38+
},
39+
'authz.team.remove.user.modal.body.1': {
40+
id: 'authz.team.remove.user.modal.body.1',
41+
defaultMessage: 'Are you sure you want to remove the {role} role from the user “{userName}” in the scope {scope}?',
42+
description: 'AuthZ team management remove user modal body',
43+
},
44+
'authz.team.remove.user.modal.body.2': {
45+
id: 'authz.team.remove.user.modal.body.2',
46+
defaultMessage: "This is the user's only role in this scope. Removing it will revoke their access completely, and they will no longer appear in the scope's member list.",
47+
description: 'AuthZ team management remove user modal body',
48+
},
49+
'authz.team.remove.user.modal.body.3': {
50+
id: 'authz.team.remove.user.modal.body.3',
51+
defaultMessage: 'Are you sure you want to proceed?',
52+
description: 'AuthZ team management remove user modal body',
53+
},
1954
});
2055

2156
export default messages;

src/authz-module/constants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,3 +119,7 @@ export enum RoleOperationErrorStatus {
119119
}
120120

121121
export const TABLE_DEFAULT_PAGE_SIZE = 10;
122+
123+
export const DJANGO_ROLES = ['Super Admin', 'Global Staff'];
124+
125+
export const ADMIN_ROLES = ['Course Admin', 'Library Admin'];

src/authz-module/messages.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,6 @@ const messages = defineMessages(
77
defaultMessage: 'Roles and Permissions Management',
88
description: 'Text for the roles and permissions management home page title navigation link',
99
},
10-
'authz.management.specific.user.nav.link': {
11-
id: 'authz.management.specific.user.nav.link',
12-
defaultMessage: 'Specific User',
13-
description: 'Text for the specific user page navigation link',
14-
},
1510
'authz.management.assign.role.title': {
1611
id: 'authz.management.assign.role.title',
1712
defaultMessage: 'Assign Role',

src/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ export interface RoleMetadata {
2929
name: string;
3030
description: string;
3131
}
32+
// TODO: remove unnecessary fields when libraries gets removed
3233
export interface Role extends RoleMetadata {
34+
scope: string;
3335
userCount: number;
3436
permissions: string[];
3537
}

0 commit comments

Comments
 (0)