Skip to content

Commit 2151f75

Browse files
feat: roles table for audit user page
1 parent 568828d commit 2151f75

19 files changed

Lines changed: 599 additions & 4 deletions
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
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';
6+
import messages from './messages';
7+
import { getPermissionsCountByRole } from './utils';
8+
9+
type CellProps = TableCellValue<UserRole>;
10+
11+
export const ViewAllPermissionsCell = ({ row }: CellProps) => {
12+
const { formatMessage } = useIntl();
13+
return (
14+
<ViewMoreLink
15+
label={formatMessage(messages['authz.user.table.view_all_permissions.link.text'])}
16+
// TODO: Implement view more functionality
17+
onClick={() => console.log('View more clicked for row:', row)}
18+
iconSrc={ExpandMore}
19+
/>
20+
);
21+
};
22+
23+
export const ActionsCell = ({ row }: CellProps) => {
24+
const { formatMessage } = useIntl();
25+
const handleDelete = () => {
26+
// TODO: Implement delete functionality
27+
console.log('Delete clicked for row:', row);
28+
};
29+
30+
return (
31+
<IconButton variant="danger" onClick={handleDelete} alt={formatMessage(messages['authz.user.table.delete.action.alt'])} src={Delete} />
32+
);
33+
};
34+
35+
export const PermissionsCell = ({ row }: CellProps) => {
36+
const { formatMessage } = useIntl();
37+
// TODO handle permissions length per role
38+
const count = getPermissionsCountByRole(row.original.role);
39+
return (
40+
<span>
41+
{formatMessage(messages['authz.user.table.permissions.available.count'], { count })}
42+
</span>
43+
);
44+
};
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
import { useMemo } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import debounce from 'lodash.debounce';
4+
import {
5+
Container, DataTable,
6+
} from '@openedx/paragon';
7+
import TableFooter from '@src/authz-module/components/TableFooter/TableFooter';
8+
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
9+
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
10+
import { useNavigate, useParams } from 'react-router-dom';
11+
import { useUserAccount } from '@src/data/hooks';
12+
import baseMessages from '@src/authz-module/messages';
13+
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
14+
import { RoleCell } from '@src/authz-module/components/TableCells';
15+
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
16+
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
17+
import messages from './messages';
18+
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
19+
20+
const dummyData = [
21+
{
22+
role: 'Course Admin',
23+
organization: 'edX',
24+
scope: 'Course: Demo Course',
25+
permissions: ['View', 'Edit', 'Delete'],
26+
},
27+
{
28+
role: 'Course Auditor',
29+
organization: 'edX',
30+
scope: 'Course: Demo Course 2',
31+
permissions: ['View', 'Edit'],
32+
},
33+
{
34+
role: 'Super Admin',
35+
organization: 'edX',
36+
scope: 'Course: Demo Course',
37+
permissions: ['View', 'Edit', 'Delete'],
38+
},
39+
{
40+
role: 'Course Auditor',
41+
organization: 'edX',
42+
scope: 'Course: Demo Course 2',
43+
permissions: ['View', 'Edit'],
44+
},
45+
{
46+
role: 'Global Staff',
47+
organization: 'edX',
48+
scope: 'Course: Demo Course',
49+
permissions: ['View', 'Edit', 'Delete'],
50+
},
51+
{
52+
role: 'Course Auditor',
53+
organization: 'edX',
54+
scope: 'Course: Demo Course 2',
55+
permissions: ['View', 'Edit'],
56+
},
57+
{
58+
role: 'Course Admin',
59+
organization: 'edX',
60+
scope: 'Course: Demo Course',
61+
permissions: ['View', 'Edit', 'Delete'],
62+
},
63+
{
64+
role: 'Course Auditor',
65+
organization: 'edX',
66+
scope: 'Course: Demo Course 2',
67+
permissions: ['View', 'Edit'],
68+
},
69+
{
70+
role: 'Course Admin',
71+
organization: 'edX',
72+
scope: 'Course: Demo Course',
73+
permissions: ['View', 'Edit', 'Delete'],
74+
},
75+
{
76+
role: 'Course Auditor',
77+
organization: 'edX',
78+
scope: 'Course: Demo Course 2',
79+
permissions: ['View', 'Edit'],
80+
},
81+
{
82+
role: 'Course Admin',
83+
organization: 'edX',
84+
scope: 'Course: Demo Course',
85+
permissions: ['View', 'Edit', 'Delete'],
86+
},
87+
{
88+
role: 'Course Auditor',
89+
organization: 'edX',
90+
scope: 'Course: Demo Course 2',
91+
permissions: ['View', 'Edit'],
92+
},
93+
{
94+
role: 'Course Admin',
95+
organization: 'edX',
96+
scope: 'Course: Demo Course',
97+
permissions: ['View', 'Edit', 'Delete'],
98+
},
99+
{
100+
role: 'Course Auditor',
101+
organization: 'edX',
102+
scope: 'Course: Demo Course 2',
103+
permissions: ['View', 'Edit'],
104+
},
105+
];
106+
107+
const AuditUserPage = () => {
108+
const { formatMessage } = useIntl();
109+
const { username } = useParams();
110+
const navigate = useNavigate();
111+
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
112+
const { querySettings, handleTableFetch } = useQuerySettings();
113+
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
114+
const { data: _userAssignedRoles } = useUserAssignedRoles(username ?? '', querySettings);
115+
const authzHomePath = '/authz';
116+
if (!user && !isLoadingUser) {
117+
navigate(authzHomePath);
118+
}
119+
const navLinks = [
120+
{
121+
label: formatMessage(baseMessages['authz.management.home.nav.link']),
122+
to: authzHomePath,
123+
},
124+
];
125+
const additionalColumns = [
126+
{
127+
id: 'view_permissions',
128+
Header: '',
129+
Cell: ViewAllPermissionsCell,
130+
},
131+
{
132+
id: 'action',
133+
Header: formatMessage(messages['authz.user.table.action.column.header']),
134+
Cell: ActionsCell,
135+
},
136+
];
137+
const columns = [
138+
{
139+
Header: formatMessage(messages['authz.user.table.role.column.header']),
140+
accessor: 'role',
141+
Cell: RoleCell,
142+
},
143+
{
144+
Header: formatMessage(messages['authz.user.table.organization.column.header']),
145+
accessor: 'organization',
146+
},
147+
{
148+
Header: formatMessage(messages['authz.user.table.scope.column.header']),
149+
accessor: 'scope',
150+
disableFilters: true,
151+
},
152+
{
153+
Header: formatMessage(messages['authz.user.table.permissions.column.header']),
154+
Cell: PermissionsCell,
155+
disableFilters: true,
156+
disableSortBy: true,
157+
},
158+
];
159+
const pageCount = dummyData?.length ? Math.ceil(dummyData.length / TABLE_DEFAULT_PAGE_SIZE) : 1;
160+
161+
const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]);
162+
163+
return (
164+
<div className="authz-module">
165+
<AuthZLayout
166+
context={{
167+
id: '',
168+
org: '',
169+
title: '',
170+
}}
171+
navLinks={navLinks}
172+
activeLabel={username || ''}
173+
pageTitle={user?.username || ''}
174+
pageSubtitle={user?.email || ''}
175+
actions={
176+
[
177+
<AddRoleButton presetUsername={user?.username} key="add-role-button" />,
178+
]
179+
}
180+
>
181+
<Container className="bg-light-200 p-5">
182+
<DataTable
183+
isPaginated
184+
manualPagination
185+
data={dummyData}
186+
fetchData={fetchData}
187+
itemCount={dummyData?.length || 0}
188+
pageCount={pageCount}
189+
initialState={{ pageSize: TABLE_DEFAULT_PAGE_SIZE }}
190+
additionalColumns={additionalColumns}
191+
columns={columns}
192+
>
193+
<DataTable.Table />
194+
<TableFooter />
195+
</DataTable>
196+
197+
</Container>
198+
</AuthZLayout>
199+
</div>
200+
);
201+
};
202+
203+
export default AuditUserPage;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { defineMessages } from '@edx/frontend-platform/i18n';
2+
3+
const messages = defineMessages(
4+
{
5+
'authz.user.table.role.column.header': {
6+
id: 'authz.user.table.role.column.header',
7+
defaultMessage: 'Role',
8+
description: 'Header for the role column in the user table',
9+
},
10+
'authz.user.table.organization.column.header': {
11+
id: 'authz.user.table.organization.column.header',
12+
defaultMessage: 'Organization',
13+
description: 'Header for the organization column in the user table',
14+
},
15+
'authz.user.table.scope.column.header': {
16+
id: 'authz.user.table.scope.column.header',
17+
defaultMessage: 'Scope',
18+
description: 'Header for the scope column in the user table',
19+
},
20+
'authz.user.table.permissions.column.header': {
21+
id: 'authz.user.table.permissions.column.header',
22+
defaultMessage: 'Permissions',
23+
description: 'Header for the permissions column in the user table',
24+
},
25+
'authz.user.table.action.column.header': {
26+
id: 'authz.user.table.action.column.header',
27+
defaultMessage: 'Actions',
28+
description: 'Header for the actions column in the user table',
29+
},
30+
'authz.user.table.view_all_permissions.link.text': {
31+
id: 'authz.user.table.view_all_permissions.link.text',
32+
defaultMessage: 'View all permissions',
33+
description: 'Text for the link to view all permissions in the user table',
34+
},
35+
'authz.user.table.delete.action.alt': {
36+
id: 'authz.user.table.delete.action.alt',
37+
defaultMessage: 'Delete role action',
38+
description: 'Alt description for delete button',
39+
},
40+
'authz.user.table.permissions.available.count': {
41+
id: 'authz.user.table.permissions.available.count',
42+
defaultMessage: '{count, plural, one {# permission available} other {# permissions available}}',
43+
description: 'Text showing the number of permissions available, with proper pluralization',
44+
},
45+
},
46+
);
47+
48+
export default messages;
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
export const getPermissionsCountByRole = (role: string) => {
2+
/*
3+
const roleData = permissionsList.find(item => item.role === role);
4+
return roleData ? roleData.permissions.length : 0;
5+
*/
6+
return Math.floor(Math.random() * 50);
7+
};
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import React from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { Button } from '@openedx/paragon';
4+
import { Plus } from '@openedx/paragon/icons';
5+
6+
import baseMessages from '@src/authz-module/messages';
7+
import { useNavigate } from 'react-router-dom';
8+
9+
interface AddRoleButtonProps {
10+
presetUsername?: string;
11+
}
12+
13+
const AddRoleButton = ({ presetUsername }: AddRoleButtonProps) => {
14+
const intl = useIntl();
15+
const navigate = useNavigate();
16+
17+
const handleClick = () => {
18+
const assignRolePath = `/authz/assign-role${presetUsername ? `?username=${presetUsername}` : ''}`;
19+
navigate(assignRolePath);
20+
};
21+
22+
return (
23+
<Button
24+
iconBefore={Plus}
25+
onClick={handleClick}
26+
>
27+
{intl.formatMessage(baseMessages['authz.management.assign.role.title'])}
28+
</Button>
29+
);
30+
};
31+
32+
export default AddRoleButton;
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
// src/components/ProtectedRoute.tsx
2+
import { ReactElement } from 'react';
3+
import { useValidateUserPermissions } from '@src/data/hooks';
4+
import LoadingPage from 'components/LoadingPage';
5+
import { CustomErrors } from 'constants';
6+
import { CONTENT_COURSE_PERMISSIONS, CONTENT_LIBRARY_PERMISSIONS } from 'authz-module/constants';
7+
8+
const REQUIRED_USER_PERMISSIONS = [
9+
CONTENT_LIBRARY_PERMISSIONS.VIEW_LIBRARY_TEAM,
10+
CONTENT_LIBRARY_PERMISSIONS.MANAGE_LIBRARY_TEAM,
11+
CONTENT_COURSE_PERMISSIONS.VIEW_COURSE_TEAM,
12+
CONTENT_COURSE_PERMISSIONS.MANAGE_COURSE_TEAM,
13+
];
14+
15+
type ProtectedRouteProps = {
16+
children: ReactElement;
17+
fallback?: ReactElement;
18+
};
19+
20+
export const ProtectedRoute = ({
21+
children,
22+
fallback,
23+
}: ProtectedRouteProps) => {
24+
// TODO: which scope?
25+
const requiredPermissions = REQUIRED_USER_PERMISSIONS.map(action => ({ action, scope: '*' }));
26+
const { data: permissions, isLoading, isError } = useValidateUserPermissions(requiredPermissions);
27+
28+
if (isLoading) {
29+
return <LoadingPage />;
30+
}
31+
32+
if (isError && fallback) {
33+
return fallback;
34+
}
35+
if (isError) {
36+
throw new Error(CustomErrors.SERVER_ERROR);
37+
}
38+
39+
const hasAccess = permissions.some(permission => permission.allowed);
40+
41+
if (!hasAccess) {
42+
throw new Error(CustomErrors.NO_ACCESS);
43+
}
44+
45+
return children;
46+
};

0 commit comments

Comments
 (0)