Skip to content

Commit ea6a661

Browse files
feat: [UIE-8142] - IAM RBAC - Implement Roles Table (linode#12012)
* feat: [UIE-8142] - IAM RBAC - Implement Roles Table * merged with recent changes plus small tweaks to roles table * remove personalized package.json * responding to some PR feedback * fix package json * fixed lock file * Added changeset: Introduced the Web Component library, used table as POC --------- Co-authored-by: cpathipa <[email protected]>
1 parent eaa2392 commit ea6a661

File tree

16 files changed

+647
-18
lines changed

16 files changed

+647
-18
lines changed

packages/api-v4/src/iam/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,13 @@ export type AccountAccessRole =
1515
| 'account_admin'
1616
| 'account_linode_admin'
1717
| 'account_viewer'
18+
| 'account_volume_admin'
1819
| 'firewall_creator'
1920
| 'linode_contributor'
2021
| 'linode_creator';
2122

2223
export type EntityAccessRole =
24+
| 'database_admin'
2325
| 'firewall_admin'
2426
| 'firewall_creator'
2527
| 'linode_contributor'
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
UIE-8142 - implement Roles table ([#12012](https://github.com/linode/manager/pull/12012))
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Added
3+
---
4+
5+
Introduced the Web Component library, used table as POC ([#12012](https://github.com/linode/manager/pull/12012))

packages/manager/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
"@tanstack/react-query-devtools": "5.51.24",
4646
"@tanstack/react-router": "^1.111.11",
4747
"@xterm/xterm": "^5.5.0",
48+
"akamai-cds-react-components": "0.0.1-alpha.6",
4849
"algoliasearch": "^4.14.3",
4950
"axios": "~1.8.3",
5051
"braintree-web": "^3.92.2",
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { screen } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { accountPermissionsFactory } from 'src/factories/accountPermissions';
5+
import { renderWithTheme } from 'src/utilities/testHelpers';
6+
7+
import { RolesLanding } from './Roles';
8+
9+
const queryMocks = vi.hoisted(() => ({
10+
useAccountPermissions: vi.fn().mockReturnValue({}),
11+
}));
12+
13+
vi.mock('src/queries/iam/iam', async () => {
14+
const actual = await vi.importActual<any>('src/queries/iam/iam');
15+
return {
16+
...actual,
17+
useAccountPermissions: queryMocks.useAccountPermissions,
18+
};
19+
});
20+
21+
vi.mock('src/features/IAM/Shared/utilities', async () => {
22+
const actual = await vi.importActual<any>(
23+
'src/features/IAM/Shared/utilities'
24+
);
25+
return {
26+
...actual,
27+
mapAccountPermissionsToRoles: vi.fn(),
28+
};
29+
});
30+
31+
beforeEach(() => {
32+
vi.clearAllMocks();
33+
});
34+
35+
describe('RolesLanding', () => {
36+
it('renders loading state when permissions are loading', () => {
37+
queryMocks.useAccountPermissions.mockReturnValue({
38+
data: null,
39+
isLoading: true,
40+
});
41+
42+
renderWithTheme(<RolesLanding />);
43+
44+
expect(screen.getByRole('progressbar')).toBeInTheDocument();
45+
});
46+
47+
it('renders roles table when permissions are loaded', () => {
48+
const mockPermissions = accountPermissionsFactory.build();
49+
queryMocks.useAccountPermissions.mockReturnValue({
50+
data: mockPermissions,
51+
isLoading: false,
52+
});
53+
54+
renderWithTheme(<RolesLanding />);
55+
// RolesTable has a textbox at the top
56+
expect(screen.getByRole('textbox')).toBeInTheDocument();
57+
});
58+
});
Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,28 @@
1+
import { CircleProgress, Paper } from '@linode/ui';
12
import React from 'react';
23

4+
import { RolesTable } from 'src/features/IAM/Roles/RolesTable/RolesTable';
5+
import { mapAccountPermissionsToRoles } from 'src/features/IAM/Shared/utilities';
6+
import { useAccountPermissions } from 'src/queries/iam/iam';
7+
38
export const RolesLanding = () => {
9+
const { data: accountPermissions, isLoading } = useAccountPermissions();
10+
11+
const { roles } = React.useMemo(() => {
12+
if (!accountPermissions) {
13+
return { roles: [] };
14+
}
15+
const roles = mapAccountPermissionsToRoles(accountPermissions);
16+
return { roles };
17+
}, [accountPermissions]);
18+
19+
if (isLoading) {
20+
return <CircleProgress />;
21+
}
22+
423
return (
5-
<>
6-
<p>Roles Table - UIE-8142 </p>
7-
</>
24+
<Paper sx={(theme) => ({ marginTop: theme.tokens.spacing.S16 })}>
25+
<RolesTable roles={roles} />
26+
</Paper>
827
);
928
};
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import { fireEvent, screen, waitFor } from '@testing-library/react';
2+
import React from 'react';
3+
4+
import { renderWithTheme } from 'src/utilities/testHelpers';
5+
6+
import { RolesTable } from './RolesTable';
7+
8+
import type { RoleMap } from '../../Shared/utilities';
9+
10+
vi.mock('src/features/IAM/Shared/utilities', async () => {
11+
const actual = await vi.importActual<any>(
12+
'src/features/IAM/Shared/utilities'
13+
);
14+
return {
15+
...actual,
16+
mapAccountPermissionsToRoles: vi.fn(),
17+
};
18+
});
19+
20+
const mockRoles: RoleMap[] = [
21+
{
22+
access: 'account_access',
23+
description: 'Account volume admin',
24+
entity_ids: [1],
25+
entity_type: 'volume',
26+
id: 'account_volume_admin',
27+
name: 'account_volume_admin',
28+
permissions: ['attach_volume', 'delete_volume', 'clone_volume'],
29+
},
30+
{
31+
access: 'entity_access',
32+
description: 'Firewall admin',
33+
entity_ids: [1],
34+
entity_type: 'firewall',
35+
id: 'firewall_admin',
36+
name: 'firewall_admin',
37+
permissions: [],
38+
},
39+
];
40+
41+
beforeEach(() => {
42+
vi.clearAllMocks();
43+
});
44+
45+
describe('RolesTable', () => {
46+
it('renders no roles when roles array is empty', () => {
47+
const { getByText, getByTestId } = renderWithTheme(
48+
<RolesTable roles={[]} />
49+
);
50+
51+
expect(getByTestId('roles-table')).toBeInTheDocument();
52+
expect(getByText('No items to display.')).toBeInTheDocument();
53+
});
54+
55+
it('renders roles correctly when roles array is provided', () => {
56+
const { getByText, getByTestId, getAllByRole } = renderWithTheme(
57+
<RolesTable roles={mockRoles} />
58+
);
59+
60+
expect(getByTestId('roles-table')).toBeInTheDocument();
61+
expect(getAllByRole('combobox').length).toEqual(1);
62+
expect(getByText('Account volume admin')).toBeInTheDocument();
63+
});
64+
65+
it('filters roles to warranted results based on search input', async () => {
66+
renderWithTheme(<RolesTable roles={mockRoles} />);
67+
const searchInput: HTMLInputElement = screen.getByPlaceholderText('Search');
68+
fireEvent.change(searchInput, { target: { value: 'Account' } });
69+
70+
await waitFor(() => {
71+
expect(screen.getByTestId('roles-table')).toBeInTheDocument();
72+
expect(searchInput.value).toBe('Account');
73+
// TODO - if there is a way to pierce the shadow DOM, we can check these results, but these tests fail currently
74+
// expect(screen.getByText('Account')).toBeInTheDocument();
75+
// expect(screen.queryByText('Database')).not.toBeInTheDocument();
76+
// expect(screen.getByText('No items to display.')).not.toBeInTheDocument();
77+
});
78+
});
79+
80+
it('filters roles to no results based on search input if warranted', async () => {
81+
renderWithTheme(<RolesTable roles={mockRoles} />);
82+
83+
const searchInput: HTMLInputElement = screen.getByPlaceholderText('Search');
84+
fireEvent.change(searchInput, {
85+
target: { value: 'NonsenseThatWontMatchAnything' },
86+
});
87+
88+
await waitFor(() => {
89+
expect(screen.getByTestId('roles-table')).toBeInTheDocument();
90+
expect(searchInput.value).toBe('NonsenseThatWontMatchAnything');
91+
expect(screen.getByText('No items to display.')).toBeInTheDocument();
92+
});
93+
});
94+
});

0 commit comments

Comments
 (0)