Skip to content

Commit ef7bf9b

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

18 files changed

Lines changed: 553 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: 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: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { TableCellValue, TeamMember } from '@src/types';
2+
3+
type CellProps = TableCellValue<TeamMember>;
4+
type ExtendedCellProps = CellProps & {
5+
value: string;
6+
cell: {
7+
getCellProps: (props?: Record<string, string>) => Record<string, string>;
8+
};
9+
};
10+
11+
const RoleCell = ({ value, cell }: ExtendedCellProps) => (
12+
<td {...cell.getCellProps({ 'data-role': value })}>
13+
{value}
14+
</td>
15+
);
16+
17+
export {
18+
RoleCell,
19+
};
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
import React, { useContext } from 'react';
2+
import { useIntl } from '@edx/frontend-platform/i18n';
3+
import { DataTableContext, Pagination, TableFooter } from '@openedx/paragon';
4+
import messages from '../messages';
5+
6+
const Footer = () => {
7+
const { formatMessage } = useIntl();
8+
const {
9+
pageCount, gotoPage, state, itemCount,
10+
// @ts-ignore-next-line - Paragon's DataTableContext is not typed
11+
} = useContext<DataTableContext>(DataTableContext);
12+
const { pageIndex, pageSize } = state;
13+
14+
return (
15+
<TableFooter>
16+
<span>
17+
{formatMessage(messages['authz.table.footer.items.showing.text'], { pageSize, itemCount })}
18+
</span>
19+
<Pagination
20+
variant="reduced"
21+
currentPage={pageIndex + 1}
22+
pageCount={pageCount}
23+
onPageSelect={(pageNum) => gotoPage(pageNum - 1)}
24+
/>
25+
</TableFooter>
26+
);
27+
};
28+
29+
export default Footer;

0 commit comments

Comments
 (0)