Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
88 changes: 88 additions & 0 deletions src/authz-module/audit-user/CustomCells.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import ViewMoreLink from '@src/authz-module/components/ViewMoreLink';
import {
Delete, ExpandMore, Info,
} from '@openedx/paragon/icons';
import {
Icon,
IconButton, OverlayTrigger, Tooltip,
} from '@openedx/paragon';
import { Role, TableCellValue, UserRole } from 'types';
import { ADMIN_ROLES, DJANGO_ROLES } from 'authz-module/constants';
import messages from './messages';
import { getPermissionsCountByRole } from './utils';

type CellProps = TableCellValue<UserRole>;
type ActionsCellProps = CellProps & {
onClickDeleteButton: (role: Role) => void;
};

export const ViewAllPermissionsCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
return (
<ViewMoreLink
label={formatMessage(messages['authz.user.table.view_all_permissions.link.text'])}
// TODO: Implement view more functionality
onClick={() => console.log('View more clicked for row:', row)}

Check warning on line 26 in src/authz-module/audit-user/CustomCells.tsx

View workflow job for this annotation

GitHub Actions / test

Unexpected console statement
iconSrc={ExpandMore}
/>
);
};

export const ActionsCell = ({ row, onClickDeleteButton }: ActionsCellProps) => {
const { formatMessage } = useIntl();
const { role } = row.original;
const handleDelete = () => {
const roleToDelete = {
name: role,
scope: row.original.scope,
} as Role;
onClickDeleteButton(roleToDelete);
};

if (DJANGO_ROLES.includes(role)) {
return (
<OverlayTrigger
placement="left"
overlay={(
<Tooltip variant="light" id="tooltip-left">
{formatMessage(messages['authz.user.table.delete.action.djangorole.tooltip'])}
</Tooltip>
)}
>
<Icon
className="mx-2 pl-1"
src={Info}
/>
</OverlayTrigger>
);
}

if (ADMIN_ROLES.includes(role)) {
return (
<IconButton
// @ts-ignore
disabled
isActive={false}
variant="light"
alt={formatMessage(messages['authz.user.table.delete.action.alt'])}
src={Delete}
/>
);
}

return (
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
);
};

export const PermissionsCell = ({ row }: CellProps) => {
const { formatMessage } = useIntl();
// TODO handle permissions length per role
const count = getPermissionsCountByRole(row.original.role);
return (
<span>
{formatMessage(messages['authz.user.table.permissions.available.count'], { count })}
</span>
);
};
290 changes: 290 additions & 0 deletions src/authz-module/audit-user/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,290 @@
import React, { useMemo, useState } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import debounce from 'lodash.debounce';
import {
Container, DataTable, TableFooter,
} from '@openedx/paragon';
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
import { useNavigate, useParams } from 'react-router-dom';
import { useUserAccount } from '@src/data/hooks';
import baseMessages from '@src/authz-module/messages';
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
import { RoleCell } from '@src/authz-module/components/TableCells';
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
import { useRevokeUserRoles, useUserAssignedRoles } from '@src/authz-module/data/hooks';
import { Role } from 'types';
import { useToastManager } from 'authz-module/libraries-manager/ToastManagerContext';
import messages from './messages';
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
import ConfirmDeletionModal from '../components/ConfirmDeletionModal';

const dummyData = [
{
role: 'Super Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Super Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Global Staff',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Course Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Course Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Course Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
{
role: 'Course Admin',
organization: 'edX',
scope: 'Course: Demo Course',
permissions: ['View', 'Edit', 'Delete'],
},
{
role: 'Course Auditor',
organization: 'edX',
scope: 'Course: Demo Course 2',
permissions: ['View', 'Edit'],
},
];

const AuditUserPage = () => {
const { formatMessage } = useIntl();
const { username } = useParams();
const navigate = useNavigate();
const [roleToDelete, setRoleToDelete] = useState<Role | null>(null);
const [showConfirmDeletionModal, setShowConfirmDeletionModal] = useState(false);
const {
showToast, showErrorToast, Bold, Br,
} = useToastManager();
const { mutate: revokeUserRoles, isPending: isRevokingUserRole } = useRevokeUserRoles();
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
const { querySettings, handleTableFetch } = useQuerySettings();
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
const { data: _userAssignedRoles } = useUserAssignedRoles(username ?? '', querySettings);

Check failure on line 122 in src/authz-module/audit-user/index.tsx

View workflow job for this annotation

GitHub Actions / test

'_userAssignedRoles' is assigned a value but never used

Check failure on line 122 in src/authz-module/audit-user/index.tsx

View workflow job for this annotation

GitHub Actions / test

Variable name `_userAssignedRoles` must match one of the following formats: camelCase, PascalCase, UPPER_CASE
const authzHomePath = '/authz';
if (!user && !isLoadingUser) {
navigate(authzHomePath);
}
const navLinks = [
{
label: formatMessage(baseMessages['authz.management.home.nav.link']),
to: authzHomePath,
},
];

const columns = [
{
Header: formatMessage(messages['authz.user.table.role.column.header']),
accessor: 'role',
Cell: RoleCell,
},
{
Header: formatMessage(messages['authz.user.table.organization.column.header']),
accessor: 'organization',
},
{
Header: formatMessage(messages['authz.user.table.scope.column.header']),
accessor: 'scope',
disableFilters: true,
},
{
Header: formatMessage(messages['authz.user.table.permissions.column.header']),
Cell: PermissionsCell,
disableFilters: true,
disableSortBy: true,
},
];
const pageCount = dummyData?.length ? Math.ceil(dummyData.length / TABLE_DEFAULT_PAGE_SIZE) : 1;

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

const handleShowConfirmDeletionModal = (role: Role) => {
if (isRevokingUserRole) { return; }

setRoleToDelete(role);
setShowConfirmDeletionModal(true);
};

const handleCloseConfirmDeletionModal = () => {
setRoleToDelete(null);
setShowConfirmDeletionModal(false);
};

const handleRevokeUserRole = () => {
if (!user || !roleToDelete) { return; }

const data = {
users: user.username,
role: roleToDelete.role,
scope: roleToDelete.scope,
};

const runRevokeRole = (variables = { data }) => {
revokeUserRoles(variables, {
onSuccess: (response) => {
const { errors } = response;

if (errors.length) {
showToast({
type: 'error',
message: formatMessage(
messages['library.authz.team.toast.default.error.message'],
{ Bold, Br },
),
});
return;
}

const remainingRolesCount = dummyData.length - 1;
showToast({
message: formatMessage(
messages['library.authz.team.remove.user.toast.success.description'],
{
role: roleToDelete.name,
rolesCount: remainingRolesCount,
},
),
type: 'success',
});
},
onError: (error, retryVariables) => {
showErrorToast(error, () => runRevokeRole(retryVariables));
},
});
};

handleCloseConfirmDeletionModal();
runRevokeRole();
};

// TODO:
// eslint-disable-next-line func-names, react/no-unstable-nested-components
const createActionsCell = (extraProps) => function (cellProps) {
return <ActionsCell {...cellProps} {...extraProps} />;
};

const additionalColumns = [
{
id: 'view_permissions',
Header: '',
Cell: ViewAllPermissionsCell,
},
{
id: 'action',
Header: formatMessage(messages['authz.user.table.action.column.header']),
Cell: createActionsCell({ onClickDeleteButton: handleShowConfirmDeletionModal }),
},
];

return (
<div className="authz-module">
<ConfirmDeletionModal
isOpen={showConfirmDeletionModal}
close={handleCloseConfirmDeletionModal}
onSave={handleRevokeUserRole}
isDeleting={isRevokingUserRole}
context={{
userName: user?.username || '',
scope: roleToDelete?.scope || '',
role: roleToDelete?.name || '',
rolesCount: dummyData.length,
}}
/>
<AuthZLayout
context={{
id: '',
org: '',
title: '',
}}
navLinks={navLinks}
activeLabel={username || ''}
pageTitle={user?.username || ''}
pageSubtitle={user?.email || ''}
actions={
[
<AddRoleButton presetUsername={user?.username} key="add-role-button" />,
]
}
>
<Container className="bg-light-200 p-5">
<DataTable
isPaginated
manualPagination
data={dummyData}
fetchData={fetchData}
itemCount={dummyData?.length || 0}
pageCount={pageCount}
initialState={{ pageSize: TABLE_DEFAULT_PAGE_SIZE }}
additionalColumns={additionalColumns}
columns={columns}
>
<DataTable.Table />
<TableFooter />
</DataTable>

</Container>
</AuthZLayout>
</div>
);
};

export default AuditUserPage;
Loading
Loading