Skip to content

Commit 32c4c5e

Browse files
upcoming: [UIE-9461] IAM Parent/Child - server side filter on Switch Account Drawer (#13318)
* upcoming: [UIE-9461] IAM Parent/Child - server side filter on Switch Account Drawer * Added changeset: IAM Parent/Child - Enable server side filters on Switch Account drawer * add infinite query * replace infinite scroll with a table * add ChildAccountsTable component for IAM * update cds-react-components, refactoring * fix pagination * review fix
1 parent 184e7c4 commit 32c4c5e

File tree

11 files changed

+343
-223
lines changed

11 files changed

+343
-223
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,11 +81,13 @@ export const updateChildAccountDelegates = ({
8181

8282
export const getMyDelegatedChildAccounts = ({
8383
params,
84+
filter,
8485
}: GetMyDelegatedChildAccountsParams) =>
8586
Request<Page<Account>>(
8687
setURL(`${BETA_API_ROOT}/iam/delegation/profile/child-accounts`),
8788
setMethod('GET'),
8889
setParams(params),
90+
setXFilter(filter),
8991
);
9092

9193
export const getDelegatedChildAccount = ({ euuid }: { euuid: string }) =>

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ export interface ChildAccountWithDelegates extends ChildAccount {
1717
}
1818

1919
export interface GetMyDelegatedChildAccountsParams {
20+
filter?: Filter;
2021
params?: Params;
2122
}
2223

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@linode/manager": Upcoming Features
3+
---
4+
5+
IAM Parent/Child - Enable server side filters on Switch Account drawer ([#13318](https://github.com/linode/manager/pull/13318))

packages/manager/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +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.19",
48+
"akamai-cds-react-components": "0.1.0",
4949
"algoliasearch": "^4.14.3",
5050
"axios": "~1.12.0",
5151
"braintree-web": "^3.92.2",

packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@ import { SwitchAccountDrawer } from './SwitchAccountDrawer';
99

1010
const queryMocks = vi.hoisted(() => ({
1111
useProfile: vi.fn().mockReturnValue({}),
12-
useAllListMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}),
12+
useGetListMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}),
1313
}));
1414

1515
vi.mock('@linode/queries', async () => {
1616
const actual = await vi.importActual('@linode/queries');
1717
return {
1818
...actual,
1919
useProfile: queryMocks.useProfile,
20-
useAllListMyDelegatedChildAccountsQuery:
21-
queryMocks.useAllListMyDelegatedChildAccountsQuery,
20+
useGetListMyDelegatedChildAccountsQuery:
21+
queryMocks.useGetListMyDelegatedChildAccountsQuery,
2222
};
2323
});
2424

@@ -31,7 +31,7 @@ const props = {
3131
describe('SwitchAccountDrawer', () => {
3232
beforeEach(() => {
3333
queryMocks.useProfile.mockReturnValue({});
34-
queryMocks.useAllListMyDelegatedChildAccountsQuery.mockReturnValue({
34+
queryMocks.useGetListMyDelegatedChildAccountsQuery.mockReturnValue({
3535
data: accountFactory.buildList(5, {
3636
company: 'Test Account 1',
3737
euuid: '123',

packages/manager/src/features/Account/SwitchAccountDrawer.tsx

Lines changed: 142 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import {
2-
useAllListMyDelegatedChildAccountsQuery,
32
useChildAccountsInfiniteQuery,
3+
useMyDelegatedChildAccountsQuery,
44
} from '@linode/queries';
5-
import { Drawer, LinkButton, Notice, Typography } from '@linode/ui';
5+
import {
6+
Button,
7+
Drawer,
8+
LinkButton,
9+
Notice,
10+
Stack,
11+
Typography,
12+
useTheme,
13+
} from '@linode/ui';
614
import React, { useMemo, useState } from 'react';
715

16+
import ErrorStateCloud from 'src/assets/icons/error-state-cloud.svg';
817
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
918
import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication';
1019
import { useSwitchToParentAccount } from 'src/features/Account/SwitchAccounts/useSwitchToParentAccount';
@@ -14,6 +23,7 @@ import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEv
1423
import { getStorage, storage } from 'src/utilities/storage';
1524

1625
import { ChildAccountList } from './SwitchAccounts/ChildAccountList';
26+
import { ChildAccountsTable } from './SwitchAccounts/ChildAccountsTable';
1727
import { updateParentTokenInLocalStorage } from './SwitchAccounts/utils';
1828

1929
import type { APIError, Filter, UserType } from '@linode/api-v4';
@@ -34,10 +44,13 @@ interface HandleSwitchToChildAccountProps {
3444

3545
export const SwitchAccountDrawer = (props: Props) => {
3646
const { onClose, open, userType } = props;
47+
const theme = useTheme();
3748
const [isParentTokenError, setIsParentTokenError] = React.useState<
3849
APIError[]
3950
>([]);
4051
const [searchQuery, setSearchQuery] = React.useState<string>('');
52+
const [page, setPage] = useState(1);
53+
const [pageSize, setPageSize] = useState(25);
4154
const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled();
4255
const isParentUserType = userType === 'parent';
4356
const isProxyUserType = userType === 'proxy';
@@ -94,20 +107,20 @@ export const SwitchAccountDrawer = (props: Props) => {
94107
);
95108

96109
const {
97-
data: allChildAccounts,
98-
error: allChildAccountsError,
99-
isLoading: allChildAccountsLoading,
100-
isRefetching: allChildAccountsIsRefetching,
101-
refetch: refetchAllChildAccounts,
102-
} = useAllListMyDelegatedChildAccountsQuery({
103-
params: {},
110+
data: delegatedChildAccounts,
111+
error: delegatedChildAccountsError,
112+
isLoading: delegatedChildAccountsLoading,
113+
isRefetching: delegatedChildAccountsIsRefetching,
114+
refetch: refetchDelegatedChildAccounts,
115+
} = useMyDelegatedChildAccountsQuery({
116+
params: {
117+
page,
118+
page_size: pageSize,
119+
},
120+
filter,
104121
enabled: isIAMDelegationEnabled && isParentUserType,
105122
});
106123

107-
const refetchFn = isIAMDelegationEnabled
108-
? refetchAllChildAccounts
109-
: refetchChildAccounts;
110-
111124
const handleSwitchToChildAccount = React.useCallback(
112125
async ({
113126
currentTokenWithBearer,
@@ -147,35 +160,57 @@ export const SwitchAccountDrawer = (props: Props) => {
147160
});
148161
onClose(event);
149162
location.reload();
150-
} catch (error) {
163+
} catch {
151164
// Error is handled by createTokenError.
152165
}
153166
},
154-
[createToken, updateCurrentToken, revokeToken]
167+
[createToken, isProxyUserType, updateCurrentToken, revokeToken]
155168
);
156169

157170
const [isSwitchingChildAccounts, setIsSwitchingChildAccounts] =
158171
useState<boolean>(false);
159172

173+
const isLoading =
174+
isInitialLoading ||
175+
isSubmitting ||
176+
isSwitchingChildAccounts ||
177+
isRefetching ||
178+
delegatedChildAccountsLoading ||
179+
delegatedChildAccountsIsRefetching;
180+
181+
const refetchFn = isIAMDelegationEnabled
182+
? refetchDelegatedChildAccounts
183+
: refetchChildAccounts;
160184
const handleClose = () => {
161185
setIsSwitchingChildAccounts(false);
186+
setSearchQuery('');
162187
onClose();
163188
};
164189

165190
const childAccounts = useMemo(() => {
166191
if (isIAMDelegationEnabled) {
167-
if (searchQuery && allChildAccounts) {
168-
// Client-side filter: match company field with searchQuery (case-insensitive, contains)
169-
const normalizedQuery = searchQuery.toLowerCase();
170-
return allChildAccounts.filter((account) =>
171-
account.company?.toLowerCase().includes(normalizedQuery)
172-
);
173-
}
174-
return allChildAccounts;
192+
return delegatedChildAccounts?.data || [];
175193
}
176194
return data?.pages.flatMap((page) => page.data);
177-
}, [isIAMDelegationEnabled, searchQuery, allChildAccounts, data]);
195+
}, [isIAMDelegationEnabled, delegatedChildAccounts, data]);
196+
197+
const handlePageChange = (newPage: number) => {
198+
setPage(newPage);
199+
};
178200

201+
const handlePageSizeChange = (newPageSize: number) => {
202+
setPageSize(newPageSize);
203+
setPage(1); // Reset to first page when page size changes
204+
};
205+
206+
const handleSearchQueryChange = (query: string) => {
207+
setSearchQuery(query);
208+
setPage(1); // Reset to first page when search query changes
209+
};
210+
211+
const hasError = isIAMDelegationEnabled
212+
? delegatedChildAccountsError
213+
: childAccountInfiniteError;
179214
return (
180215
<Drawer onClose={handleClose} open={open} title="Switch Account">
181216
{createTokenErrorReason && (
@@ -207,69 +242,95 @@ export const SwitchAccountDrawer = (props: Props) => {
207242
)}
208243
.
209244
</Typography>
210-
{isIAMDelegationEnabled &&
211-
allChildAccounts &&
212-
allChildAccounts.length !== 0 && (
213-
<>
214-
<DebouncedSearchTextField
215-
clearable
216-
debounceTime={250}
217-
hideLabel
218-
label="Search"
219-
onSearch={setSearchQuery}
220-
placeholder="Search"
221-
sx={{ marginBottom: 3 }}
222-
value={searchQuery}
223-
/>
224-
{searchQuery && childAccounts && childAccounts.length === 0 && (
225-
<Typography sx={{ fontStyle: 'italic' }}>
245+
246+
{hasError && (
247+
<Stack alignItems="center" gap={1} justifyContent="center">
248+
<ErrorStateCloud />
249+
<Typography>Unable to load data.</Typography>
250+
<Typography>
251+
Try again or contact support if the issue persists.
252+
</Typography>
253+
<Button
254+
buttonType="primary"
255+
onClick={() => refetchFn()}
256+
sx={(theme) => ({
257+
marginTop: theme.spacingFunction(16),
258+
})}
259+
>
260+
Try again
261+
</Button>
262+
</Stack>
263+
)}
264+
{!hasError && (
265+
<>
266+
<DebouncedSearchTextField
267+
clearable
268+
debounceTime={250}
269+
hideLabel
270+
key={`switch-search-${searchQuery}`}
271+
label="Search"
272+
onSearch={handleSearchQueryChange}
273+
placeholder="Search"
274+
sx={{ marginBottom: theme.spacingFunction(12) }}
275+
value={searchQuery}
276+
/>
277+
{searchQuery &&
278+
childAccounts &&
279+
childAccounts.length === 0 &&
280+
!isLoading && (
281+
<Typography
282+
sx={{
283+
fontStyle: 'italic',
284+
marginTop: theme.spacingFunction(6),
285+
}}
286+
>
226287
No search results
227288
</Typography>
228289
)}
229-
</>
230-
)}
290+
</>
291+
)}
292+
{isIAMDelegationEnabled && (
293+
<ChildAccountsTable
294+
childAccounts={childAccounts}
295+
currentTokenWithBearer={
296+
isProxyOrDelegateUserType
297+
? currentParentTokenWithBearer
298+
: currentTokenWithBearer
299+
}
300+
isLoading={isLoading}
301+
isSwitchingChildAccounts={isSwitchingChildAccounts}
302+
onClose={onClose}
303+
onPageChange={handlePageChange}
304+
onPageSizeChange={handlePageSizeChange}
305+
onSwitchAccount={handleSwitchToChildAccount}
306+
page={page}
307+
pageSize={pageSize}
308+
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
309+
totalResults={delegatedChildAccounts?.results || 0}
310+
userType={userType}
311+
/>
312+
)}
231313
{!isIAMDelegationEnabled && (
232-
<DebouncedSearchTextField
233-
clearable
234-
debounceTime={250}
235-
hideLabel
236-
label="Search"
237-
onSearch={setSearchQuery}
238-
placeholder="Search"
239-
sx={{ marginBottom: 3 }}
240-
value={searchQuery}
314+
<ChildAccountList
315+
childAccounts={childAccounts}
316+
currentTokenWithBearer={
317+
isProxyOrDelegateUserType
318+
? currentParentTokenWithBearer
319+
: currentTokenWithBearer
320+
}
321+
fetchNextPage={fetchNextPage}
322+
filter={filter}
323+
hasNextPage={hasNextPage}
324+
isFetchingNextPage={isFetchingNextPage}
325+
isLoading={isLoading}
326+
isSwitchingChildAccounts={isSwitchingChildAccounts}
327+
onClose={onClose}
328+
onSwitchAccount={handleSwitchToChildAccount}
329+
refetchFn={refetchFn}
330+
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
331+
userType={userType}
241332
/>
242333
)}
243-
<ChildAccountList
244-
childAccounts={childAccounts}
245-
currentTokenWithBearer={
246-
isProxyOrDelegateUserType
247-
? currentParentTokenWithBearer
248-
: currentTokenWithBearer
249-
}
250-
errors={{
251-
childAccountInfiniteError,
252-
allChildAccountsError,
253-
}}
254-
fetchNextPage={fetchNextPage}
255-
filter={filter}
256-
hasNextPage={hasNextPage}
257-
isFetchingNextPage={isFetchingNextPage}
258-
isLoading={
259-
isInitialLoading ||
260-
isSubmitting ||
261-
isSwitchingChildAccounts ||
262-
isRefetching ||
263-
allChildAccountsLoading ||
264-
allChildAccountsIsRefetching
265-
}
266-
isSwitchingChildAccounts={isSwitchingChildAccounts}
267-
onClose={onClose}
268-
onSwitchAccount={handleSwitchToChildAccount}
269-
refetchFn={refetchFn}
270-
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
271-
userType={userType}
272-
/>
273334
</Drawer>
274335
);
275336
};

packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,6 @@ const props: ChildAccountListProps = {
2626
onClose: vi.fn(),
2727
onSwitchAccount: vi.fn(),
2828
userType: undefined,
29-
errors: {
30-
childAccountInfiniteError: false,
31-
allChildAccountsError: null,
32-
},
3329
fetchNextPage: vi.fn(),
3430
filter: {},
3531
hasNextPage: false,

0 commit comments

Comments
 (0)