diff --git a/packages/api-v4/src/iam/delegation.ts b/packages/api-v4/src/iam/delegation.ts index 53046b7e501..8f6b736a819 100644 --- a/packages/api-v4/src/iam/delegation.ts +++ b/packages/api-v4/src/iam/delegation.ts @@ -24,18 +24,22 @@ import type { IamUserRoles } from './types'; export const getChildAccountsIam = ({ params, users, -}: GetChildAccountsIamParams) => - users + filter, +}: GetChildAccountsIamParams) => { + return users ? Request>( setURL(`${BETA_API_ROOT}/iam/delegation/child-accounts?users=true`), setMethod('GET'), - setParams({ ...params }), + setParams(params), + setXFilter(filter), ) : Request>( setURL(`${BETA_API_ROOT}/iam/delegation/child-accounts`), setMethod('GET'), - setParams({ ...params }), + setParams(params), + setXFilter(filter), ); +}; export const getDelegatedChildAccountsForUser = ({ username, diff --git a/packages/api-v4/src/iam/delegation.types.ts b/packages/api-v4/src/iam/delegation.types.ts index 185078ebdfd..08d884733e9 100644 --- a/packages/api-v4/src/iam/delegation.types.ts +++ b/packages/api-v4/src/iam/delegation.types.ts @@ -6,6 +6,8 @@ export interface ChildAccount { } export interface GetChildAccountsIamParams { + enabled?: boolean; + filter?: Filter; params?: Params; users?: boolean; } diff --git a/packages/manager/.changeset/pr-13342-changed-1770382518204.md b/packages/manager/.changeset/pr-13342-changed-1770382518204.md new file mode 100644 index 00000000000..d4e731d94e4 --- /dev/null +++ b/packages/manager/.changeset/pr-13342-changed-1770382518204.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Changed +--- + +IAM Parent/Child - Enable server side filters, pagination and search on Child Delegations ([#13342](https://github.com/linode/manager/pull/13342)) diff --git a/packages/manager/src/features/IAM/Delegations/AccountDelegations.test.tsx b/packages/manager/src/features/IAM/Delegations/AccountDelegations.test.tsx index 6351aec3157..6827a0013b3 100644 --- a/packages/manager/src/features/IAM/Delegations/AccountDelegations.test.tsx +++ b/packages/manager/src/features/IAM/Delegations/AccountDelegations.test.tsx @@ -10,7 +10,9 @@ beforeAll(() => mockMatchMedia()); const mocks = vi.hoisted(() => ({ mockNavigate: vi.fn(), - mockUseGetAllChildAccountsQuery: vi.fn(), + mockUseGetChildAccountsQuery: vi.fn(), + useParams: vi.fn().mockReturnValue({}), + useSearch: vi.fn().mockReturnValue({}), })); vi.mock('@tanstack/react-router', async () => { @@ -18,6 +20,8 @@ vi.mock('@tanstack/react-router', async () => { return { ...actual, useNavigate: () => mocks.mockNavigate, + useParams: mocks.useParams, + useSearch: mocks.useSearch, }; }); @@ -25,7 +29,7 @@ vi.mock('@linode/queries', async () => { const actual = await vi.importActual('@linode/queries'); return { ...actual, - useGetAllChildAccountsQuery: mocks.mockUseGetAllChildAccountsQuery, + useGetChildAccountsQuery: mocks.mockUseGetChildAccountsQuery, }; }); @@ -45,8 +49,9 @@ const mockDelegations = [ describe('AccountDelegations', () => { beforeEach(() => { vi.clearAllMocks(); - mocks.mockUseGetAllChildAccountsQuery.mockReturnValue({ - data: mockDelegations, + mocks.mockUseGetChildAccountsQuery.mockReturnValue({ + data: { data: mockDelegations, results: mockDelegations.length }, + isLoading: false, }); }); @@ -54,6 +59,7 @@ describe('AccountDelegations', () => { renderWithTheme(, { flags: { iamDelegation: { enabled: true }, + iam: { enabled: true }, }, initialRoute: '/iam', }); @@ -72,12 +78,13 @@ describe('AccountDelegations', () => { }); it('should render empty state when no delegations', async () => { - mocks.mockUseGetAllChildAccountsQuery.mockReturnValue({ - data: [], + mocks.mockUseGetChildAccountsQuery.mockReturnValue({ + data: { data: [], results: 0 }, + isLoading: false, }); renderWithTheme(, { - flags: { iamDelegation: { enabled: true } }, + flags: { iamDelegation: { enabled: true }, iam: { enabled: true } }, initialRoute: '/iam', }); diff --git a/packages/manager/src/features/IAM/Delegations/AccountDelegations.tsx b/packages/manager/src/features/IAM/Delegations/AccountDelegations.tsx index 9a52505f00d..2393e8649f3 100644 --- a/packages/manager/src/features/IAM/Delegations/AccountDelegations.tsx +++ b/packages/manager/src/features/IAM/Delegations/AccountDelegations.tsx @@ -1,24 +1,24 @@ -import { useGetAllChildAccountsQuery } from '@linode/queries'; +import { useGetChildAccountsQuery } from '@linode/queries'; import { CircleProgress, Paper, Stack } from '@linode/ui'; import { useMediaQuery, useTheme } from '@mui/material'; import { useNavigate, useSearch } from '@tanstack/react-router'; import React from 'react'; import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField'; -import Paginate from 'src/components/Paginate'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; -import { useFlags } from 'src/hooks/useFlags'; import { useOrderV2 } from 'src/hooks/useOrderV2'; import { usePaginationV2 } from 'src/hooks/usePaginationV2'; +import { useIsIAMDelegationEnabled } from '../hooks/useIsIAMEnabled'; import { AccountDelegationsTable } from './AccountDelegationsTable'; const DELEGATIONS_ROUTE = '/iam/delegations'; export const AccountDelegations = () => { const navigate = useNavigate(); - const flags = useFlags(); - const { query } = useSearch({ + const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); + + const { company } = useSearch({ from: '/iam', }); const theme = useTheme(); @@ -29,73 +29,59 @@ export const AccountDelegations = () => { const numColsLg = isLgDown ? 3 : 2; const numCols = isSmDown ? 2 : numColsLg; - // TODO: UIE-9292 - replace this with API filtering - const { - data: childAccountsWithDelegates, - error, - isLoading, - } = useGetAllChildAccountsQuery({ - params: {}, - users: true, - }); - const { handleOrderChange, order, orderBy } = useOrderV2({ initialRoute: { defaultOrder: { order: 'asc', orderBy: 'company', }, - from: '/iam/delegations', + from: DELEGATIONS_ROUTE, }, - preferenceKey: 'iam-delegations-order', + preferenceKey: 'iam-delegations-pagination', }); - // Apply search filter - const filteredDelegations = React.useMemo(() => { - if (!childAccountsWithDelegates) return []; - if (!query?.trim()) return childAccountsWithDelegates; - - const searchTerm = query.toLowerCase().trim(); - return childAccountsWithDelegates.filter((delegation) => - delegation.company?.toLowerCase().includes(searchTerm) - ); - }, [childAccountsWithDelegates, query]); - - // Sort filtered data globally - const sortedDelegations = React.useMemo(() => { - if (!filteredDelegations.length) return []; - - return [...filteredDelegations].sort((a, b) => { - const aValue = a.company || ''; - const bValue = b.company || ''; - - const comparison = aValue.localeCompare(bValue, undefined, { - numeric: true, - sensitivity: 'base', - }); - - return order === 'asc' ? comparison : -comparison; - }); - }, [filteredDelegations, order]); - const pagination = usePaginationV2({ - currentRoute: '/iam/delegations', - initialPage: 1, + currentRoute: DELEGATIONS_ROUTE, preferenceKey: 'iam-delegations-pagination', - clientSidePaginationData: sortedDelegations, + initialPage: 1, + searchParams: (prev) => ({ + ...prev, + company: company || undefined, + }), + }); + + const filter = { + ['+order']: order, + ['+order_by']: orderBy, + ...(company && { company: { '+contains': company } }), + }; + + const { + data: childAccountsWithDelegates, + isFetching, + isLoading, + error, + } = useGetChildAccountsQuery({ + params: { + page: pagination.page, + page_size: pagination.pageSize, + }, + users: true, + filter, }); const handleSearch = (value: string) => { + pagination.handlePageChange(1); navigate({ to: DELEGATIONS_ROUTE, - search: { query: value || undefined }, + search: { company: value || undefined }, }); }; if (isLoading) { return ; } - if (!flags.iamDelegation?.enabled) { + if (!isIAMDelegationEnabled) { return null; } return ( @@ -115,46 +101,29 @@ export const AccountDelegations = () => { }} debounceTime={250} hideLabel + isSearching={isFetching} label="Search" onSearch={handleSearch} placeholder="Search" - value={query ?? ''} + value={company ?? ''} /> - - + - {({ - count, - data: paginatedData, - handlePageChange, - handlePageSizeChange, - }) => ( - <> - - - - )} - + /> ); }; diff --git a/packages/queries/src/iam/delegation.ts b/packages/queries/src/iam/delegation.ts index 270ad768cbc..efb447e1c77 100644 --- a/packages/queries/src/iam/delegation.ts +++ b/packages/queries/src/iam/delegation.ts @@ -34,28 +34,15 @@ import type { } from '@linode/api-v4'; import type { UseMutationResult, UseQueryResult } from '@tanstack/react-query'; -const getAllDelegationsRequest = ( - _params: Params = {}, - _users: boolean = true, -) => { - return getAll((params) => { - return getChildAccountsIam({ - params: { ...params, ..._params }, - users: _users, - }); - })().then((data) => { - return data.data; - }); -}; - export const delegationQueries = createQueryKeys('delegation', { - childAccounts: ({ params, users }: GetChildAccountsIamParams) => ({ - queryFn: () => getChildAccountsIam({ params, users }), - queryKey: [params, users], - }), - allChildAccounts: (params: Params = {}, users: boolean = true) => ({ - queryFn: () => getAllDelegationsRequest(params, users), - queryKey: ['all', params, users], + childAccounts: ({ + params, + users, + enabled = true, + filter = {}, + }: GetChildAccountsIamParams) => ({ + queryFn: () => getChildAccountsIam({ params, users, enabled, filter }), + queryKey: [params, users, enabled, filter], }), delegatedChildAccountsForUser: ({ username, @@ -107,31 +94,16 @@ export const delegationQueries = createQueryKeys('delegation', { export const useGetChildAccountsQuery = ({ params, users, -}: GetChildAccountsIamParams): UseQueryResult< + filter, + enabled = true, +}: GetChildAccountsIamParams & { enabled?: boolean }): UseQueryResult< ResourcePage, APIError[] > => { return useQuery({ - ...delegationQueries.childAccounts({ params, users }), - }); -}; - -/** - * List ALL child accounts (fetches all data) - gets all child accounts without pagination - * - Purpose: Get ALL child accounts under a parent account for client-side operations - * - Scope: All child accounts for the parent (for sorting, filtering, etc.) - * - Audience: Parent account administrators needing full dataset. - * - CRUD: GET /iam/delegation/child-accounts?users=true (uses getAll utility) - */ -export const useGetAllChildAccountsQuery = ({ - params = {}, - users = true, -}: Partial = {}): UseQueryResult< - (ChildAccount | ChildAccountWithDelegates)[], - APIError[] -> => { - return useQuery({ - ...delegationQueries.allChildAccounts(params, users), + ...delegationQueries.childAccounts({ params, users, filter }), + placeholderData: keepPreviousData, + enabled, }); }; @@ -208,10 +180,6 @@ export const useUpdateChildAccountDelegatesQuery = (): UseMutationResult< queryKey: delegationQueries.childAccounts({ params: {}, users: true }) .queryKey, }); - // Invalidate all child accounts - queryClient.invalidateQueries({ - queryKey: delegationQueries.allChildAccounts._def, - }); // Invalidate all child account delegates queryClient.invalidateQueries({ queryKey: delegationQueries.childAccountDelegates({ euuid }).queryKey,