Skip to content

Commit 01d7205

Browse files
feat: roles table for audit user page
1 parent 568828d commit 01d7205

15 files changed

Lines changed: 538 additions & 4 deletions

File tree

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: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
import React, { useMemo } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import debounce from 'lodash.debounce';
4+
import {
5+
Container, DataTable, TableFooter,
6+
} from '@openedx/paragon';
7+
import { TABLE_DEFAULT_PAGE_SIZE } from '@src/authz-module/constants';
8+
import AuthZLayout from '@src/authz-module/components/AuthZLayout';
9+
import { useNavigate, useParams } from 'react-router-dom';
10+
import { useUserAccount } from '@src/data/hooks';
11+
import baseMessages from '@src/authz-module/messages';
12+
import AddRoleButton from '@src/authz-module/components/AddRoleButton';
13+
import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
14+
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
15+
import messages from './messages';
16+
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
17+
18+
const dummyData = [
19+
{
20+
role: 'Course Admin',
21+
organization: 'edX',
22+
scope: 'Course: Demo Course',
23+
permissions: ['View', 'Edit', 'Delete'],
24+
},
25+
{
26+
role: 'Course Auditor',
27+
organization: 'edX',
28+
scope: 'Course: Demo Course 2',
29+
permissions: ['View', 'Edit'],
30+
},
31+
{
32+
role: 'Course Admin',
33+
organization: 'edX',
34+
scope: 'Course: Demo Course',
35+
permissions: ['View', 'Edit', 'Delete'],
36+
},
37+
{
38+
role: 'Course Auditor',
39+
organization: 'edX',
40+
scope: 'Course: Demo Course 2',
41+
permissions: ['View', 'Edit'],
42+
},
43+
{
44+
role: 'Course Admin',
45+
organization: 'edX',
46+
scope: 'Course: Demo Course',
47+
permissions: ['View', 'Edit', 'Delete'],
48+
},
49+
{
50+
role: 'Course Auditor',
51+
organization: 'edX',
52+
scope: 'Course: Demo Course 2',
53+
permissions: ['View', 'Edit'],
54+
},
55+
{
56+
role: 'Course Admin',
57+
organization: 'edX',
58+
scope: 'Course: Demo Course',
59+
permissions: ['View', 'Edit', 'Delete'],
60+
},
61+
{
62+
role: 'Course Auditor',
63+
organization: 'edX',
64+
scope: 'Course: Demo Course 2',
65+
permissions: ['View', 'Edit'],
66+
},
67+
{
68+
role: 'Course Admin',
69+
organization: 'edX',
70+
scope: 'Course: Demo Course',
71+
permissions: ['View', 'Edit', 'Delete'],
72+
},
73+
{
74+
role: 'Course Auditor',
75+
organization: 'edX',
76+
scope: 'Course: Demo Course 2',
77+
permissions: ['View', 'Edit'],
78+
},
79+
{
80+
role: 'Course Admin',
81+
organization: 'edX',
82+
scope: 'Course: Demo Course',
83+
permissions: ['View', 'Edit', 'Delete'],
84+
},
85+
{
86+
role: 'Course Auditor',
87+
organization: 'edX',
88+
scope: 'Course: Demo Course 2',
89+
permissions: ['View', 'Edit'],
90+
},
91+
{
92+
role: 'Course Admin',
93+
organization: 'edX',
94+
scope: 'Course: Demo Course',
95+
permissions: ['View', 'Edit', 'Delete'],
96+
},
97+
{
98+
role: 'Course Auditor',
99+
organization: 'edX',
100+
scope: 'Course: Demo Course 2',
101+
permissions: ['View', 'Edit'],
102+
},
103+
];
104+
105+
const AuditUserPage = () => {
106+
const { formatMessage } = useIntl();
107+
const { username } = useParams();
108+
const navigate = useNavigate();
109+
const { isLoading: isLoadingUser, data: user } = useUserAccount(username ?? '');
110+
const { querySettings, handleTableFetch } = useQuerySettings();
111+
// TODO: use actual assigned roles data when API is ready, currently using dummy data for development purpose
112+
const { data: _userAssignedRoles } = useUserAssignedRoles(username ?? '', querySettings);
113+
const authzHomePath = '/authz';
114+
if (!user && !isLoadingUser) {
115+
navigate(authzHomePath);
116+
}
117+
const navLinks = [
118+
{
119+
label: formatMessage(baseMessages['authz.management.home.nav.link']),
120+
to: authzHomePath,
121+
},
122+
];
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+
];
135+
const columns = [
136+
{
137+
Header: formatMessage(messages['authz.user.table.role.column.header']),
138+
accessor: 'role',
139+
},
140+
{
141+
Header: formatMessage(messages['authz.user.table.organization.column.header']),
142+
accessor: 'organization',
143+
},
144+
{
145+
Header: formatMessage(messages['authz.user.table.scope.column.header']),
146+
accessor: 'scope',
147+
disableFilters: true,
148+
},
149+
{
150+
Header: formatMessage(messages['authz.user.table.permissions.column.header']),
151+
Cell: PermissionsCell,
152+
disableFilters: true,
153+
disableSortBy: true,
154+
},
155+
];
156+
const pageCount = dummyData?.length ? Math.ceil(dummyData.length / TABLE_DEFAULT_PAGE_SIZE) : 1;
157+
158+
const fetchData = useMemo(() => debounce(handleTableFetch, 500), [handleTableFetch]);
159+
160+
return (
161+
<div className="authz-module">
162+
<AuthZLayout
163+
context={{
164+
id: '',
165+
org: '',
166+
title: '',
167+
}}
168+
navLinks={navLinks}
169+
activeLabel={formatMessage(baseMessages['authz.management.specific.user.nav.link'])}
170+
pageTitle={user?.username || ''}
171+
pageSubtitle={user?.email || ''}
172+
actions={
173+
[
174+
<AddRoleButton presetUsername={user?.username} key="add-role-button" />,
175+
]
176+
}
177+
>
178+
<Container className="bg-light-200 p-5">
179+
<DataTable
180+
isPaginated
181+
manualPagination
182+
data={dummyData}
183+
fetchData={fetchData}
184+
itemCount={dummyData?.length || 0}
185+
pageCount={pageCount}
186+
initialState={{ pageSize: TABLE_DEFAULT_PAGE_SIZE }}
187+
additionalColumns={additionalColumns}
188+
columns={columns}
189+
>
190+
<DataTable.Table />
191+
<TableFooter />
192+
</DataTable>
193+
194+
</Container>
195+
</AuthZLayout>
196+
</div>
197+
);
198+
};
199+
200+
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)