diff --git a/src/authz-module/libraries-manager/components/TeamTable.test.tsx b/src/authz-module/libraries-manager/components/TeamTable.test.tsx
new file mode 100644
index 0000000..d9cf678
--- /dev/null
+++ b/src/authz-module/libraries-manager/components/TeamTable.test.tsx
@@ -0,0 +1,126 @@
+import { screen } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { ROUTES } from '@src/authz-module/constants';
+import { renderWrapper } from '@src/setupTest';
+import TeamTable from './TeamTable';
+import { useTeamMembers } from '../data/hooks';
+import { useLibraryAuthZ } from '../context';
+
+const mockNavigate = jest.fn();
+jest.mock('react-router', () => ({
+ ...jest.requireActual('react-router'),
+ useNavigate: () => mockNavigate,
+}));
+
+jest.mock('../data/hooks', () => ({
+ useTeamMembers: jest.fn(),
+}));
+
+jest.mock('../context', () => ({
+ useLibraryAuthZ: jest.fn(),
+}));
+
+describe('TeamTable', () => {
+ const mockTeamMembers = [
+ {
+ displayName: 'Alice',
+ email: 'alice@example.com',
+ roles: ['Admin', 'Editor'],
+ username: 'alice',
+ },
+ {
+ displayName: 'Bob',
+ email: 'bob@example.com',
+ roles: ['Viewer'],
+ username: 'bob',
+ },
+ ];
+
+ const mockAuthZ = {
+ libraryId: 'lib:123',
+ canManageTeam: true,
+ username: 'alice',
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('shows skeletons while loading', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: null,
+ isLoading: true,
+ });
+ (useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
+
+ renderWrapper();
+
+ const skeletons = screen.getAllByText('', { selector: '[aria-busy="true"]' });
+ expect(skeletons.length).toBeGreaterThan(0);
+ });
+
+ it('renders team member data after loading', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: mockTeamMembers,
+ isLoading: false,
+ });
+ (useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
+
+ renderWrapper();
+
+ expect(screen.getByText('Alice')).toBeInTheDocument();
+ expect(screen.getByText('alice@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Admin')).toBeInTheDocument();
+ expect(screen.getByText('Editor')).toBeInTheDocument();
+
+ expect(screen.getByText('Bob')).toBeInTheDocument();
+ expect(screen.getByText('bob@example.com')).toBeInTheDocument();
+ expect(screen.getByText('Viewer')).toBeInTheDocument();
+ });
+
+ it('renders Edit button only for users with than can manage team members (current user can not edit themselves)', async () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: mockTeamMembers,
+ isLoading: false,
+ });
+ (useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
+
+ renderWrapper();
+
+ const editButtons = screen.queryAllByText('Edit');
+ // Should not find Edit button for current user
+ expect(editButtons).toHaveLength(1);
+
+ await userEvent.click(editButtons[0]);
+ expect(mockNavigate).toHaveBeenCalledWith(
+ `/authz/${ROUTES.LIBRARIES_USER_PATH.replace(':username', 'alice')}`,
+ );
+ });
+
+ it('does not render Edit button if canManageTeam is false', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: mockTeamMembers,
+ isLoading: false,
+ });
+ (useLibraryAuthZ as jest.Mock).mockReturnValue({
+ ...mockAuthZ,
+ canManageTeam: false,
+ });
+
+ renderWrapper();
+
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
+ });
+
+ it('does not render Edit button while loading', () => {
+ (useTeamMembers as jest.Mock).mockReturnValue({
+ data: null,
+ isLoading: true,
+ });
+ (useLibraryAuthZ as jest.Mock).mockReturnValue(mockAuthZ);
+
+ renderWrapper();
+
+ expect(screen.queryByText('Edit')).not.toBeInTheDocument();
+ });
+});
diff --git a/src/authz-module/libraries-manager/components/TeamTable.tsx b/src/authz-module/libraries-manager/components/TeamTable.tsx
new file mode 100644
index 0000000..48a7dad
--- /dev/null
+++ b/src/authz-module/libraries-manager/components/TeamTable.tsx
@@ -0,0 +1,105 @@
+import { useMemo } from 'react';
+import { useNavigate } from 'react-router';
+import { useIntl } from '@edx/frontend-platform/i18n';
+import {
+ DataTable, Button, Chip, Skeleton,
+} from '@openedx/paragon';
+import { Edit } from '@openedx/paragon/icons';
+import { ROUTES, TableCellValue, TeamMember } from '@src/authz-module/constants';
+import { useTeamMembers } from '../data/hooks';
+import { useLibraryAuthZ } from '../context';
+import messages from './messages';
+
+const SKELETON_ROWS = Array.from({ length: 10 }).map(() => ({
+ username: 'skeleton',
+ name: '',
+ email: '',
+ roles: [],
+}));
+
+type CellProps = TableCellValue;
+
+const EmailCell = ({ row }: CellProps) => (row.original?.username === SKELETON_ROWS[0].username ? (
+
+) : (
+ row.original.email
+));
+
+const NameCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
+
+) : (
+ row.original.displayName
+));
+
+const RolesCell = ({ row }: CellProps) => (row.original.username === SKELETON_ROWS[0].username ? (
+
+) : (
+ row.original.roles.map((role) => (
+ {role}
+ ))
+));
+
+const TeamTable = () => {
+ const intl = useIntl();
+ const { libraryId, canManageTeam, username } = useLibraryAuthZ();
+
+ // TODO: Display error in the notification system
+ const {
+ data: teamMembers, isLoading, isError
+ } = useTeamMembers(libraryId);
+
+ const rows = isError ? [] : (teamMembers || SKELETON_ROWS);
+
+ const navigate = useNavigate();
+
+ const columns = useMemo(() => [
+ {
+ Header: intl.formatMessage(messages['library.authz.team.table.display.name']),
+ accessor: 'displayName',
+ Cell: NameCell,
+ },
+ {
+ Header: intl.formatMessage(messages['library.authz.team.table.email']),
+ accessor: 'email',
+ Cell: EmailCell,
+ },
+ {
+ Header: intl.formatMessage(messages['library.authz.team.table.roles']),
+ accessor: 'roles',
+ Cell: RolesCell,
+ },
+ ], [isLoading]);
+
+ return (
+ (
+ canManageTeam && row.original.username !== username && !isLoading ? (
+
+ ) : null),
+ },
+ ]}
+ initialState={{
+ pageSize: 10,
+ }}
+ columns={columns}
+ />
+ );
+};
+
+export default TeamTable;
diff --git a/src/authz-module/libraries-manager/components/messages.ts b/src/authz-module/libraries-manager/components/messages.ts
new file mode 100644
index 0000000..70181e4
--- /dev/null
+++ b/src/authz-module/libraries-manager/components/messages.ts
@@ -0,0 +1,31 @@
+import { defineMessages } from '@edx/frontend-platform/i18n';
+
+const messages = defineMessages({
+ 'library.authz.team.table.display.name': {
+ id: 'library.authz.team.table.display.name',
+ defaultMessage: 'Name',
+ description: 'Libraries team management table name column header',
+ },
+ 'library.authz.team.table.email': {
+ id: 'library.team.table.email',
+ defaultMessage: 'Email',
+ description: 'Libraries team management table email column header',
+ },
+ 'library.authz.team.table.roles': {
+ id: 'library.authz.team.table.roles',
+ defaultMessage: 'Roles',
+ description: 'Libraries team management table roles column header',
+ },
+ 'library.authz.team.table.action': {
+ id: 'library.authz.team.table.action',
+ defaultMessage: 'Action',
+ description: 'Libraries team management table action column header',
+ },
+ 'authz.libraries.team.table.edit.action': {
+ id: 'authz.libraries.team.table.edit.action',
+ defaultMessage: 'Edit',
+ description: 'Edit action',
+ },
+});
+
+export default messages;