From 2bb0348cf6cc29bb024f007b3bee20b3c8e346a5 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Fri, 23 Jan 2026 16:41:16 +0100 Subject: [PATCH 1/8] upcoming: [UIE-9461] IAM Parent/Child - server side filter on Switch Account Drawer --- packages/api-v4/src/iam/delegation.ts | 2 + packages/api-v4/src/iam/delegation.types.ts | 1 + .../Account/SwitchAccountDrawer.test.tsx | 8 +-- .../features/Account/SwitchAccountDrawer.tsx | 19 ++++--- packages/queries/src/iam/delegation.ts | 53 +++++-------------- 5 files changed, 29 insertions(+), 54 deletions(-) diff --git a/packages/api-v4/src/iam/delegation.ts b/packages/api-v4/src/iam/delegation.ts index 8f6b736a819..18f439153d3 100644 --- a/packages/api-v4/src/iam/delegation.ts +++ b/packages/api-v4/src/iam/delegation.ts @@ -81,11 +81,13 @@ export const updateChildAccountDelegates = ({ export const getMyDelegatedChildAccounts = ({ params, + filter, }: GetMyDelegatedChildAccountsParams) => Request>( setURL(`${BETA_API_ROOT}/iam/delegation/profile/child-accounts`), setMethod('GET'), setParams(params), + setXFilter(filter), ); export const getDelegatedChildAccount = ({ euuid }: { euuid: string }) => diff --git a/packages/api-v4/src/iam/delegation.types.ts b/packages/api-v4/src/iam/delegation.types.ts index 08d884733e9..953ca57c467 100644 --- a/packages/api-v4/src/iam/delegation.types.ts +++ b/packages/api-v4/src/iam/delegation.types.ts @@ -17,6 +17,7 @@ export interface ChildAccountWithDelegates extends ChildAccount { } export interface GetMyDelegatedChildAccountsParams { + filter?: Filter; params?: Params; } diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx index b4fa59ab5f8..36f4e9fcd7b 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx @@ -9,7 +9,7 @@ import { SwitchAccountDrawer } from './SwitchAccountDrawer'; const queryMocks = vi.hoisted(() => ({ useProfile: vi.fn().mockReturnValue({}), - useAllListMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}), + useGetListMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}), })); vi.mock('@linode/queries', async () => { @@ -17,8 +17,8 @@ vi.mock('@linode/queries', async () => { return { ...actual, useProfile: queryMocks.useProfile, - useAllListMyDelegatedChildAccountsQuery: - queryMocks.useAllListMyDelegatedChildAccountsQuery, + useGetListMyDelegatedChildAccountsQuery: + queryMocks.useGetListMyDelegatedChildAccountsQuery, }; }); @@ -31,7 +31,7 @@ const props = { describe('SwitchAccountDrawer', () => { beforeEach(() => { queryMocks.useProfile.mockReturnValue({}); - queryMocks.useAllListMyDelegatedChildAccountsQuery.mockReturnValue({ + queryMocks.useGetListMyDelegatedChildAccountsQuery.mockReturnValue({ data: accountFactory.buildList(5, { company: 'Test Account 1', euuid: '123', diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index d8be164ba46..31f957e92dd 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -1,6 +1,6 @@ import { - useAllListMyDelegatedChildAccountsQuery, useChildAccountsInfiniteQuery, + useGetMyDelegatedChildAccountsQuery, } from '@linode/queries'; import { Drawer, LinkButton, Notice, Typography } from '@linode/ui'; import React, { useMemo, useState } from 'react'; @@ -99,10 +99,7 @@ export const SwitchAccountDrawer = (props: Props) => { isLoading: allChildAccountsLoading, isRefetching: allChildAccountsIsRefetching, refetch: refetchAllChildAccounts, - } = useAllListMyDelegatedChildAccountsQuery({ - params: {}, - enabled: isIAMDelegationEnabled && isParentUserType, - }); + } = useGetMyDelegatedChildAccountsQuery({}, filter, isIAMDelegationEnabled && isParentUserType); const refetchFn = isIAMDelegationEnabled ? refetchAllChildAccounts @@ -147,7 +144,7 @@ export const SwitchAccountDrawer = (props: Props) => { }); onClose(event); location.reload(); - } catch (error) { + } catch { // Error is handled by createTokenError. } }, @@ -167,11 +164,11 @@ export const SwitchAccountDrawer = (props: Props) => { if (searchQuery && allChildAccounts) { // Client-side filter: match company field with searchQuery (case-insensitive, contains) const normalizedQuery = searchQuery.toLowerCase(); - return allChildAccounts.filter((account) => + return allChildAccounts?.data.filter((account) => account.company?.toLowerCase().includes(normalizedQuery) ); } - return allChildAccounts; + return allChildAccounts?.data; } return data?.pages.flatMap((page) => page.data); }, [isIAMDelegationEnabled, searchQuery, allChildAccounts, data]); @@ -209,7 +206,7 @@ export const SwitchAccountDrawer = (props: Props) => { {isIAMDelegationEnabled && allChildAccounts && - allChildAccounts.length !== 0 && ( + allChildAccounts?.data.length !== 0 && ( <> { } errors={{ childAccountInfiniteError, - allChildAccountsError, + allChildAccountsError: allChildAccountsError?.[0] + ? new Error(allChildAccountsError[0].reason) + : null, }} fetchNextPage={fetchNextPage} filter={filter} diff --git a/packages/queries/src/iam/delegation.ts b/packages/queries/src/iam/delegation.ts index efb447e1c77..51e6c95f2dc 100644 --- a/packages/queries/src/iam/delegation.ts +++ b/packages/queries/src/iam/delegation.ts @@ -23,9 +23,11 @@ import type { APIError, ChildAccount, ChildAccountWithDelegates, + Filter, GetChildAccountDelegatesParams, GetChildAccountsIamParams, GetDelegatedChildAccountsForUserParams, + GetMyDelegatedChildAccountsParams, IamUserRoles, Params, ResourcePage, @@ -61,19 +63,13 @@ export const delegationQueries = createQueryKeys('delegation', { queryFn: () => getChildAccountDelegates({ euuid, params }), queryKey: [euuid, params], }), - myDelegatedChildAccounts: { - contextQueries: { - all: (params: Params) => ({ - queryFn: () => getAllMyDelegatedChildAccounts(params), - queryKey: [params], - }), - paginated: (params: Params) => ({ - queryFn: () => getMyDelegatedChildAccounts({ params }), - queryKey: [params], - }), - }, - queryKey: null, - }, + myDelegatedChildAccounts: ({ + params, + filter = {}, + }: GetMyDelegatedChildAccountsParams) => ({ + queryFn: () => getMyDelegatedChildAccounts({ params, filter }), + queryKey: [params, filter], + }), delegatedChildAccount: (euuid: string) => ({ queryFn: () => getDelegatedChildAccount({ euuid }), queryKey: [euuid], @@ -190,7 +186,7 @@ export const useUpdateChildAccountDelegatesQuery = (): UseMutationResult< }); // Invalidate all my delegated child accounts since delegation may have changed queryClient.invalidateQueries({ - queryKey: delegationQueries.myDelegatedChildAccounts._ctx.all._def, + queryKey: delegationQueries.myDelegatedChildAccounts._def, }); }, }); @@ -205,27 +201,12 @@ export const useUpdateChildAccountDelegatesQuery = (): UseMutationResult< */ export const useGetMyDelegatedChildAccountsQuery = ( params: Params, -): UseQueryResult, APIError[]> => { - return useQuery({ - ...delegationQueries.myDelegatedChildAccounts._ctx.paginated(params), - }); -}; - -/** - * List all my delegated child accounts (fetches all pages of child accounts where user has view_child_account permission) - * - Purpose: Retrieve the full list of child accounts the current caller can manage via delegation, across all pages. - * - Scope: Only child accounts where the caller has an active delegate and required view permission; returns all results, not paginated. - * - Audience: Callers needing the complete set of accessible accounts for the current user. - * - Data: Account[] (limited profile fields) for `GET /iam/delegation/profile/child-accounts` (all pages). - * - Usage: Pass `enabled` to control query activation (e.g., only if IAM Delegation is enabled). - */ -export const useAllListMyDelegatedChildAccountsQuery = ({ - params = {}, + filter: Filter, enabled = true, -}) => { +): UseQueryResult, APIError[]> => { return useQuery({ + ...delegationQueries.myDelegatedChildAccounts({ params, filter }), enabled, - ...delegationQueries.myDelegatedChildAccounts._ctx.all(params), }); }; @@ -297,11 +278,3 @@ export const useUpdateDefaultDelegationAccessQuery = (): UseMutationResult< }, }); }; - -/** - * Fetches all my delegated child accounts for the current user (all pages). - */ -const getAllMyDelegatedChildAccounts = (_params: Params = {}) => - getAll((params) => - getMyDelegatedChildAccounts({ params: { ...params, ..._params } }), - )().then((data) => data.data); From af48dcca1ce2afa2421a974f72efb5fa1f6bf815 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Mon, 26 Jan 2026 15:00:50 +0100 Subject: [PATCH 2/8] Added changeset: IAM Parent/Child - Enable server side filters on Switch Account drawer --- .../.changeset/pr-13318-upcoming-features-1769436050647.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/manager/.changeset/pr-13318-upcoming-features-1769436050647.md diff --git a/packages/manager/.changeset/pr-13318-upcoming-features-1769436050647.md b/packages/manager/.changeset/pr-13318-upcoming-features-1769436050647.md new file mode 100644 index 00000000000..d888868ee63 --- /dev/null +++ b/packages/manager/.changeset/pr-13318-upcoming-features-1769436050647.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +IAM Parent/Child - Enable server side filters on Switch Account drawer ([#13318](https://github.com/linode/manager/pull/13318)) From 1b2eac5d030bf1d07892fdfbb204a21da708e007 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Tue, 27 Jan 2026 13:03:31 +0100 Subject: [PATCH 3/8] add infinite query --- .../features/Account/SwitchAccountDrawer.tsx | 77 ++++++++++--------- packages/queries/src/iam/delegation.ts | 36 +++++---- 2 files changed, 64 insertions(+), 49 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 31f957e92dd..07b8e9b6e53 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -1,6 +1,6 @@ import { useChildAccountsInfiniteQuery, - useGetMyDelegatedChildAccountsQuery, + useMyDelegatedChildAccountsInfiniteQuery, } from '@linode/queries'; import { Drawer, LinkButton, Notice, Typography } from '@linode/ui'; import React, { useMemo, useState } from 'react'; @@ -96,10 +96,17 @@ export const SwitchAccountDrawer = (props: Props) => { const { data: allChildAccounts, error: allChildAccountsError, - isLoading: allChildAccountsLoading, + fetchNextPage: fetchNextPageIAM, + hasNextPage: hasNextPageIAM, + isFetchingNextPage: isFetchingNextPageIAM, + isInitialLoading: allChildAccountsLoading, isRefetching: allChildAccountsIsRefetching, refetch: refetchAllChildAccounts, - } = useGetMyDelegatedChildAccountsQuery({}, filter, isIAMDelegationEnabled && isParentUserType); + } = useMyDelegatedChildAccountsInfiniteQuery({ + params: {}, + filter, + enabled: isIAMDelegationEnabled && isParentUserType, + }); const refetchFn = isIAMDelegationEnabled ? refetchAllChildAccounts @@ -156,22 +163,16 @@ export const SwitchAccountDrawer = (props: Props) => { const handleClose = () => { setIsSwitchingChildAccounts(false); + setSearchQuery(''); onClose(); }; const childAccounts = useMemo(() => { if (isIAMDelegationEnabled) { - if (searchQuery && allChildAccounts) { - // Client-side filter: match company field with searchQuery (case-insensitive, contains) - const normalizedQuery = searchQuery.toLowerCase(); - return allChildAccounts?.data.filter((account) => - account.company?.toLowerCase().includes(normalizedQuery) - ); - } - return allChildAccounts?.data; + return allChildAccounts?.pages.flatMap((page) => page.data); } return data?.pages.flatMap((page) => page.data); - }, [isIAMDelegationEnabled, searchQuery, allChildAccounts, data]); + }, [isIAMDelegationEnabled, allChildAccounts, data]); return ( @@ -204,32 +205,32 @@ export const SwitchAccountDrawer = (props: Props) => { )} . - {isIAMDelegationEnabled && - allChildAccounts && - allChildAccounts?.data.length !== 0 && ( - <> - - {searchQuery && childAccounts && childAccounts.length === 0 && ( - - No search results - - )} - - )} + {isIAMDelegationEnabled && allChildAccounts && ( + <> + + {searchQuery && childAccounts && childAccounts.length === 0 && ( + + No search results + + )} + + )} {!isIAMDelegationEnabled && ( { ? new Error(allChildAccountsError[0].reason) : null, }} - fetchNextPage={fetchNextPage} + fetchNextPage={ + isIAMDelegationEnabled ? fetchNextPageIAM : fetchNextPage + } filter={filter} - hasNextPage={hasNextPage} - isFetchingNextPage={isFetchingNextPage} + hasNextPage={isIAMDelegationEnabled ? hasNextPageIAM : hasNextPage} + isFetchingNextPage={ + isIAMDelegationEnabled ? isFetchingNextPageIAM : isFetchingNextPage + } isLoading={ isInitialLoading || isSubmitting || diff --git a/packages/queries/src/iam/delegation.ts b/packages/queries/src/iam/delegation.ts index 51e6c95f2dc..aa5903efa50 100644 --- a/packages/queries/src/iam/delegation.ts +++ b/packages/queries/src/iam/delegation.ts @@ -23,7 +23,6 @@ import type { APIError, ChildAccount, ChildAccountWithDelegates, - Filter, GetChildAccountDelegatesParams, GetChildAccountsIamParams, GetDelegatedChildAccountsForUserParams, @@ -63,11 +62,15 @@ export const delegationQueries = createQueryKeys('delegation', { queryFn: () => getChildAccountDelegates({ euuid, params }), queryKey: [euuid, params], }), - myDelegatedChildAccounts: ({ + myDelegatedChildAccountsInfinite: ({ params, filter = {}, }: GetMyDelegatedChildAccountsParams) => ({ - queryFn: () => getMyDelegatedChildAccounts({ params, filter }), + queryFn: () => + getMyDelegatedChildAccounts({ + params, + filter, + }), queryKey: [params, filter], }), delegatedChildAccount: (euuid: string) => ({ @@ -186,27 +189,34 @@ export const useUpdateChildAccountDelegatesQuery = (): UseMutationResult< }); // Invalidate all my delegated child accounts since delegation may have changed queryClient.invalidateQueries({ - queryKey: delegationQueries.myDelegatedChildAccounts._def, + queryKey: delegationQueries.myDelegatedChildAccountsInfinite._def, }); }, }); }; /** - * List my delegated child accounts (gets child accounts where user has view_child_account permission). - * - Purpose: Get child accounts that the current authenticated user can manage via delegation. + * List my delegated child accounts with infinite scroll (gets child accounts where user has view_child_account permission). + * - Purpose: Get child accounts that the current authenticated user can manage via delegation with infinite scroll. * - Scope: Only child accounts where the caller has an active delegate and required view permission. - * - Audience: Needing to return accounts the caller can actually access. + * - Audience: Needing to return accounts the caller can actually access with pagination. * - CRUD: GET /iam/delegation/profile/child-accounts */ -export const useGetMyDelegatedChildAccountsQuery = ( - params: Params, - filter: Filter, +export const useMyDelegatedChildAccountsInfiniteQuery = ({ + params = {}, + filter = {}, enabled = true, -): UseQueryResult, APIError[]> => { - return useQuery({ - ...delegationQueries.myDelegatedChildAccounts({ params, filter }), +}) => { + return useInfiniteQuery, APIError[]>({ enabled, + getNextPageParam: ({ page, pages }) => { + if (page === pages) { + return undefined; + } + return page + 1; + }, + initialPageParam: 1, + ...delegationQueries.myDelegatedChildAccountsInfinite({ params, filter }), }); }; From 9f7c2f94e07d32854728ed735d9085c785733585 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Wed, 28 Jan 2026 15:54:08 +0100 Subject: [PATCH 4/8] replace infinite scroll with a table --- .../features/Account/SwitchAccountDrawer.tsx | 116 +++++++++------- .../SwitchAccounts/ChildAccountsTable.tsx | 129 ++++++++++++++++++ packages/queries/src/iam/delegation.ts | 25 ++-- 3 files changed, 202 insertions(+), 68 deletions(-) create mode 100644 packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 07b8e9b6e53..97f22648dcc 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -1,6 +1,6 @@ import { useChildAccountsInfiniteQuery, - useMyDelegatedChildAccountsInfiniteQuery, + useMyDelegatedChildAccountsQuery, } from '@linode/queries'; import { Drawer, LinkButton, Notice, Typography } from '@linode/ui'; import React, { useMemo, useState } from 'react'; @@ -14,6 +14,7 @@ import { sendSwitchToParentAccountEvent } from 'src/utilities/analytics/customEv import { getStorage, storage } from 'src/utilities/storage'; import { ChildAccountList } from './SwitchAccounts/ChildAccountList'; +import { ChildAccountsTable } from './SwitchAccounts/ChildAccountsTable'; import { updateParentTokenInLocalStorage } from './SwitchAccounts/utils'; import type { APIError, Filter, UserType } from '@linode/api-v4'; @@ -96,13 +97,10 @@ export const SwitchAccountDrawer = (props: Props) => { const { data: allChildAccounts, error: allChildAccountsError, - fetchNextPage: fetchNextPageIAM, - hasNextPage: hasNextPageIAM, - isFetchingNextPage: isFetchingNextPageIAM, - isInitialLoading: allChildAccountsLoading, + isLoading: allChildAccountsLoading, isRefetching: allChildAccountsIsRefetching, refetch: refetchAllChildAccounts, - } = useMyDelegatedChildAccountsInfiniteQuery({ + } = useMyDelegatedChildAccountsQuery({ params: {}, filter, enabled: isIAMDelegationEnabled && isParentUserType, @@ -169,7 +167,7 @@ export const SwitchAccountDrawer = (props: Props) => { const childAccounts = useMemo(() => { if (isIAMDelegationEnabled) { - return allChildAccounts?.pages.flatMap((page) => page.data); + return allChildAccounts?.data || []; } return data?.pages.flatMap((page) => page.data); }, [isIAMDelegationEnabled, allChildAccounts, data]); @@ -215,7 +213,7 @@ export const SwitchAccountDrawer = (props: Props) => { label="Search" onSearch={setSearchQuery} placeholder="Search" - sx={{ marginBottom: 3 }} + sx={{ marginBottom: 2 }} value={searchQuery} /> {searchQuery && childAccounts && childAccounts.length === 0 && ( @@ -225,55 +223,67 @@ export const SwitchAccountDrawer = (props: Props) => { )} )} - {!isIAMDelegationEnabled && ( - )} - + } + errors={{ + childAccountInfiniteError, + allChildAccountsError: allChildAccountsError?.[0] + ? new Error(allChildAccountsError[0].reason) + : null, + }} + fetchNextPage={fetchNextPage} + filter={filter} + hasNextPage={hasNextPage} + isFetchingNextPage={isFetchingNextPage} + isLoading={ + isInitialLoading || + isSubmitting || + isSwitchingChildAccounts || + isRefetching || + allChildAccountsLoading || + allChildAccountsIsRefetching + } + isSwitchingChildAccounts={isSwitchingChildAccounts} + onClose={onClose} + onSwitchAccount={handleSwitchToChildAccount} + refetchFn={refetchFn} + setIsSwitchingChildAccounts={setIsSwitchingChildAccounts} + userType={userType} + /> + )} ); }; diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx new file mode 100644 index 00000000000..081b390360e --- /dev/null +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -0,0 +1,129 @@ +import { LinkButton } from '@linode/ui'; +import { + Table, + TableBody, + TableCell, + TableRow, +} from 'akamai-cds-react-components/Table'; +import React from 'react'; + +import { TableRowError } from 'src/components/TableRowError/TableRowError'; +import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; + +import type { Account, Filter, UserType } from '@linode/api-v4'; + +interface ChildAccountsTableProps { + childAccounts?: Account[]; + currentTokenWithBearer?: string; + errors: { + allChildAccountsError: Error | null; + childAccountInfiniteError: boolean; + }; + filter: Filter; + isLoading: boolean; + isSwitchingChildAccounts: boolean; + onClose: () => void; + onSwitchAccount: ({ + currentTokenWithBearer, + euuid, + event, + onClose, + userType, + }: { + currentTokenWithBearer?: string; + euuid: string; + event: React.MouseEvent; + onClose: (e: React.SyntheticEvent) => void; + userType: undefined | UserType; + }) => void; + refetchFn: () => void; + setIsSwitchingChildAccounts: (value: boolean) => void; + userType: undefined | UserType; +} + +export const ChildAccountsTable = (props: ChildAccountsTableProps) => { + const { + childAccounts, + currentTokenWithBearer, + errors, + isLoading, + isSwitchingChildAccounts, + onClose, + onSwitchAccount, + setIsSwitchingChildAccounts, + userType, + } = props; + + // const [page, setPage] = useState(1); + // const [pageSize, setPageSize] = useState(25); + + // const handlePageChange = (newPage: number) => { + // setPage(newPage); + // }; + + // const handlePageSizeChange = (newPageSize: number) => { + // setPageSize(newPageSize); + // setPage(1); // Reset to first page when page size changes + // }; + + // const startIndex = (page - 1) * pageSize; + // const endIndex = startIndex + pageSize; + // const paginatedAccounts = childAccounts?.slice(startIndex, endIndex) || []; + // const totalCount = childAccounts?.length || 0; + + if (isLoading) { + return ; + } + + if (errors.allChildAccountsError) { + return ( + + ); + } + + return ( + <> + + + {childAccounts?.map((childAccount, idx) => ( + + + { + setIsSwitchingChildAccounts(true); + onSwitchAccount({ + currentTokenWithBearer, + euuid: childAccount.euuid, + event, + onClose, + userType, + }); + }} + > + {childAccount.company} + + + + ))} + +
+ {/* {totalCount > pageSize && ( + handlePageChange(newPage)} + onRowsPerPageChange={(event) => + handlePageSizeChange(parseInt(event.target.value, 10)) + } + page={page} + rowsPerPage={pageSize} + rowsPerPageOptions={[25, 50, 100]} + /> + )} */} + + ); +}; diff --git a/packages/queries/src/iam/delegation.ts b/packages/queries/src/iam/delegation.ts index aa5903efa50..69dc8cd9a32 100644 --- a/packages/queries/src/iam/delegation.ts +++ b/packages/queries/src/iam/delegation.ts @@ -62,7 +62,7 @@ export const delegationQueries = createQueryKeys('delegation', { queryFn: () => getChildAccountDelegates({ euuid, params }), queryKey: [euuid, params], }), - myDelegatedChildAccountsInfinite: ({ + myDelegatedChildAccounts: ({ params, filter = {}, }: GetMyDelegatedChildAccountsParams) => ({ @@ -189,34 +189,29 @@ export const useUpdateChildAccountDelegatesQuery = (): UseMutationResult< }); // Invalidate all my delegated child accounts since delegation may have changed queryClient.invalidateQueries({ - queryKey: delegationQueries.myDelegatedChildAccountsInfinite._def, + queryKey: delegationQueries.myDelegatedChildAccounts._def, }); }, }); }; /** - * List my delegated child accounts with infinite scroll (gets child accounts where user has view_child_account permission). - * - Purpose: Get child accounts that the current authenticated user can manage via delegation with infinite scroll. + * List my delegated child accounts (gets child accounts where user has view_child_account permission). + * - Purpose: Get child accounts that the current authenticated user can manage via delegation with pagination. * - Scope: Only child accounts where the caller has an active delegate and required view permission. * - Audience: Needing to return accounts the caller can actually access with pagination. * - CRUD: GET /iam/delegation/profile/child-accounts */ -export const useMyDelegatedChildAccountsInfiniteQuery = ({ +export const useMyDelegatedChildAccountsQuery = ({ params = {}, filter = {}, enabled = true, -}) => { - return useInfiniteQuery, APIError[]>({ +}: GetMyDelegatedChildAccountsParams & { + enabled?: boolean; +}): UseQueryResult, APIError[]> => { + return useQuery({ enabled, - getNextPageParam: ({ page, pages }) => { - if (page === pages) { - return undefined; - } - return page + 1; - }, - initialPageParam: 1, - ...delegationQueries.myDelegatedChildAccountsInfinite({ params, filter }), + ...delegationQueries.myDelegatedChildAccounts({ params, filter }), }); }; From da4e78ebefd18b3ec44816cd56426f784f1b63a9 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Wed, 4 Feb 2026 13:37:35 +0100 Subject: [PATCH 5/8] add ChildAccountsTable component for IAM --- .../features/Account/SwitchAccountDrawer.tsx | 117 ++++++++++++------ .../SwitchAccounts/ChildAccountList.tsx | 38 +----- .../SwitchAccounts/ChildAccountsTable.tsx | 77 +++++------- 3 files changed, 114 insertions(+), 118 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 97f22648dcc..21845803cc4 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -2,9 +2,18 @@ import { useChildAccountsInfiniteQuery, useMyDelegatedChildAccountsQuery, } from '@linode/queries'; -import { Drawer, LinkButton, Notice, Typography } from '@linode/ui'; +import { + Button, + Drawer, + LinkButton, + Notice, + Stack, + Typography, + useTheme, +} from '@linode/ui'; import React, { useMemo, useState } from 'react'; +import ErrorStateCloud from 'src/assets/icons/error-state-cloud.svg'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication'; import { useSwitchToParentAccount } from 'src/features/Account/SwitchAccounts/useSwitchToParentAccount'; @@ -35,10 +44,13 @@ interface HandleSwitchToChildAccountProps { export const SwitchAccountDrawer = (props: Props) => { const { onClose, open, userType } = props; + const theme = useTheme(); const [isParentTokenError, setIsParentTokenError] = React.useState< APIError[] >([]); const [searchQuery, setSearchQuery] = React.useState(''); + const [page, setPage] = useState(1); + const [pageSize, setPageSize] = useState(25); const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); const isParentUserType = userType === 'parent'; const isProxyUserType = userType === 'proxy'; @@ -101,15 +113,14 @@ export const SwitchAccountDrawer = (props: Props) => { isRefetching: allChildAccountsIsRefetching, refetch: refetchAllChildAccounts, } = useMyDelegatedChildAccountsQuery({ - params: {}, + params: { + page, + page_size: pageSize, + }, filter, enabled: isIAMDelegationEnabled && isParentUserType, }); - const refetchFn = isIAMDelegationEnabled - ? refetchAllChildAccounts - : refetchChildAccounts; - const handleSwitchToChildAccount = React.useCallback( async ({ currentTokenWithBearer, @@ -159,6 +170,17 @@ export const SwitchAccountDrawer = (props: Props) => { const [isSwitchingChildAccounts, setIsSwitchingChildAccounts] = useState(false); + const isLoading = + isInitialLoading || + isSubmitting || + isSwitchingChildAccounts || + isRefetching || + allChildAccountsLoading || + allChildAccountsIsRefetching; + + const refetchFn = isIAMDelegationEnabled + ? refetchAllChildAccounts + : refetchChildAccounts; const handleClose = () => { setIsSwitchingChildAccounts(false); setSearchQuery(''); @@ -172,6 +194,15 @@ export const SwitchAccountDrawer = (props: Props) => { return data?.pages.flatMap((page) => page.data); }, [isIAMDelegationEnabled, allChildAccounts, data]); + const handlePageChange = (newPage: number) => { + setPage(newPage); + }; + + const handlePageSizeChange = (newPageSize: number) => { + setPageSize(newPageSize); + setPage(1); // Reset to first page when page size changes + }; + return ( {createTokenErrorReason && ( @@ -203,50 +234,67 @@ export const SwitchAccountDrawer = (props: Props) => { )} . - {isIAMDelegationEnabled && allChildAccounts && ( + + {(childAccountInfiniteError || allChildAccountsError) && ( + + + Unable to load data. + + Try again or contact support if the issue persists. + + + + )} + {!(childAccountInfiniteError || allChildAccountsError) && ( <> - {searchQuery && childAccounts && childAccounts.length === 0 && ( - - No search results - - )} + {searchQuery && + childAccounts && + childAccounts.length === 0 && + !isLoading && ( + + No search results + + )} )} {isIAMDelegationEnabled && ( )} @@ -268,14 +316,7 @@ export const SwitchAccountDrawer = (props: Props) => { filter={filter} hasNextPage={hasNextPage} isFetchingNextPage={isFetchingNextPage} - isLoading={ - isInitialLoading || - isSubmitting || - isSwitchingChildAccounts || - isRefetching || - allChildAccountsLoading || - allChildAccountsIsRefetching - } + isLoading={isLoading} isSwitchingChildAccounts={isSwitchingChildAccounts} onClose={onClose} onSwitchAccount={handleSwitchToChildAccount} diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx index d0994f51578..b2d4b346335 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx @@ -1,16 +1,7 @@ -import { - Box, - Button, - CircleProgress, - LinkButton, - Notice, - Stack, - Typography, -} from '@linode/ui'; +import { Box, CircleProgress, LinkButton, Notice, Stack } from '@linode/ui'; import React from 'react'; import { Waypoint } from 'react-waypoint'; -import ErrorStateCloud from 'src/assets/icons/error-state-cloud.svg'; import { useIsIAMDelegationEnabled } from 'src/features/IAM/hooks/useIsIAMEnabled'; import type { ChildAccount, Filter, UserType } from '@linode/api-v4'; @@ -52,39 +43,12 @@ export const ChildAccountList = React.memo( onClose, onSwitchAccount, userType, - refetchFn, - errors, hasNextPage, fetchNextPage, isFetchingNextPage, }: ChildAccountListProps) => { const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); - const hasError = isIAMDelegationEnabled - ? errors.allChildAccountsError - : errors.childAccountInfiniteError; - - if (hasError) { - return ( - - - Unable to load data. - - Try again or contact support if the issue persists. - - - - ); - } - if (isLoading) { return ( diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 081b390360e..cab9f2bde4d 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -1,4 +1,5 @@ -import { LinkButton } from '@linode/ui'; +import { Box, CircleProgress, LinkButton, useTheme } from '@linode/ui'; +import { Pagination } from 'akamai-cds-react-components'; import { Table, TableBody, @@ -7,22 +8,16 @@ import { } from 'akamai-cds-react-components/Table'; import React from 'react'; -import { TableRowError } from 'src/components/TableRowError/TableRowError'; -import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; - -import type { Account, Filter, UserType } from '@linode/api-v4'; +import type { Account, UserType } from '@linode/api-v4'; interface ChildAccountsTableProps { childAccounts?: Account[]; currentTokenWithBearer?: string; - errors: { - allChildAccountsError: Error | null; - childAccountInfiniteError: boolean; - }; - filter: Filter; isLoading: boolean; isSwitchingChildAccounts: boolean; onClose: () => void; + onPageChange: (page: number) => void; + onPageSizeChange: (pageSize: number) => void; onSwitchAccount: ({ currentTokenWithBearer, euuid, @@ -36,8 +31,10 @@ interface ChildAccountsTableProps { onClose: (e: React.SyntheticEvent) => void; userType: undefined | UserType; }) => void; - refetchFn: () => void; + page: number; + pageSize: number; setIsSwitchingChildAccounts: (value: boolean) => void; + totalResults: number; userType: undefined | UserType; } @@ -45,42 +42,33 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { const { childAccounts, currentTokenWithBearer, - errors, isLoading, isSwitchingChildAccounts, onClose, onSwitchAccount, setIsSwitchingChildAccounts, userType, + page, + pageSize, + totalResults, + onPageChange, + onPageSizeChange, } = props; - // const [page, setPage] = useState(1); - // const [pageSize, setPageSize] = useState(25); - - // const handlePageChange = (newPage: number) => { - // setPage(newPage); - // }; - - // const handlePageSizeChange = (newPageSize: number) => { - // setPageSize(newPageSize); - // setPage(1); // Reset to first page when page size changes - // }; + const theme = useTheme(); + const handlePageChange = (newPage: number) => { + onPageChange(newPage); + }; - // const startIndex = (page - 1) * pageSize; - // const endIndex = startIndex + pageSize; - // const paginatedAccounts = childAccounts?.slice(startIndex, endIndex) || []; - // const totalCount = childAccounts?.length || 0; + const handlePageSizeChange = (newPageSize: number) => { + onPageSizeChange(newPageSize); + }; if (isLoading) { - return ; - } - - if (errors.allChildAccountsError) { return ( - + + + ); } @@ -112,18 +100,21 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { ))} - {/* {totalCount > pageSize && ( + {totalResults > pageSize && ( handlePageChange(newPage)} - onRowsPerPageChange={(event) => - handlePageSizeChange(parseInt(event.target.value, 10)) + count={totalResults} + onPageChange={(e: CustomEvent) => + handlePageChange(Number(e.detail)) } + onPageSizeChange={( + e: CustomEvent<{ page: number; pageSize: number }> + ) => handlePageSizeChange(Number(e.detail.pageSize))} page={page} - rowsPerPage={pageSize} - rowsPerPageOptions={[25, 50, 100]} + pageSize={pageSize} + pageSizes={[25, 50, 75, 100]} + style={{ marginTop: theme.spacingFunction(12) }} /> - )} */} + )} ); }; From f043563268ac440da6eee07f90475c5867208dcc Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Thu, 5 Feb 2026 14:38:16 +0100 Subject: [PATCH 6/8] update cds-react-components, refactoring --- packages/manager/package.json | 2 +- .../features/Account/SwitchAccountDrawer.tsx | 23 ++--- .../SwitchAccounts/ChildAccountList.test.tsx | 4 - .../SwitchAccounts/ChildAccountList.tsx | 4 - .../SwitchAccounts/ChildAccountsTable.tsx | 7 ++ pnpm-lock.yaml | 86 ++++++++----------- 6 files changed, 58 insertions(+), 68 deletions(-) diff --git a/packages/manager/package.json b/packages/manager/package.json index 20510a085fa..0c1ba8ef9d3 100644 --- a/packages/manager/package.json +++ b/packages/manager/package.json @@ -45,7 +45,7 @@ "@tanstack/react-query-devtools": "5.51.24", "@tanstack/react-router": "^1.111.11", "@xterm/xterm": "^5.5.0", - "akamai-cds-react-components": "0.0.1-alpha.19", + "akamai-cds-react-components": "0.1.0", "algoliasearch": "^4.14.3", "axios": "~1.12.0", "braintree-web": "^3.92.2", diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 21845803cc4..8f803b9ce90 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -203,6 +203,9 @@ export const SwitchAccountDrawer = (props: Props) => { setPage(1); // Reset to first page when page size changes }; + const hasError = isIAMDelegationEnabled + ? allChildAccountsError + : childAccountInfiniteError; return ( {createTokenErrorReason && ( @@ -235,7 +238,7 @@ export const SwitchAccountDrawer = (props: Props) => { . - {(childAccountInfiniteError || allChildAccountsError) && ( + {hasError && ( Unable to load data. @@ -253,7 +256,7 @@ export const SwitchAccountDrawer = (props: Props) => { )} - {!(childAccountInfiniteError || allChildAccountsError) && ( + {!hasError && ( <> { {isIAMDelegationEnabled && ( { childAccounts={childAccounts} currentTokenWithBearer={ isProxyOrDelegateUserType - ? currentParentTokenWithBearer - : currentTokenWithBearer + ? currentParentTokenWithBearer + : currentTokenWithBearer } - errors={{ - childAccountInfiniteError, - allChildAccountsError: allChildAccountsError?.[0] - ? new Error(allChildAccountsError[0].reason) - : null, - }} fetchNextPage={fetchNextPage} filter={filter} hasNextPage={hasNextPage} diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx index 4e62477a014..8c4db5fe0da 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.test.tsx @@ -26,10 +26,6 @@ const props: ChildAccountListProps = { onClose: vi.fn(), onSwitchAccount: vi.fn(), userType: undefined, - errors: { - childAccountInfiniteError: false, - allChildAccountsError: null, - }, fetchNextPage: vi.fn(), filter: {}, hasNextPage: false, diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx index b2d4b346335..5b93722145e 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx @@ -9,10 +9,6 @@ import type { ChildAccount, Filter, UserType } from '@linode/api-v4'; export interface ChildAccountListProps { childAccounts: ChildAccount[] | undefined; currentTokenWithBearer: string; - errors: { - allChildAccountsError: Error | null; - childAccountInfiniteError: boolean; - }; fetchNextPage: () => void; filter: Filter; hasNextPage: boolean; diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index cab9f2bde4d..21755f1175a 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -92,6 +92,12 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { userType, }); }} + sx={{ + textOverflow: 'ellipsis', + overflow: 'hidden', + textAlign: 'left', + whiteSpace: 'nowrap', + }} > {childAccount.company} @@ -103,6 +109,7 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { {totalResults > pageSize && ( ) => handlePageChange(Number(e.detail)) } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 68da9ff2003..d86446c627a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -215,8 +215,8 @@ importers: specifier: ^5.5.0 version: 5.5.0 akamai-cds-react-components: - specifier: 0.0.1-alpha.19 - version: 0.0.1-alpha.19(@linode/design-language-system@5.3.2)(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + specifier: 0.1.0 + version: 0.1.0(@linode/design-language-system@5.3.2)(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) algoliasearch: specifier: ^4.14.3 version: 4.24.0 @@ -1823,16 +1823,16 @@ packages: peerDependencies: eslint: ^9 - '@lit-labs/ssr-dom-shim@1.4.0': - resolution: {integrity: sha512-ficsEARKnmmW5njugNYKipTm4SFnbik7CXtoencDZzmzo/dQ+2Q0bgkzJuoJP20Aj0F+izzJjOqsnkd6F/o1bw==} + '@lit-labs/ssr-dom-shim@1.5.1': + resolution: {integrity: sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==} '@lit/react@1.0.8': resolution: {integrity: sha512-p2+YcF+JE67SRX3mMlJ1TKCSTsgyOVdAwd/nxp3NuV1+Cb6MWALbN6nT7Ld4tpmYofcE5kcaSY1YBB9erY+6fw==} peerDependencies: '@types/react': 17 || 18 || 19 - '@lit/reactive-element@2.1.1': - resolution: {integrity: sha512-N+dm5PAYdQ8e6UlywyyrgI2t++wFGXfHx+dSJ1oBrg6FAxUj40jId++EaRm80MKX5JnlH1sBsyZ5h0bcZKemCg==} + '@lit/reactive-element@2.1.2': + resolution: {integrity: sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==} '@lukemorales/query-key-factory@1.3.4': resolution: {integrity: sha512-A3frRDdkmaNNQi6mxIshsDk4chRXWoXa05US8fBo4kci/H+lVmujS6QrwQLLGIkNIRFGjMqp2uKjC4XsLdydRw==} @@ -3340,16 +3340,16 @@ packages: ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - akamai-cds-react-components@0.0.1-alpha.19: - resolution: {integrity: sha512-YNQPeP4i65qKA5bjOb4ti0ciwQOYZqsKlJ9f14g7xeJCHamyqxPuHIGsOTvykhKyFoXLL8+kejDS52H5vLvVug==} + akamai-cds-react-components@0.1.0: + resolution: {integrity: sha512-3j4RUS2gsEvTvGVihG48oxfZ+sFRGM+6zfB04UNIVrxsIa5QLYhA4C6RKqmuEqKlsQhgSeg/XkcKJh4vKmW3Qw==} peerDependencies: react: ^18.0.0 react-dom: ^18.0.0 - akamai-cds-web-components@0.0.1-alpha.19: - resolution: {integrity: sha512-8xzQjUtNgpbmINt72deakR96vWDJekfjWMt7DJ3DWumlhXDzekf2hm3Q8++hObjKVx6KgXpUDOGzAdU5yMKeiQ==} + akamai-cds-web-components@0.1.0: + resolution: {integrity: sha512-Ia8JIqr0dV1yuFAaEiP1KfTOZVNLIc0OhWIcnmPb7lnUUOtKfLSB3bbUyYhcm0xag+hLCBxo+Ds+axE9ZL55uw==} peerDependencies: - '@linode/design-language-system': ^5.2.0 + '@linode/design-language-system': ^5.3.2 algoliasearch@4.24.0: resolution: {integrity: sha512-bf0QV/9jVejssFBmz2HQLxUadxk574t4iwjCKp5E7NBzwKkrDEhKPISIIjAU/p6K5qDx3qoeh4+26zWN1jmw3g==} @@ -4600,16 +4600,17 @@ packages: glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me glob@8.1.0: resolution: {integrity: sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==} engines: {node: '>=12'} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -5173,14 +5174,14 @@ packages: resolution: {integrity: sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==} engines: {node: '>=18.0.0'} - lit-element@4.2.1: - resolution: {integrity: sha512-WGAWRGzirAgyphK2urmYOV72tlvnxw7YfyLDgQ+OZnM9vQQBQnumQ7jUJe6unEzwGU3ahFOjuz1iz1jjrpCPuw==} + lit-element@4.2.2: + resolution: {integrity: sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==} - lit-html@3.3.1: - resolution: {integrity: sha512-S9hbyDu/vs1qNrithiNyeyv64c9yqiW9l+DBgI18fL+MTvOtWoFR0FWiyq1TxaYef5wNlpEmzlXoBlZEO+WjoA==} + lit-html@3.3.2: + resolution: {integrity: sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==} - lit@3.3.1: - resolution: {integrity: sha512-Ksr/8L3PTapbdXJCk+EJVB78jDodUMaP54gD24W186zGRARvwrsPfS60wae/SSCTCNZVPd1chXqio1qHQmu4NA==} + lit@3.3.2: + resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==} load-tsconfig@0.2.5: resolution: {integrity: sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==} @@ -6879,6 +6880,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -7953,15 +7955,15 @@ snapshots: dependencies: eslint: 9.31.0(jiti@2.4.2) - '@lit-labs/ssr-dom-shim@1.4.0': {} + '@lit-labs/ssr-dom-shim@1.5.1': {} '@lit/react@1.0.8(@types/react@19.1.6)': dependencies: '@types/react': 19.1.6 - '@lit/reactive-element@2.1.1': + '@lit/reactive-element@2.1.2': dependencies: - '@lit-labs/ssr-dom-shim': 1.4.0 + '@lit-labs/ssr-dom-shim': 1.5.1 '@lukemorales/query-key-factory@1.3.4(@tanstack/query-core@5.51.24)(@tanstack/react-query@5.51.24(react@19.1.0))': dependencies: @@ -9407,18 +9409,6 @@ snapshots: tinyrainbow: 3.0.3 vitest: 4.0.10(@types/debug@4.1.12)(@types/node@22.18.1)(@vitest/ui@4.0.10)(jiti@2.4.2)(jsdom@24.1.3)(msw@2.6.5(@types/node@22.18.1)(typescript@5.7.3))(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) - '@vitest/ui@4.0.10(vitest@4.0.16)': - dependencies: - '@vitest/utils': 4.0.10 - fflate: 0.8.2 - flatted: 3.3.3 - pathe: 2.0.3 - sirv: 3.0.2 - tinyglobby: 0.2.15 - tinyrainbow: 3.0.3 - vitest: 4.0.16(@types/node@22.18.1)(@vitest/ui@4.0.10)(jiti@2.4.2)(jsdom@24.1.3)(msw@2.6.5(@types/node@22.18.1)(typescript@5.9.3))(terser@5.36.0)(tsx@4.19.3)(yaml@2.6.1) - optional: true - '@vitest/utils@3.2.4': dependencies: '@vitest/pretty-format': 3.2.4 @@ -9473,20 +9463,20 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 - akamai-cds-react-components@0.0.1-alpha.19(@linode/design-language-system@5.3.2)(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): + akamai-cds-react-components@0.1.0(@linode/design-language-system@5.3.2)(@types/react@19.1.6)(react-dom@19.1.0(react@19.1.0))(react@19.1.0): dependencies: '@lit/react': 1.0.8(@types/react@19.1.6) - akamai-cds-web-components: 0.0.1-alpha.19(@linode/design-language-system@5.3.2) + akamai-cds-web-components: 0.1.0(@linode/design-language-system@5.3.2) react: 19.1.0 react-dom: 19.1.0(react@19.1.0) transitivePeerDependencies: - '@linode/design-language-system' - '@types/react' - akamai-cds-web-components@0.0.1-alpha.19(@linode/design-language-system@5.3.2): + akamai-cds-web-components@0.1.0(@linode/design-language-system@5.3.2): dependencies: '@linode/design-language-system': 5.3.2 - lit: 3.3.1 + lit: 3.3.2 algoliasearch@4.24.0: dependencies: @@ -11588,21 +11578,21 @@ snapshots: rfdc: 1.4.1 wrap-ansi: 9.0.0 - lit-element@4.2.1: + lit-element@4.2.2: dependencies: - '@lit-labs/ssr-dom-shim': 1.4.0 - '@lit/reactive-element': 2.1.1 - lit-html: 3.3.1 + '@lit-labs/ssr-dom-shim': 1.5.1 + '@lit/reactive-element': 2.1.2 + lit-html: 3.3.2 - lit-html@3.3.1: + lit-html@3.3.2: dependencies: '@types/trusted-types': 2.0.7 - lit@3.3.1: + lit@3.3.2: dependencies: - '@lit/reactive-element': 2.1.1 - lit-element: 4.2.1 - lit-html: 3.3.1 + '@lit/reactive-element': 2.1.2 + lit-element: 4.2.2 + lit-html: 3.3.2 load-tsconfig@0.2.5: {} @@ -13467,7 +13457,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 22.18.1 - '@vitest/ui': 4.0.10(vitest@4.0.16) + '@vitest/ui': 4.0.10(vitest@4.0.10) jsdom: 24.1.3 transitivePeerDependencies: - jiti From ca7423594a8de465e06986edcba78c745e32ef91 Mon Sep 17 00:00:00 2001 From: mpolotsk Date: Tue, 10 Feb 2026 13:31:27 +0100 Subject: [PATCH 7/8] fix pagination --- .../src/features/Account/SwitchAccounts/ChildAccountsTable.tsx | 3 ++- packages/queries/src/iam/delegation.ts | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 21755f1175a..7ef82b217b3 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -10,6 +10,7 @@ import React from 'react'; import type { Account, UserType } from '@linode/api-v4'; +const DEFAULT_PAGE_SIZE = 25; interface ChildAccountsTableProps { childAccounts?: Account[]; currentTokenWithBearer?: string; @@ -106,7 +107,7 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { ))} - {totalResults > pageSize && ( + {totalResults > DEFAULT_PAGE_SIZE && ( Date: Fri, 13 Feb 2026 15:06:22 +0100 Subject: [PATCH 8/8] review fix --- .../features/Account/SwitchAccountDrawer.tsx | 33 +++++++++++-------- .../SwitchAccounts/ChildAccountsTable.tsx | 7 ++-- 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx index 8f803b9ce90..d18e6f105f1 100644 --- a/packages/manager/src/features/Account/SwitchAccountDrawer.tsx +++ b/packages/manager/src/features/Account/SwitchAccountDrawer.tsx @@ -107,11 +107,11 @@ export const SwitchAccountDrawer = (props: Props) => { ); const { - data: allChildAccounts, - error: allChildAccountsError, - isLoading: allChildAccountsLoading, - isRefetching: allChildAccountsIsRefetching, - refetch: refetchAllChildAccounts, + data: delegatedChildAccounts, + error: delegatedChildAccountsError, + isLoading: delegatedChildAccountsLoading, + isRefetching: delegatedChildAccountsIsRefetching, + refetch: refetchDelegatedChildAccounts, } = useMyDelegatedChildAccountsQuery({ params: { page, @@ -164,7 +164,7 @@ export const SwitchAccountDrawer = (props: Props) => { // Error is handled by createTokenError. } }, - [createToken, updateCurrentToken, revokeToken] + [createToken, isProxyUserType, updateCurrentToken, revokeToken] ); const [isSwitchingChildAccounts, setIsSwitchingChildAccounts] = @@ -175,11 +175,11 @@ export const SwitchAccountDrawer = (props: Props) => { isSubmitting || isSwitchingChildAccounts || isRefetching || - allChildAccountsLoading || - allChildAccountsIsRefetching; + delegatedChildAccountsLoading || + delegatedChildAccountsIsRefetching; const refetchFn = isIAMDelegationEnabled - ? refetchAllChildAccounts + ? refetchDelegatedChildAccounts : refetchChildAccounts; const handleClose = () => { setIsSwitchingChildAccounts(false); @@ -189,10 +189,10 @@ export const SwitchAccountDrawer = (props: Props) => { const childAccounts = useMemo(() => { if (isIAMDelegationEnabled) { - return allChildAccounts?.data || []; + return delegatedChildAccounts?.data || []; } return data?.pages.flatMap((page) => page.data); - }, [isIAMDelegationEnabled, allChildAccounts, data]); + }, [isIAMDelegationEnabled, delegatedChildAccounts, data]); const handlePageChange = (newPage: number) => { setPage(newPage); @@ -203,8 +203,13 @@ export const SwitchAccountDrawer = (props: Props) => { setPage(1); // Reset to first page when page size changes }; + const handleSearchQueryChange = (query: string) => { + setSearchQuery(query); + setPage(1); // Reset to first page when search query changes + }; + const hasError = isIAMDelegationEnabled - ? allChildAccountsError + ? delegatedChildAccountsError : childAccountInfiniteError; return ( @@ -264,7 +269,7 @@ export const SwitchAccountDrawer = (props: Props) => { hideLabel key={`switch-search-${searchQuery}`} label="Search" - onSearch={setSearchQuery} + onSearch={handleSearchQueryChange} placeholder="Search" sx={{ marginBottom: theme.spacingFunction(12) }} value={searchQuery} @@ -301,7 +306,7 @@ export const SwitchAccountDrawer = (props: Props) => { page={page} pageSize={pageSize} setIsSwitchingChildAccounts={setIsSwitchingChildAccounts} - totalResults={allChildAccounts?.results || 0} + totalResults={delegatedChildAccounts?.results || 0} userType={userType} /> )} diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx index 7ef82b217b3..1f4c316003d 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountsTable.tsx @@ -1,5 +1,5 @@ import { Box, CircleProgress, LinkButton, useTheme } from '@linode/ui'; -import { Pagination } from 'akamai-cds-react-components'; +import { Pagination } from 'akamai-cds-react-components/Pagination'; import { Table, TableBody, @@ -8,9 +8,10 @@ import { } from 'akamai-cds-react-components/Table'; import React from 'react'; +import { MIN_PAGE_SIZE } from 'src/components/PaginationFooter/PaginationFooter.constants'; + import type { Account, UserType } from '@linode/api-v4'; -const DEFAULT_PAGE_SIZE = 25; interface ChildAccountsTableProps { childAccounts?: Account[]; currentTokenWithBearer?: string; @@ -107,7 +108,7 @@ export const ChildAccountsTable = (props: ChildAccountsTableProps) => { ))} - {totalResults > DEFAULT_PAGE_SIZE && ( + {totalResults > MIN_PAGE_SIZE && (