Skip to content

Commit 43fb628

Browse files
feat: expanded row view for user roles added
1 parent 151f3ce commit 43fb628

12 files changed

Lines changed: 945 additions & 49 deletions

File tree

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

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,26 @@ import { TableCellValue, UserRole } from 'types';
66
import messages from './messages';
77
import { getPermissionsCountByRole } from './utils';
88

9-
type CellProps = TableCellValue<UserRole>;
9+
interface ExpandableTableRow<T> extends TableCellValue<T> {
10+
row: TableCellValue<T>['row'] & {
11+
isExpanded: boolean;
12+
toggleRowExpanded: () => void;
13+
values: T;
14+
};
15+
}
16+
17+
type CellProps = ExpandableTableRow<UserRole>;
1018

1119
export const ViewAllPermissionsCell = ({ row }: CellProps) => {
1220
const { formatMessage } = useIntl();
1321
return (
1422
<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)}
23+
label={formatMessage(
24+
row.isExpanded
25+
? messages['authz.user.table.view_all_permissions.link.text.close']
26+
: messages['authz.user.table.view_all_permissions.link.text.open'],
27+
)}
28+
onClick={() => row.toggleRowExpanded()}
1829
iconSrc={ExpandMore}
1930
/>
2031
);
@@ -35,6 +46,9 @@ export const ActionsCell = ({ row }: CellProps) => {
3546
export const PermissionsCell = ({ row }: CellProps) => {
3647
const { formatMessage } = useIntl();
3748
// TODO handle permissions length per role
49+
if (row.original.permissions.length === 1) {
50+
return <span>{row.original.permissions[0]}</span>;
51+
}
3852
const count = getPermissionsCountByRole(row.original.role);
3953
return (
4054
<span>
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { useIntl } from '@edx/frontend-platform/i18n';
2+
import messages from './messages';
3+
4+
interface RenderAdminRoleProps {
5+
role: string;
6+
}
7+
8+
const RenderAdminRole = ({ role }: RenderAdminRoleProps) => {
9+
const intl = useIntl();
10+
// Determine which message to show based on role
11+
const messageKey = role?.toLowerCase().includes('admin')
12+
? 'authz.user.table.permissions.role.admin'
13+
: 'authz.user.table.permissions.role.staff';
14+
15+
return (
16+
<div className="mb-4">
17+
<p className="mb-0 text-gray-700">
18+
{intl.formatMessage(messages[messageKey])}
19+
</p>
20+
</div>
21+
);
22+
};
23+
24+
export default RenderAdminRole;
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Icon } from '@openedx/paragon';
2+
import ResourceTooltip from 'authz-module/components/ResourceTooltip';
3+
import { RolePermission } from 'types';
4+
5+
interface ExtendedRolePermission extends RolePermission {
6+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
7+
}
8+
9+
interface PermissionItem {
10+
key: string;
11+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
12+
label: string;
13+
description: string;
14+
perms: ExtendedRolePermission[];
15+
}
16+
17+
interface RenderPermissionColumnProps {
18+
items: PermissionItem[];
19+
}
20+
21+
const RenderPermissionColumn = ({ items }: RenderPermissionColumnProps) => items.map(({
22+
key, icon, label, description, perms,
23+
}) => (
24+
<div key={key} className="mb-4">
25+
<div className="d-flex align-items-center mb-2">
26+
<Icon src={icon} className="mr-2 text-primary" size="xs" />
27+
<h5 className="text-primary m-0">{label}</h5>
28+
<ResourceTooltip
29+
resourceGroup={{
30+
key, label, description, permissions: perms,
31+
}}
32+
/>
33+
</div>
34+
<ul className="mb-0 list-unstyled d-flex">
35+
{perms.map((perm, index) => (
36+
<li
37+
key={perm.key}
38+
className={`d-flex align-items-center text-primary-400 ${index !== perms.length - 1 ? 'border-right pr-2' : ''
39+
} ${index !== 0 ? 'pl-2' : ''}`}
40+
>
41+
<Icon src={perm.icon} className="mr-2" size="xs" />
42+
<span className="text-primary small font-weight-light">
43+
{perm.label}
44+
</span>
45+
</li>
46+
))}
47+
</ul>
48+
</div>
49+
));
50+
51+
export default RenderPermissionColumn;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { Icon } from '@openedx/paragon';
2+
import ResourceTooltip from 'authz-module/components/ResourceTooltip';
3+
import { RolePermission } from 'types';
4+
5+
interface ExtendedRolePermission extends RolePermission {
6+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
7+
}
8+
9+
interface PermissionItem {
10+
key: string;
11+
icon: React.ComponentType<React.SVGProps<SVGSVGElement>>;
12+
label: string;
13+
description: string;
14+
perms: ExtendedRolePermission[];
15+
}
16+
17+
interface RenderPermissionInLineProps {
18+
items: PermissionItem[];
19+
}
20+
21+
const RenderPermissionInLine = ({ items }: RenderPermissionInLineProps) => (
22+
<div className="d-flex align-items-start w-100 no-scroll">
23+
{items.map(({
24+
key, icon, label, description, perms,
25+
}, index) => (
26+
<div
27+
key={key}
28+
className={`d-flex flex-column ${index !== items.length - 1 ? 'pr-4 mr-4 border-right' : ''}`}
29+
>
30+
<div className="d-flex align-items-center mb-2">
31+
<Icon src={icon} className="mr-2 text-primary" size="xs" />
32+
<h5 className="text-primary m-0">{label}</h5>
33+
<ResourceTooltip
34+
resourceGroup={{
35+
key, label, description, permissions: perms,
36+
}}
37+
/>
38+
</div>
39+
<div className="d-flex">
40+
{perms.map((perm, i) => (
41+
<div
42+
key={perm.key}
43+
className={`d-flex align-items-center ${i !== perms.length - 1 ? 'pr-2 mr-2 border-right' : ''}`}
44+
>
45+
<Icon src={perm.icon} className="mr-2" size="xs" />
46+
<span className="text-primary small font-weight-light">
47+
{perm.label}
48+
</span>
49+
</div>
50+
))}
51+
</div>
52+
</div>
53+
))}
54+
</div>
55+
);
56+
export default RenderPermissionInLine;
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import {
2+
courseResourceTypes,
3+
coursePermissions,
4+
rolesObject,
5+
} from '../courses/constant';
6+
import {
7+
libraryResourceTypes,
8+
libraryPermissions,
9+
rolesLibraryObject,
10+
} from '../libraries/constants';
11+
import RenderPermissionColumn from './RenderPermissionColumn';
12+
import RenderPermissionInLine from './RenderPermissionInLine';
13+
import RenderAdminRole from './RenderAdminRole';
14+
15+
interface UserPermissionsProps {
16+
row: {
17+
original: {
18+
role: string;
19+
};
20+
};
21+
}
22+
23+
const UserPermissions = ({ row }: UserPermissionsProps) => {
24+
let roleKey = row?.original?.role;
25+
if (!roleKey) { return null; }
26+
27+
// validation to show django roles
28+
const normalizedRole = roleKey.trim().toLowerCase();
29+
if (!normalizedRole.includes('library') && !normalizedRole.includes('course')
30+
&& (normalizedRole.includes('admin') || normalizedRole.includes('staff'))) {
31+
return (
32+
<div className="d-flex flex-wrap bg-white px-4 py-4 border border-light">
33+
<RenderAdminRole role={roleKey} />
34+
</div>
35+
);
36+
}
37+
38+
// Normalize role string to match keys in constants (e.g. "Course Admin" -> "course_admin")
39+
roleKey = roleKey.trim().toLowerCase().replace(/[-\s]+/g, '_');
40+
const isLibraryRole = roleKey.includes('library');
41+
const config = isLibraryRole
42+
? {
43+
resourceTypes: libraryResourceTypes,
44+
permissions: libraryPermissions,
45+
roles: rolesLibraryObject,
46+
}
47+
: {
48+
resourceTypes: courseResourceTypes,
49+
permissions: coursePermissions,
50+
roles: rolesObject,
51+
};
52+
53+
const roleObj = config.roles.find(r => r.role === roleKey);
54+
if (!roleObj) { return null; }
55+
56+
const rolePerms = new Set(roleObj.permissions.map(String));
57+
// Build resource list with permissions (only once)
58+
const resources = config.resourceTypes
59+
.map(resource => {
60+
const perms = config.permissions.filter(
61+
p => p.resource === resource.key && rolePerms.has(String(p.key)),
62+
);
63+
return perms.length ? { ...resource, perms } : null;
64+
})
65+
.filter(Boolean);
66+
67+
const isSingleRow = resources.length <= 3;
68+
const mid = Math.ceil(resources.length / 2);
69+
const columns = isSingleRow
70+
? [resources]
71+
: [resources.slice(0, mid), resources.slice(mid)];
72+
return (
73+
<div className="d-flex flex-wrap bg-white px-4 py-4 border border-info-200">
74+
{isSingleRow
75+
? <RenderPermissionInLine items={resources} />
76+
: columns.map((col, index) => (
77+
<div
78+
key={`column-${index === 0 ? 'left' : 'right'}`}
79+
className={`w-100 w-md-50 py-3 ${
80+
index === 0 ? 'pr-md-3 border-right' : 'pl-md-4'
81+
}`}
82+
>
83+
<RenderPermissionColumn items={col} />
84+
</div>
85+
))}
86+
</div>
87+
);
88+
};
89+
90+
export default UserPermissions;

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

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,19 +14,20 @@ import { useQuerySettings } from '@src/authz-module/hooks/useQuerySettings';
1414
import { useUserAssignedRoles } from '@src/authz-module/data/hooks';
1515
import messages from './messages';
1616
import { ViewAllPermissionsCell, ActionsCell, PermissionsCell } from './CustomCells';
17+
import UserPermissions from './UserPermissions';
1718

1819
const dummyData = [
1920
{
20-
role: 'Course Admin',
21-
organization: 'edX',
22-
scope: 'Course: Demo Course',
23-
permissions: ['View', 'Edit', 'Delete'],
21+
role: 'Super Admin',
22+
organization: 'All organizations',
23+
scope: 'Global',
24+
permissions: ['Total access'],
2425
},
2526
{
26-
role: 'Course Auditor',
27-
organization: 'edX',
28-
scope: 'Course: Demo Course 2',
29-
permissions: ['View', 'Edit'],
27+
role: 'Global Staff',
28+
organization: 'All organizations',
29+
scope: 'Global',
30+
permissions: ['Partial access'],
3031
},
3132
{
3233
role: 'Course Admin',
@@ -35,25 +36,13 @@ const dummyData = [
3536
permissions: ['View', 'Edit', 'Delete'],
3637
},
3738
{
38-
role: 'Course Auditor',
39-
organization: 'edX',
40-
scope: 'Course: Demo Course 2',
41-
permissions: ['View', 'Edit'],
42-
},
43-
{
44-
role: 'Course Admin',
39+
role: 'Course Staff',
4540
organization: 'edX',
4641
scope: 'Course: Demo Course',
4742
permissions: ['View', 'Edit', 'Delete'],
4843
},
4944
{
50-
role: 'Course Auditor',
51-
organization: 'edX',
52-
scope: 'Course: Demo Course 2',
53-
permissions: ['View', 'Edit'],
54-
},
55-
{
56-
role: 'Course Admin',
45+
role: 'Course Editor',
5746
organization: 'edX',
5847
scope: 'Course: Demo Course',
5948
permissions: ['View', 'Edit', 'Delete'],
@@ -64,38 +53,26 @@ const dummyData = [
6453
scope: 'Course: Demo Course 2',
6554
permissions: ['View', 'Edit'],
6655
},
67-
{
68-
role: 'Course Admin',
56+
{
57+
role: 'Library Admin',
6958
organization: 'edX',
7059
scope: 'Course: Demo Course',
7160
permissions: ['View', 'Edit', 'Delete'],
7261
},
7362
{
74-
role: 'Course Auditor',
63+
role: 'Library Author',
7564
organization: 'edX',
7665
scope: 'Course: Demo Course 2',
7766
permissions: ['View', 'Edit'],
7867
},
7968
{
80-
role: 'Course Admin',
69+
role: 'Library Contributor',
8170
organization: 'edX',
8271
scope: 'Course: Demo Course',
8372
permissions: ['View', 'Edit', 'Delete'],
8473
},
8574
{
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',
75+
role: 'Library User',
9976
organization: 'edX',
10077
scope: 'Course: Demo Course 2',
10178
permissions: ['View', 'Edit'],
@@ -129,7 +106,7 @@ const AuditUserPage = () => {
129106
{
130107
id: 'action',
131108
Header: formatMessage(messages['authz.user.table.action.column.header']),
132-
Cell: ActionsCell,
109+
Cell: ActionsCell, // call the new component from other PR to show the Info Icon when is required
133110
},
134111
];
135112
const columns = [
@@ -186,6 +163,12 @@ const AuditUserPage = () => {
186163
initialState={{ pageSize: TABLE_DEFAULT_PAGE_SIZE }}
187164
additionalColumns={additionalColumns}
188165
columns={columns}
166+
isExpandable
167+
renderRowSubComponent={({ row }) => (
168+
<div>
169+
<UserPermissions row={row} />
170+
</div>
171+
)}
189172
>
190173
<DataTable.Table />
191174
<TableFooter />

0 commit comments

Comments
 (0)