Skip to content

Commit 50beaef

Browse files
authored
feat(authz): [FC-0099] permissions tab (#12)
* feat: create permissions tab * style: add tsdocs to getPermissionMetadata and buildPermissionMatrix * style: fix padding on rows table * feat: add skeleton and improve testing * feat: create ResourceTooltip component and enhance types for permission matrix by role and resource * style: enhance tooltip styles and permissions displayed order * test: remove data-testid for PermissionTable * style: use card for adding background color to the permission table component
1 parent 27a952d commit 50beaef

File tree

13 files changed

+452
-112
lines changed

13 files changed

+452
-112
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { Check, Close } from '@openedx/paragon/icons';
2+
import { Card, Icon } from '@openedx/paragon';
3+
import { PermissionsResourceGrouped, Role } from '@src/types';
4+
import { actionsDictionary } from './RoleCard/constants';
5+
import ResourceTooltip from './ResourceTooltip';
6+
7+
type PermissionTableProps = {
8+
roles: Role[];
9+
permissionsTable: PermissionsResourceGrouped[];
10+
};
11+
12+
const PermissionTable = ({ permissionsTable, roles }: PermissionTableProps) => (
13+
<Card>
14+
<table className="permission-table w-100">
15+
<thead>
16+
<tr>
17+
<th className="" aria-hidden="true" />
18+
{roles.map(role => (
19+
<th key={role.name} className="text-center py-3">{role.name}</th>
20+
))}
21+
</tr>
22+
</thead>
23+
<tbody>
24+
{permissionsTable.map(resourceGroup => (
25+
<>
26+
<tr className="bg-info-100 text-primary">
27+
<td colSpan={roles.length + 1} className="text-start py-3 px-4">
28+
<strong>{resourceGroup.label}</strong>
29+
<ResourceTooltip resourceGroup={resourceGroup} />
30+
</td>
31+
</tr>
32+
{resourceGroup.permissions.map(permission => (
33+
<tr key={permission.key} className="border-top">
34+
<td className="text-start d-flex align-items-center small px-4 py-3">
35+
<Icon className="d-inline-block mr-2" size="sm" src={actionsDictionary[permission.actionKey]} />
36+
{permission.label}
37+
</td>
38+
{roles.map(role => (
39+
<td key={role.name} className="text-center">
40+
{permission.roles[role.name] ? <Icon className="d-inline-block" src={Check} /> : <Icon className="text-danger d-inline-block" src={Close} />}
41+
</td>
42+
))}
43+
</tr>
44+
))}
45+
</>
46+
))}
47+
</tbody>
48+
</table>
49+
</Card>
50+
);
51+
52+
export default PermissionTable;
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { Icon, OverlayTrigger, Popover } from '@openedx/paragon';
2+
import { Info } from '@openedx/paragon/icons';
3+
import { PermissionsResourceGrouped, RoleResourceGroup } from '@src/types';
4+
5+
type ResourceTooltipProps = {
6+
resourceGroup: PermissionsResourceGrouped | RoleResourceGroup;
7+
};
8+
9+
const ResourceTooltip = ({ resourceGroup }:ResourceTooltipProps) => (
10+
<OverlayTrigger
11+
key={`overlay-${resourceGroup.key}`}
12+
placement="auto"
13+
overlay={(
14+
<Popover id={`tooltip-${resourceGroup.label}`}>
15+
<Popover.Content className="p-3">
16+
<h4 className="text-primary">{resourceGroup.label}</h4>
17+
<p className="small">{resourceGroup.description}</p>
18+
<ul className="small">
19+
{resourceGroup.permissions.map(permission => (
20+
<li><b>{permission.label.trim()}:</b> {permission.description}</li>
21+
))}
22+
</ul>
23+
</Popover.Content>
24+
</Popover>
25+
)}
26+
>
27+
<Icon className="d-inline-block text-gray ml-2 my-auto" size="inline" src={Info} />
28+
</OverlayTrigger>
29+
);
30+
31+
export default ResourceTooltip;

src/authz-module/components/RoleCard/PermissionsRow.tsx

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,34 @@ import { ComponentType } from 'react';
22
import {
33
Chip, Col, Row,
44
} from '@openedx/paragon';
5+
import { RoleResourceGroup } from '@src/types';
56
import { actionsDictionary, ActionKey } from './constants';
7+
import ResourceTooltip from '../ResourceTooltip';
68

7-
interface Action {
8-
key: string;
9-
label?: string;
10-
disabled?: boolean;
11-
}
9+
type PermissionRowProps = {
10+
resource: RoleResourceGroup;
11+
};
1212

13-
interface PermissionRowProps {
14-
resourceLabel: string;
15-
actions: Action[];
16-
}
17-
18-
const PermissionRow = ({ resourceLabel, actions }: PermissionRowProps) => (
13+
const PermissionRow = ({ resource }: PermissionRowProps) => (
1914
<Row className="row align-items-center border px-2 py-2">
2015
<Col md={2}>
21-
<span className="small font-weight-bold">{resourceLabel}</span>
16+
<span className="small font-weight-bold">{resource.label}</span>
17+
<ResourceTooltip resourceGroup={resource} />
2218
</Col>
2319
<Col>
2420
<div className="w-100 d-flex flex-wrap align-items-center">
25-
{actions.map((action, index) => (
21+
{resource.permissions.map((action, index) => (
2622
<>
2723
<Chip
2824
key={action.key}
29-
iconBefore={actionsDictionary[action.key as ActionKey] as ComponentType}
25+
iconBefore={actionsDictionary[action.actionKey as ActionKey] as ComponentType}
3026
disabled={action.disabled}
3127
className="mx-3 my-2 px-3 bg-primary-100 border-0 permission-chip"
3228
variant="light"
3329
>
3430
{action.label}
3531
</Chip>
36-
{(index === actions.length - 1) ? null
32+
{(index === resource.permissions.length - 1) ? null
3733
: (<hr className="border-right mx-2" style={{ height: '24px' }} />)}
3834
</>
3935
))}

src/authz-module/components/RoleCard/index.test.tsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,17 @@ describe('RoleCard', () => {
2222
description: 'Can manage everything',
2323
showDelete: true,
2424
userCounter: 2,
25-
permissions: [
25+
permissionsByResource: [
2626
{
2727
key: 'library',
2828
label: 'Library Resource',
29-
actions: [
30-
{ key: 'view', label: 'View' },
31-
{ key: 'manage', label: 'Manage', disabled: true },
29+
permissions: [
30+
{
31+
key: 'view', label: 'View', actionKey: 'view', disabled: false,
32+
},
33+
{
34+
key: 'manage', label: 'Manage', actionKey: 'manage', disabled: true,
35+
},
3236
],
3337
},
3438
],
@@ -83,7 +87,7 @@ describe('RoleCard', () => {
8387
});
8488

8589
it('handles empty permissions gracefully', () => {
86-
renderWrapper(<RoleCard {...defaultProps} permissions={[]} />);
90+
renderWrapper(<RoleCard {...defaultProps} permissionsByResource={[]} />);
8791
expect(screen.queryByText('Library Resource')).not.toBeInTheDocument();
8892
});
8993
});

src/authz-module/components/RoleCard/index.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ interface RoleCardProps extends CardTitleProps {
1515
objectName?: string | null;
1616
description: string;
1717
showDelete?: boolean;
18-
permissions: any[];
18+
permissionsByResource: any[];
1919
}
2020

2121
const CardTitle = ({ title, userCounter = null }: CardTitleProps) => (
@@ -31,7 +31,7 @@ const CardTitle = ({ title, userCounter = null }: CardTitleProps) => (
3131
);
3232

3333
const RoleCard = ({
34-
title, objectName, description, showDelete, permissions, userCounter,
34+
title, objectName, description, showDelete, permissionsByResource, userCounter,
3535
}: RoleCardProps) => {
3636
const intl = useIntl();
3737

@@ -51,13 +51,11 @@ const RoleCard = ({
5151
title={intl.formatMessage(messages['authz.permissions.title'])}
5252
>
5353
<Container>
54-
{permissions.map(({ key, label, actions }) => (
54+
{permissionsByResource.map((resourceGroup) => (
5555
<PermissionRow
56-
key={`${title}-${key}`}
57-
resourceLabel={label}
58-
actions={actions}
56+
key={`${title}-${resourceGroup.key}`}
57+
resource={resourceGroup}
5958
/>
60-
6159
))}
6260
</Container>
6361
</Collapsible>

src/authz-module/index.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@
3434
height: var(--pgn-size-icon-xs);
3535
}
3636
}
37+
38+
.permission-table {
39+
td {
40+
line-height: 24px;
41+
}
42+
}
3743
}
3844

3945

src/authz-module/libraries-manager/LibrariesTeamManager.test.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { screen } from '@testing-library/react';
1+
import { screen, within } from '@testing-library/react';
22
import userEvent from '@testing-library/user-event';
33
import { renderWrapper } from '@src/setupTest';
44
import { initializeMockApp } from '@edx/frontend-platform/testing';
@@ -32,11 +32,15 @@ jest.mock('./components/AddNewTeamMemberModal', () => ({
3232

3333
jest.mock('../components/RoleCard', () => ({
3434
__esModule: true,
35-
default: ({ title, description, permissions }: { title: string, description: string, permissions: any[] }) => (
35+
default: ({ title, description, permissionsByResource }: {
36+
title: string,
37+
description: string,
38+
permissionsByResource: any[]
39+
}) => (
3640
<div data-testid="role-card">
3741
<div>{title}</div>
3842
<div>{description}</div>
39-
<div>{permissions.length} permissions</div>
43+
<div>{permissionsByResource.length} permissions</div>
4044
</div>
4145
),
4246
}));
@@ -63,9 +67,9 @@ describe('LibrariesTeamManager', () => {
6367
],
6468
permissions: [
6569
{ key: 'view_library', label: 'view', resource: 'library' },
66-
{ key: 'edit_library', name: 'edit', resource: 'library' },
70+
{ key: 'edit_library', label: 'edit', resource: 'library' },
6771
],
68-
resources: [{ key: 'library', displayName: 'Library' }],
72+
resources: [{ key: 'library', label: 'Library' }],
6973
canManageTeam: true,
7074
});
7175

@@ -106,10 +110,28 @@ describe('LibrariesTeamManager', () => {
106110
await user.click(rolesTab);
107111

108112
const roleCards = await screen.findAllByTestId('role-card');
109-
110-
expect(roleCards.length).toBeGreaterThan(0);
111-
expect(screen.getByText('Instructor')).toBeInTheDocument();
113+
const rolesScope = within(roleCards[0]);
114+
expect(roleCards.length).toBe(1);
115+
expect(rolesScope.getByText('Instructor')).toBeInTheDocument();
112116
expect(screen.getByText(/Can manage content/i)).toBeInTheDocument();
113117
expect(screen.getByText(/1 permissions/i)).toBeInTheDocument();
114118
});
119+
120+
it('renders role matrix when "Permissions" tab is selected', async () => {
121+
const user = userEvent.setup();
122+
123+
renderWrapper(<LibrariesTeamManager />);
124+
125+
// Click on "Permissions" tab
126+
const permissionsTab = await screen.findByRole('tab', { name: /permissions/i });
127+
await user.click(permissionsTab);
128+
129+
const tablePermissionMatrix = await screen.getByRole('table');
130+
const matrixScope = within(tablePermissionMatrix);
131+
132+
expect(matrixScope.getByText('Library')).toBeInTheDocument();
133+
expect(matrixScope.getByText('Instructor')).toBeInTheDocument();
134+
expect(matrixScope.getByText('edit')).toBeInTheDocument();
135+
expect(matrixScope.getByText('view')).toBeInTheDocument();
136+
});
115137
});

src/authz-module/libraries-manager/LibrariesTeamManager.tsx

Lines changed: 28 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@ import { useLocation } from 'react-router-dom';
88
import TeamTable from './components/TeamTable';
99
import AuthZLayout from '../components/AuthZLayout';
1010
import RoleCard from '../components/RoleCard';
11+
import PermissionTable from '../components/PermissionTable';
1112
import { useLibraryAuthZ } from './context';
1213
import { AddNewTeamMemberTrigger } from './components/AddNewTeamMemberModal';
13-
import { buildPermissionsByRoleMatrix } from './utils';
14+
import { buildPermissionMatrixByResource, buildPermissionMatrixByRole } from './utils';
1415

1516
import messages from './messages';
1617

@@ -23,12 +24,18 @@ const LibrariesTeamManager = () => {
2324
const { data: library } = useLibrary(libraryId);
2425
const rootBradecrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
2526
const pageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
26-
const libraryRoles = useMemo(() => roles.map(role => ({
27-
...role,
28-
permissions: buildPermissionsByRoleMatrix({
29-
rolePermissions: role.permissions, permissions, resources, intl,
30-
}),
31-
})), [roles, permissions, resources, intl]);
27+
28+
const [libraryPermissionsByRole, libraryPermissionsByResource] = useMemo(() => {
29+
if (!roles && !permissions && !resources) { return [null, null]; }
30+
const permissionsByRole = buildPermissionMatrixByRole({
31+
roles, permissions, resources, intl,
32+
});
33+
const permissionsByResource = buildPermissionMatrixByResource({
34+
roles, permissions, resources, intl,
35+
});
36+
37+
return [permissionsByRole, permissionsByResource];
38+
}, [roles, permissions, resources, intl]);
3239

3340
return (
3441
<div className="authz-libraries">
@@ -54,20 +61,23 @@ const LibrariesTeamManager = () => {
5461
</Tab>
5562
<Tab eventKey="roles" title={intl.formatMessage(messages['library.authz.tabs.roles'])}>
5663
<Container className="p-5">
57-
{!libraryRoles ? <Skeleton count={2} height={200} /> : null}
58-
{libraryRoles && libraryRoles.map(role => (
59-
<RoleCard
60-
key={`${role.role}-description`}
61-
title={role.name}
62-
userCounter={role.userCount}
63-
description={role.description}
64-
permissions={role.permissions as any[]}
65-
/>
66-
))}
64+
{!libraryPermissionsByRole ? <Skeleton count={2} height={200} />
65+
: libraryPermissionsByRole.map(role => (
66+
<RoleCard
67+
key={`${role.role}-description`}
68+
title={role.name}
69+
userCounter={role.userCount}
70+
description={role.description}
71+
permissionsByResource={role.resources as any[]}
72+
/>
73+
))}
6774
</Container>
6875
</Tab>
6976
<Tab id="libraries-permissions-tab" eventKey="permissions" title={intl.formatMessage(messages['library.authz.tabs.permissions'])}>
70-
Permissions tab.
77+
<Container className="p-5 container-mw-lg">
78+
{!libraryPermissionsByResource ? <Skeleton count={2} height={200} />
79+
: <PermissionTable permissionsTable={libraryPermissionsByResource} roles={roles} />}
80+
</Container>
7181
</Tab>
7282
</Tabs>
7383
</AuthZLayout>

src/authz-module/libraries-manager/LibrariesUserManager.tsx

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { useLibraryAuthZ } from './context';
88
import RoleCard from '../components/RoleCard';
99
import { AssignNewRoleTrigger } from './components/AssignNewRoleModal';
1010
import { useLibrary, useTeamMembers } from '../data/hooks';
11-
import { buildPermissionsByRoleMatrix } from './utils';
11+
import { buildPermissionMatrixByRole } from './utils';
1212

1313
import messages from './messages';
1414

@@ -34,14 +34,10 @@ const LibrariesUserManager = () => {
3434
const user = teamMember?.results?.find(member => member.username === username);
3535

3636
const userRoles = useMemo(() => {
37-
const assignedRoles = roles.filter(role => user?.roles.includes(role.role))
38-
.map(role => ({
39-
...role,
40-
permissions: buildPermissionsByRoleMatrix({
41-
rolePermissions: role.permissions, permissions, resources, intl,
42-
}),
43-
}));
44-
return assignedRoles;
37+
const assignedRoles = roles.filter(role => user?.roles.includes(role.role));
38+
return buildPermissionMatrixByRole({
39+
roles: assignedRoles, permissions, resources, intl,
40+
});
4541
}, [roles, user?.roles, permissions, resources, intl]);
4642

4743
return (
@@ -69,7 +65,7 @@ const LibrariesUserManager = () => {
6965
objectName={library.title}
7066
description={role.description}
7167
showDelete
72-
permissions={role.permissions as any[]}
68+
permissionsByResource={role.resources as any[]}
7369
/>
7470
))}
7571
</Container>

0 commit comments

Comments
 (0)