Skip to content

Commit 89c8aea

Browse files
feat: [UIE-10061] - IAM Delegation: empty state for user delegations (linode#13314)
* feat: [UIE-10061] - IAM Delegation: empty state for user delegations * Added changeset: IAM Delegation: empty state for user delegations * typo * resolve conflicts * update tests
1 parent 18e43d7 commit 89c8aea

File tree

6 files changed

+366
-222
lines changed

6 files changed

+366
-222
lines changed
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+
IAM Delegation: empty state for user delegations ([#13314](https://github.com/linode/manager/pull/13314))

packages/manager/src/features/IAM/Shared/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ export const NO_ASSIGNED_ENTITIES_TEXT = `The user doesn't have any entity acces
1111

1212
export const NO_ASSIGNED_DEFAULT_ENTITIES_TEXT = `There are no default entity access roles assigned yet. Once you assign the default role on specific entities, these entities will show up here.`;
1313

14+
export const NO_ACCOUNT_DELEGATIONS_TEXT = `The user is not added to any account delegations. Once the user is added to an account delegation for specific child accounts, their list will show up here.`;
15+
1416
export const INTERNAL_ERROR_NO_CHANGES_SAVED = `Internal Error. No changes were saved.`;
1517

1618
export const LAST_ACCOUNT_ADMIN_ERROR =
Lines changed: 23 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,19 @@
11
import { childAccountFactory } from '@linode/utilities';
2-
import { screen, waitFor } from '@testing-library/react';
3-
import userEvent from '@testing-library/user-event';
2+
import { screen } from '@testing-library/react';
43
import React from 'react';
54

5+
import { accountRolesFactory } from 'src/factories/accountRoles';
66
import { renderWithTheme } from 'src/utilities/testHelpers';
77

8+
import { NO_ACCOUNT_DELEGATIONS_TEXT } from '../../Shared/constants';
89
import { UserDelegations } from './UserDelegations';
910

10-
const mockChildAccounts = {
11-
data: [
12-
{
13-
company: 'Test Account 1',
14-
euuid: '123',
15-
},
16-
{
17-
company: 'Test Account 2',
18-
euuid: '456',
19-
},
20-
],
21-
};
22-
2311
const queryMocks = vi.hoisted(() => ({
24-
useGetDelegatedChildAccountsForUserQuery: vi.fn().mockReturnValue({}),
2512
useParams: vi.fn().mockReturnValue({}),
2613
useSearch: vi.fn().mockReturnValue({}),
27-
useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}),
14+
useNavigate: vi.fn().mockReturnValue(vi.fn()),
15+
useGetDelegatedChildAccountsForUserQuery: vi.fn().mockReturnValue({}),
16+
useAccountRoles: vi.fn().mockReturnValue({}),
2817
}));
2918

3019
vi.mock('@linode/queries', async () => {
@@ -33,6 +22,7 @@ vi.mock('@linode/queries', async () => {
3322
...actual,
3423
useGetDelegatedChildAccountsForUserQuery:
3524
queryMocks.useGetDelegatedChildAccountsForUserQuery,
25+
useAccountRoles: queryMocks.useAccountRoles,
3626
};
3727
});
3828

@@ -42,16 +32,7 @@ vi.mock('@tanstack/react-router', async () => {
4232
...actual,
4333
useParams: queryMocks.useParams,
4434
useSearch: queryMocks.useSearch,
45-
};
46-
});
47-
48-
vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
49-
const actual = await vi.importActual(
50-
'src/features/IAM/hooks/useIsIAMEnabled'
51-
);
52-
return {
53-
...actual,
54-
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
35+
useNavigate: queryMocks.useNavigate,
5536
};
5637
});
5738

@@ -60,86 +41,49 @@ describe('UserDelegations', () => {
6041
queryMocks.useParams.mockReturnValue({
6142
username: 'test-user',
6243
});
63-
queryMocks.useGetDelegatedChildAccountsForUserQuery.mockReturnValue({
64-
data: mockChildAccounts,
44+
queryMocks.useSearch.mockReturnValue({ query: '' });
45+
queryMocks.useNavigate.mockReturnValue(vi.fn());
46+
// Ensure IAM is considered enabled
47+
queryMocks.useAccountRoles.mockReturnValue({
48+
data: accountRolesFactory.build(),
6549
isLoading: false,
6650
});
67-
queryMocks.useSearch.mockReturnValue({
68-
query: '',
69-
});
70-
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
71-
isIAMDelegationEnabled: true,
72-
});
73-
});
74-
75-
it('renders the correct number of child accounts', () => {
76-
renderWithTheme(<UserDelegations />, {
77-
flags: {
78-
iamDelegation: {
79-
enabled: true,
80-
},
81-
},
82-
});
83-
84-
screen.getByText('Test Account 1');
85-
screen.getByText('Test Account 2');
8651
});
8752

88-
it('shows pagination when there are more than 25 child accounts', () => {
53+
it('should display no roles text if no roles are assigned to user', async () => {
8954
queryMocks.useGetDelegatedChildAccountsForUserQuery.mockReturnValue({
90-
data: { data: childAccountFactory.buildList(30), results: 30 },
55+
data: { data: childAccountFactory.buildList(0), results: 0 },
9156
isLoading: false,
9257
});
9358

9459
renderWithTheme(<UserDelegations />, {
9560
flags: {
61+
iam: { enabled: true },
9662
iamDelegation: {
9763
enabled: true,
9864
},
9965
},
10066
});
101-
102-
const tabelRows = screen.getAllByRole('row');
103-
const paginationRow = screen.getByRole('navigation', {
104-
name: 'pagination navigation',
105-
});
106-
expect(tabelRows).toHaveLength(32); // 30 rows + header row + pagination row
107-
expect(paginationRow).toBeInTheDocument();
67+
expect(screen.getByText('This list is empty')).toBeVisible();
68+
expect(screen.getByText(NO_ACCOUNT_DELEGATIONS_TEXT)).toBeVisible();
10869
});
10970

110-
it('filters child accounts by search', async () => {
71+
it('should display table if user has delegations', async () => {
11172
queryMocks.useGetDelegatedChildAccountsForUserQuery.mockReturnValue({
112-
data: { data: childAccountFactory.buildList(30), results: 30 },
73+
data: { data: childAccountFactory.buildList(2), results: 2 },
74+
11375
isLoading: false,
11476
});
11577

11678
renderWithTheme(<UserDelegations />, {
11779
flags: {
80+
iam: { enabled: true },
11881
iamDelegation: {
11982
enabled: true,
12083
},
12184
},
12285
});
12386

124-
const paginationRow = screen.getByRole('navigation', {
125-
name: 'pagination navigation',
126-
});
127-
128-
screen.getByText('child-account-31');
129-
screen.getByText('child-account-32');
130-
131-
expect(paginationRow).toBeInTheDocument();
132-
133-
const searchInput = screen.getByPlaceholderText('Search');
134-
await userEvent.type(searchInput, 'child-account-31');
135-
136-
screen.getByText('child-account-31');
137-
138-
await waitFor(() => {
139-
expect(screen.queryByText('Child Account 32')).not.toBeInTheDocument();
140-
});
141-
await waitFor(() => {
142-
expect(paginationRow).not.toBeInTheDocument();
143-
});
87+
expect(screen.getByText('Account Delegations')).toBeVisible();
14488
});
14589
});
Lines changed: 29 additions & 143 deletions
Original file line numberDiff line numberDiff line change
@@ -1,165 +1,51 @@
11
import { useGetDelegatedChildAccountsForUserQuery } from '@linode/queries';
2-
import {
3-
CircleProgress,
4-
ErrorState,
5-
Paper,
6-
Stack,
7-
Typography,
8-
} from '@linode/ui';
9-
import { useNavigate, useParams, useSearch } from '@tanstack/react-router';
10-
import * as React from 'react';
2+
import { CircleProgress, ErrorState } from '@linode/ui';
3+
import { useParams } from '@tanstack/react-router';
4+
import React from 'react';
115

12-
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
13-
import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter';
14-
import { Table } from 'src/components/Table';
15-
import { TableBody } from 'src/components/TableBody';
16-
import { TableCell } from 'src/components/TableCell';
17-
import { TableHead } from 'src/components/TableHead';
18-
import { TableRow } from 'src/components/TableRow';
19-
import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty';
20-
import { TableSortCell } from 'src/components/TableSortCell';
21-
import { useIsIAMDelegationEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled';
22-
import { NO_DELEGATED_USERS_TEXT } from 'src/features/IAM/Shared/constants';
23-
import { useOrderV2 } from 'src/hooks/useOrderV2';
24-
import { usePaginationV2 } from 'src/hooks/usePaginationV2';
6+
import { DocumentTitleSegment } from 'src/components/DocumentTitle';
257

26-
import type { Theme } from '@mui/material';
8+
import {
9+
ERROR_STATE_TEXT,
10+
NO_ACCOUNT_DELEGATIONS_TEXT,
11+
} from '../../Shared/constants';
12+
import { NoAssignedRoles } from '../../Shared/NoAssignedRoles/NoAssignedRoles';
13+
import { UserDelegationsTable } from './UserDelegationsTable';
2714

2815
export const UserDelegations = () => {
2916
const { username } = useParams({ from: '/iam/users/$username' });
30-
const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled();
31-
const { company } = useSearch({
32-
from: '/iam/users/$username/delegations',
33-
});
34-
const navigate = useNavigate();
35-
36-
const { handleOrderChange, order, orderBy } = useOrderV2({
37-
initialRoute: {
38-
defaultOrder: {
39-
order: 'asc',
40-
orderBy: 'company',
41-
},
42-
from: '/iam/users/$username/delegations',
43-
},
44-
preferenceKey: 'user-delegations',
45-
});
46-
47-
const pagination = usePaginationV2({
48-
currentRoute: '/iam/users/$username/delegations',
49-
preferenceKey: 'user-delegations',
50-
initialPage: 1,
51-
searchParams: (prev) => ({
52-
...prev,
53-
company: company || undefined,
54-
}),
55-
});
56-
57-
const filter = {
58-
company: {
59-
'+contains': company,
60-
},
61-
['+order']: order,
62-
['+order_by']: orderBy,
63-
};
6417

6518
const {
66-
data: childAccounts,
67-
isFetching: isFetchingChildAccounts,
68-
isLoading: isLoadingChildAccounts,
69-
error: errorChildAccounts,
19+
data: allDelegatedChildAccounts,
20+
isLoading,
21+
error,
7022
} = useGetDelegatedChildAccountsForUserQuery({
71-
params: {
72-
page: pagination.page,
73-
page_size: pagination.pageSize,
74-
},
7523
username,
76-
filter,
7724
});
7825

79-
const handleSearch = (value: string) => {
80-
pagination.handlePageChange(1);
81-
navigate({
82-
to: '/iam/users/$username/delegations',
83-
params: { username },
84-
search: { company: value || undefined },
85-
});
86-
};
87-
88-
if (!isIAMDelegationEnabled) {
89-
return null;
90-
}
26+
const hasDelegatedChildAccounts = allDelegatedChildAccounts
27+
? allDelegatedChildAccounts.data.length > 0
28+
: false;
9129

92-
if (isLoadingChildAccounts) {
30+
if (isLoading) {
9331
return <CircleProgress />;
9432
}
9533

96-
if (errorChildAccounts) {
97-
return <ErrorState errorText={errorChildAccounts[0].reason} />;
34+
if (error) {
35+
return <ErrorState errorText={ERROR_STATE_TEXT} />;
9836
}
9937

10038
return (
101-
<Paper>
102-
<Stack>
103-
<Typography variant="h2">Account Delegations</Typography>
104-
<DebouncedSearchTextField
105-
clearable
106-
debounceTime={250}
107-
hideLabel
108-
isSearching={isFetchingChildAccounts}
109-
label="Search"
110-
onSearch={handleSearch}
111-
placeholder="Search"
112-
sx={{ mt: 3 }}
113-
value={company ?? ''}
39+
<>
40+
<DocumentTitleSegment segment={`${username} - User Delegations`} />
41+
{hasDelegatedChildAccounts ? (
42+
<UserDelegationsTable />
43+
) : (
44+
<NoAssignedRoles
45+
hasAssignNewRoleDrawer={false}
46+
text={NO_ACCOUNT_DELEGATIONS_TEXT}
11447
/>
115-
<Table sx={{ mt: 2 }}>
116-
<TableHead>
117-
<TableRow>
118-
<TableSortCell
119-
active={orderBy === 'company'}
120-
direction={order}
121-
handleClick={handleOrderChange}
122-
label={'company'}
123-
>
124-
Account
125-
</TableSortCell>
126-
</TableRow>
127-
</TableHead>
128-
<TableBody>
129-
{childAccounts?.data.length === 0 && (
130-
<TableRowEmpty colSpan={1} message={NO_DELEGATED_USERS_TEXT} />
131-
)}
132-
{childAccounts?.data?.map((childAccount) => (
133-
<TableRow key={childAccount.euuid}>
134-
<TableCell>{childAccount.company}</TableCell>
135-
</TableRow>
136-
))}
137-
{(childAccounts?.results ?? 0) > pagination.pageSize && (
138-
<TableRow>
139-
<TableCell
140-
colSpan={1}
141-
sx={(theme: Theme) => ({
142-
padding: 0,
143-
'& > div': {
144-
border: 'none',
145-
borderTop: `1px solid ${theme.borderColors.divider}`,
146-
},
147-
})}
148-
>
149-
<PaginationFooter
150-
count={childAccounts?.results ?? 0}
151-
eventCategory="DelegatedChildAccounts"
152-
handlePageChange={pagination.handlePageChange}
153-
handleSizeChange={pagination.handlePageSizeChange}
154-
page={pagination.page}
155-
pageSize={pagination.pageSize}
156-
/>
157-
</TableCell>
158-
</TableRow>
159-
)}
160-
</TableBody>
161-
</Table>
162-
</Stack>
163-
</Paper>
48+
)}
49+
</>
16450
);
16551
};

0 commit comments

Comments
 (0)