Skip to content

Commit dd14b5c

Browse files
feat: delete role functionality added to user table
1 parent 01d7205 commit dd14b5c

7 files changed

Lines changed: 278 additions & 22 deletions

File tree

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

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,20 @@
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, AdminPanelSettings, DeleteForever,
5+
} from '@openedx/paragon/icons';
6+
import {
7+
IconButton, OverlayTrigger, Tooltip,
8+
} from '@openedx/paragon';
9+
import { Role, TableCellValue, UserRole } from 'types';
10+
import { ADMIN_ROLES, DJANGO_ROLES } from 'authz-module/constants';
611
import messages from './messages';
712
import { getPermissionsCountByRole } from './utils';
813

914
type CellProps = TableCellValue<UserRole>;
15+
type ActionsCellProps = CellProps & {
16+
onClickDeleteButton: (role: Role) => void;
17+
};
1018

1119
export const ViewAllPermissionsCell = ({ row }: CellProps) => {
1220
const { formatMessage } = useIntl();
@@ -20,13 +28,54 @@ export const ViewAllPermissionsCell = ({ row }: CellProps) => {
2028
);
2129
};
2230

23-
export const ActionsCell = ({ row }: CellProps) => {
31+
export const ActionsCell = ({ row, onClickDeleteButton }: ActionsCellProps) => {
2432
const { formatMessage } = useIntl();
33+
const { role } = row.original;
2534
const handleDelete = () => {
26-
// TODO: Implement delete functionality
27-
console.log('Delete clicked for row:', row);
35+
const roleToDelete = {
36+
name: role,
37+
scope: row.original.scope,
38+
} as Role;
39+
onClickDeleteButton(roleToDelete);
2840
};
2941

42+
// TODO: change icon when design is ready for non-deletable roles,
43+
// currently using the AdminPanelSettings icon with a tooltip to indicate why it's not deletable
44+
if (DJANGO_ROLES.includes(role)) {
45+
return (
46+
<OverlayTrigger
47+
placement="left"
48+
overlay={(
49+
<Tooltip variant="light" id="tooltip-left">
50+
{formatMessage(messages['authz.user.table.delete.action.djangorole.tooltip'])}
51+
</Tooltip>
52+
)}
53+
>
54+
<IconButton
55+
isActive={false}
56+
variant="danger"
57+
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
58+
src={AdminPanelSettings}
59+
/>
60+
</OverlayTrigger>
61+
);
62+
}
63+
64+
// TODO: change icon when design is ready for admin roles that are not deletable,
65+
// currently using the DeleteForever icon with a tooltip to indicate why it's not deletable
66+
if (ADMIN_ROLES.includes(role)) {
67+
return (
68+
<IconButton
69+
// @ts-ignore
70+
disabled
71+
isActive={false}
72+
variant="light"
73+
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
74+
src={DeleteForever}
75+
/>
76+
);
77+
}
78+
3079
return (
3180
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
3281
);

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

Lines changed: 104 additions & 16 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 {
@@ -11,13 +11,16 @@ import { useUserAccount } from '@src/data/hooks';
1111
import baseMessages from '@src/authz-module/messages';
1212
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
1313
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
14-
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
14+
import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data/hooks';
15+
import { Role } from 'types';
16+
import { useToastManager } from 'authz-module/libraries-manager/ToastManagerContext';
1517
import messages from './messages';
1618
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
19+
import ConfirmDeletionModal from '../components/ConfirmDeletionModal';
1720

1821
const dummyData = [
1922
{
20-
role: 'Course Admin',
23+
role: 'Super Admin',
2124
organization: 'edX',
2225
scope: 'Course: Demo Course',
2326
permissions: ['View', 'Edit', 'Delete'],
@@ -29,7 +32,7 @@ const dummyData = [
2932
permissions: ['View', 'Edit'],
3033
},
3134
{
32-
role: 'Course Admin',
35+
role: 'Global Staff',
3336
organization: 'edX',
3437
scope: 'Course: Demo Course',
3538
permissions: ['View', 'Edit', 'Delete'],
@@ -106,6 +109,12 @@ const AuditUserPage = () => {
106109
const { formatMessage } = useIntl();
107110
const { username } = useParams();
108111
const navigate = useNavigate();
112+
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
113+
const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false);
114+
const {
115+
showToast, showErrorToast, Bold, Br,
116+
} = useToastManager();
117+
const { mutate: revokeUserRoles, isPending: isRevokingUserRole } = useRevokeUserRoles();
109118
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
110119
const { querySettings, handleTableFetch } = useQuerySettings();
111120
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
@@ -120,18 +129,7 @@ const AuditUserPage = () => {
120129
to: authzHomePath,
121130
},
122131
];
123-
const additionalColumns = [
124-
{
125-
id: 'view_permissions',
126-
Header: '',
127-
Cell: ViewAllPermissionsCell,
128-
},
129-
{
130-
id: 'action',
131-
Header: formatMessage(messages['authz.user.table.action.column.header']),
132-
Cell: ActionsCell,
133-
},
134-
];
132+
135133
const columns = [
136134
{
137135
Header: formatMessage(messages['authz.user.table.role.column.header']),
@@ -157,8 +155,98 @@ const AuditUserPage = () => {
157155

158156
const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]);
159157

158+
const handleShowConfirmDeletionModal = (role: Role) => {
159+
if (isRevokingUserRole) { return; }
160+
161+
setRoleToDelete(role);
162+
setShowConfirmDeletionModal(true);
163+
};
164+
165+
const handleCloseConfirmDeletionModal = () => {
166+
setRoleToDelete(null);
167+
setShowConfirmDeletionModal(false);
168+
};
169+
170+
const handleRevokeUserRole = () => {
171+
if (!user || !roleToDelete) { return; }
172+
173+
const data = {
174+
users: user.username,
175+
role: roleToDelete.role,
176+
scope: roleToDelete.scope,
177+
};
178+
179+
const runRevokeRole = (variables = { data }) => {
180+
revokeUserRoles(variables, {
181+
onSuccess: (response) => {
182+
const { errors } = response;
183+
184+
if (errors.length) {
185+
showToast({
186+
type: 'error',
187+
message: formatMessage(
188+
messages['library.authz.team.toast.default.error.message'],
189+
{ Bold, Br },
190+
),
191+
});
192+
return;
193+
}
194+
195+
const remainingRolesCount = dummyData.length - 1;
196+
showToast({
197+
message: formatMessage(
198+
messages['library.authz.team.remove.user.toast.success.description'],
199+
{
200+
role: roleToDelete.name,
201+
rolesCount: remainingRolesCount,
202+
},
203+
),
204+
type: 'success',
205+
});
206+
},
207+
onError: (error, retryVariables) => {
208+
showErrorToast(error, () => runRevokeRole(retryVariables));
209+
},
210+
});
211+
};
212+
213+
handleCloseConfirmDeletionModal();
214+
runRevokeRole();
215+
};
216+
217+
// TODO:
218+
// eslint-disable-next-line func-names, react/no-unstable-nested-components
219+
const createActionsCell = (extraProps) => function (cellProps) {
220+
return <ActionsCell {...cellProps} {...extraProps} />;
221+
};
222+
223+
const additionalColumns = [
224+
{
225+
id: 'view_permissions',
226+
Header: '',
227+
Cell: ViewAllPermissionsCell,
228+
},
229+
{
230+
id: 'action',
231+
Header: formatMessage(messages['authz.user.table.action.column.header']),
232+
Cell: createActionsCell({ onClickDeleteButton: handleShowConfirmDeletionModal }),
233+
},
234+
];
235+
160236
return (
161237
<div className="authz-module">
238+
<ConfirmDeletionModal
239+
isOpen={showConfirmDeletionModal}
240+
close={handleCloseConfirmDeletionModal}
241+
onSave={handleRevokeUserRole}
242+
isDeleting={isRevokingUserRole}
243+
context={{
244+
userName: user?.username || '',
245+
scope: roleToDelete?.scope || '',
246+
role: roleToDelete?.name || '',
247+
rolesCount: dummyData.length,
248+
}}
249+
/>
162250
<AuthZLayout
163251
context={{
164252
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
@@ -11,6 +11,41 @@ const messages = defineMessages({
1111
defaultMessage: 'Permission denied in {roleName} role',
1212
description: 'Label for denied status of a permission in the permissions table',
1313
},
14+
'authz.team.remove.user.modal.title': {
15+
id: 'authz.team.remove.user.modal.title',
16+
defaultMessage: 'Remove role?',
17+
description: 'AuthZ team management remove user modal title',
18+
},
19+
'authz.manage.cancel.button': {
20+
id: 'authz.manage.cancel.button',
21+
defaultMessage: 'Cancel',
22+
description: 'AuthZ cancel button title',
23+
},
24+
'authz.manage.remove.button': {
25+
id: 'authz.manage.remove.button',
26+
defaultMessage: 'Remove',
27+
description: 'AuthZ remove button title',
28+
},
29+
'authz.manage.removing.button': {
30+
id: 'authz.manage.removing.button',
31+
defaultMessage: 'Removing...',
32+
description: 'AuthZ removing button title',
33+
},
34+
'authz.team.remove.user.modal.body.1': {
35+
id: 'authz.team.remove.user.modal.body.1',
36+
defaultMessage: 'Are you sure you want to remove the {role} role from the user “{userName}” in the scope {scope}?',
37+
description: 'AuthZ team management remove user modal body',
38+
},
39+
'authz.team.remove.user.modal.body.2': {
40+
id: 'authz.team.remove.user.modal.body.2',
41+
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.",
42+
description: 'AuthZ team management remove user modal body',
43+
},
44+
'authz.team.remove.user.modal.body.3': {
45+
id: 'authz.team.remove.user.modal.body.3',
46+
defaultMessage: 'Are you sure you want to proceed?',
47+
description: 'AuthZ team management remove user modal body',
48+
},
1449
});
1550

1651
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/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)