Skip to content

Commit 0494ee6

Browse files
committed
feat: create the user management view
1 parent ba5478d commit 0494ee6

5 files changed

Lines changed: 238 additions & 1 deletion

File tree

src/authz-module/index.scss

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,31 @@
77
.tab-content {
88
background-color: var(--pgn-color-light-200);
99
}
10-
}
10+
11+
.collapsible-card {
12+
border: none;
13+
14+
.collapsible-body {
15+
padding: 0;
16+
}
17+
}
18+
19+
.collapsible-trigger {
20+
background-color: var(--pgn-color-info-100);
21+
border: none;
22+
border-radius: 0 !important;
23+
color: var(--pgn-color-primary-base);
24+
padding: 1rem 2rem 1rem 1rem;
25+
}
26+
27+
.permission-chip {
28+
.pgn__chip__label {
29+
font-weight: var(--pgn-typography-font-weight-base);
30+
}
31+
32+
svg {
33+
width: var(--pgn-size-icon-xs);
34+
height: var(--pgn-size-icon-xs);
35+
}
36+
}
37+
}
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useParams } from 'react-router-dom';
2+
import { screen } from '@testing-library/react';
3+
import { renderWrapper } from '@src/setupTest';
4+
import LibrariesUserManager from './LibrariesUserManager';
5+
import { useLibraryAuthZ } from './context';
6+
import { useLibrary, useTeamMembers } from '../data/hooks';
7+
8+
jest.mock('react-router-dom', () => ({
9+
...jest.requireActual('react-router-dom'),
10+
useParams: jest.fn(),
11+
}));
12+
13+
jest.mock('./context', () => ({
14+
useLibraryAuthZ: jest.fn(),
15+
}));
16+
17+
jest.mock('../data/hooks', () => ({
18+
useLibrary: jest.fn(),
19+
useTeamMembers: jest.fn(),
20+
}));
21+
jest.mock('../components/RoleCard', () => ({
22+
__esModule: true,
23+
default: ({ title, description }: { title: string, description: string }) => (
24+
<div data-testid="role-card">
25+
<div>{title}</div>
26+
<div>{description}</div>
27+
</div>
28+
),
29+
}));
30+
31+
describe('LibrariesUserManager', () => {
32+
beforeEach(() => {
33+
jest.clearAllMocks();
34+
35+
// Mock route params
36+
(useParams as jest.Mock).mockReturnValue({ username: 'testuser' });
37+
38+
// Mock library authz context
39+
(useLibraryAuthZ as jest.Mock).mockReturnValue({
40+
libraryId: 'lib:123',
41+
permissions: [{ key: 'view' }, { key: 'reuse' }],
42+
roles: [
43+
{
44+
role: 'admin',
45+
name: 'Admin',
46+
description: 'Administrator Role',
47+
permissions: ['view', 'reuse'],
48+
},
49+
],
50+
resources: [
51+
{ key: 'library', label: 'Library', description: '' },
52+
],
53+
});
54+
55+
// Mock library data
56+
(useLibrary as jest.Mock).mockReturnValue({
57+
data: {
58+
title: 'Test Library',
59+
org: 'Test Org',
60+
},
61+
});
62+
63+
// Mock team members
64+
(useTeamMembers as jest.Mock).mockReturnValue({
65+
data: [
66+
{
67+
username: 'testuser',
68+
69+
roles: ['admin'],
70+
},
71+
],
72+
});
73+
});
74+
75+
it('renders the user roles correctly', () => {
76+
renderWrapper(<LibrariesUserManager />);
77+
78+
// Breadcrumb check
79+
expect(screen.getByText('Manage Access')).toBeInTheDocument();
80+
expect(screen.getByText('Library Team Management')).toBeInTheDocument();
81+
expect(screen.getByRole('listitem', { current: 'page' })).toHaveTextContent('testuser');
82+
// Page title and subtitle
83+
expect(screen.getByRole('heading', { level: 1 })).toHaveTextContent('testuser');
84+
expect(screen.getByRole('paragraph')).toHaveTextContent('[email protected]');
85+
86+
// RoleCard rendering
87+
expect(screen.getByTestId('role-card')).toHaveTextContent('Admin');
88+
expect(screen.getByTestId('role-card')).toHaveTextContent('Administrator Role');
89+
});
90+
});
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { useMemo } from 'react';
2+
import { useParams } from 'react-router-dom';
3+
import { useIntl } from '@edx/frontend-platform/i18n';
4+
import { Container } from '@openedx/paragon';
5+
import { ROUTES } from '@src/authz-module/constants';
6+
import AuthZLayout from '../components/AuthZLayout';
7+
import { useLibraryAuthZ } from './context';
8+
import RoleCard from '../components/RoleCard';
9+
import { useLibrary, useTeamMembers } from '../data/hooks';
10+
import { buildPermissionsByRoleMatrix } from './utils';
11+
12+
import messages from './messages';
13+
14+
const LibrariesUserManager = () => {
15+
const intl = useIntl();
16+
const { username } = useParams();
17+
const {
18+
libraryId, permissions, roles, resources,
19+
} = useLibraryAuthZ();
20+
const { data: library } = useLibrary(libraryId);
21+
const rootBreadcrumb = intl.formatMessage(messages['library.authz.breadcrumb.root']) || '';
22+
const pageManageTitle = intl.formatMessage(messages['library.authz.manage.page.title']);
23+
24+
const { data: teamMembers } = useTeamMembers(libraryId);
25+
const user = teamMembers?.find(member => member.username === username);
26+
const userRoles = useMemo(() => {
27+
const assignedRoles = roles.filter(role => user?.roles.includes(role.role))
28+
.map(role => ({
29+
...role,
30+
permissions: buildPermissionsByRoleMatrix({
31+
rolePermissions: role.permissions, permissions, resources, intl,
32+
}),
33+
}));
34+
return assignedRoles;
35+
}, [roles, user?.roles, permissions, resources, intl]);
36+
37+
return (
38+
<div className="authz-libraries">
39+
<AuthZLayout
40+
context={{ id: libraryId, title: library.title, org: library.org }}
41+
navLinks={[{ label: rootBreadcrumb }, { label: pageManageTitle, to: `/authz/${ROUTES.LIBRARIES_TEAM_PATH.replace(':libraryId', libraryId)}` }]}
42+
activeLabel={user?.username || ''}
43+
pageTitle={user?.username || ''}
44+
pageSubtitle={<p>{user?.email}</p>}
45+
actions={[]}
46+
>
47+
<Container className="bg-light-200 p-5">
48+
{userRoles && userRoles.map(role => (
49+
<RoleCard
50+
key={`${role}-${username}`}
51+
title={role.name}
52+
objectName={library.title}
53+
description={role.description}
54+
showDelete
55+
permissions={role.permissions as any[]}
56+
/>
57+
))}
58+
</Container>
59+
</AuthZLayout>
60+
</div>
61+
);
62+
};
63+
64+
export default LibrariesUserManager;
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { buildPermissionsByRoleMatrix } from './utils';
2+
3+
describe('buildPermissionsByRoleMatrix', () => {
4+
it('returns permissions matrix for given role', () => {
5+
const rolePermissions = ['create_library'];
6+
const permissions = [
7+
{ key: 'create_library', resource: 'library', label: 'Create Library' },
8+
{ key: 'edit_library', resource: 'library', label: 'Edit Library' },
9+
];
10+
const resources = [
11+
{ key: 'library', label: 'Library', description: '' },
12+
];
13+
14+
const intl = { formatMessage: jest.fn((msg: any) => msg.defaultMessage) };
15+
const matrix = buildPermissionsByRoleMatrix({
16+
rolePermissions, permissions, resources, intl,
17+
}) as Array<{ key: string; actions: Array<{ disabled: boolean }> }>;
18+
expect(matrix[0].key).toBe('library');
19+
expect(matrix[0].actions.length).toBe(2);
20+
expect(matrix[0].actions[0].disabled).toBe(false);
21+
expect(matrix[0].actions[1].disabled).toBe(true);
22+
});
23+
});
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { actionKeys } from '@src/authz-module/components/RoleCard/constants';
2+
import actionMessages from '../components/RoleCard/messages';
3+
4+
const buildPermissionsByRoleMatrix = ({
5+
rolePermissions, permissions, resources, intl,
6+
}) => {
7+
const permissionsMatrix = {};
8+
const allowedPermissions = new Set(rolePermissions);
9+
10+
permissions.forEach((permission) => {
11+
const resourceLabel = resources.find(r => r.key === permission.resource)?.label || permission.resource;
12+
const actionKey = actionKeys.find(action => permission.key.includes(action));
13+
let messageKey = `authz.permissions.actions.${actionKey}`;
14+
let messageResource = '';
15+
16+
permissionsMatrix[permission.resource] = permissionsMatrix[permission.resource]
17+
|| { key: permission.resource, label: resourceLabel, actions: [] };
18+
19+
if (actionKey === 'tag' || actionKey === 'team') {
20+
messageKey = 'authz.permissions.actions.manage';
21+
messageResource = actionKey === 'tag' ? 'Tags' : messageResource;
22+
}
23+
24+
permissionsMatrix[permission.resource].actions.push({
25+
key: actionKey,
26+
label: permission.label || intl.formatMessage(actionMessages[messageKey], { resource: messageResource }),
27+
disabled: !allowedPermissions.has(permission.key),
28+
});
29+
});
30+
return Object.values(permissionsMatrix);
31+
};
32+
33+
export { buildPermissionsByRoleMatrix };

0 commit comments

Comments
 (0)