diff --git a/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md b/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md new file mode 100644 index 00000000000..5ba4c7ca4d2 --- /dev/null +++ b/packages/manager/.changeset/pr-13299-upcoming-features-1768999121997.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Fix error handling in ChildAccountList component ([#13299](https://github.com/linode/manager/pull/13299)) diff --git a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx index 2281527b795..d0994f51578 100644 --- a/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx +++ b/packages/manager/src/features/Account/SwitchAccounts/ChildAccountList.tsx @@ -60,7 +60,11 @@ export const ChildAccountList = React.memo( }: ChildAccountListProps) => { const { isIAMDelegationEnabled } = useIsIAMDelegationEnabled(); - if (errors.childAccountInfiniteError || errors.allChildAccountsError) { + const hasError = isIAMDelegationEnabled + ? errors.allChildAccountsError + : errors.childAccountInfiniteError; + + if (hasError) { return ( diff --git a/packages/manager/src/features/IAM/Roles/Roles.test.tsx b/packages/manager/src/features/IAM/Roles/Roles.test.tsx index 9f3cb0e7450..252006f05ef 100644 --- a/packages/manager/src/features/IAM/Roles/Roles.test.tsx +++ b/packages/manager/src/features/IAM/Roles/Roles.test.tsx @@ -114,6 +114,7 @@ describe('RolesLanding', () => { renderWithTheme(, { flags: { iamDelegation: { enabled: true }, + iam: { enabled: true }, }, }); expect(screen.getByText(DEFAULT_ROLES_PANEL_TEXT)).toBeInTheDocument(); diff --git a/packages/manager/src/features/IAM/Users/UserDelegations/UserDelegations.test.tsx b/packages/manager/src/features/IAM/Users/UserDelegations/UserDelegations.test.tsx index 83e805ea987..18f608a5c66 100644 --- a/packages/manager/src/features/IAM/Users/UserDelegations/UserDelegations.test.tsx +++ b/packages/manager/src/features/IAM/Users/UserDelegations/UserDelegations.test.tsx @@ -22,6 +22,7 @@ const queryMocks = vi.hoisted(() => ({ useAllGetDelegatedChildAccountsForUserQuery: vi.fn().mockReturnValue({}), useParams: vi.fn().mockReturnValue({}), useSearch: vi.fn().mockReturnValue({}), + useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}), })); vi.mock('@linode/queries', async () => { @@ -42,6 +43,16 @@ vi.mock('@tanstack/react-router', async () => { }; }); +vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => { + const actual = await vi.importActual( + 'src/features/IAM/hooks/useIsIAMEnabled' + ); + return { + ...actual, + useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled, + }; +}); + describe('UserDelegations', () => { beforeEach(() => { queryMocks.useParams.mockReturnValue({ @@ -54,6 +65,9 @@ describe('UserDelegations', () => { queryMocks.useSearch.mockReturnValue({ query: '', }); + queryMocks.useIsIAMDelegationEnabled.mockReturnValue({ + isIAMDelegationEnabled: true, + }); }); it('renders the correct number of child accounts', () => { diff --git a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx index c0228a47fb1..f8beb1d7945 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/UserRow.test.tsx @@ -3,7 +3,6 @@ import { waitFor } from '@testing-library/react'; import React from 'react'; import { accountUserFactory } from 'src/factories/accountUsers'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme, @@ -17,18 +16,35 @@ import { UserRow } from './UserRow'; beforeAll(() => mockMatchMedia()); const queryMocks = vi.hoisted(() => ({ - useFlags: vi.fn().mockReturnValue({}), + useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}), + useProfile: vi.fn().mockReturnValue({}), })); -vi.mock('src/hooks/useFlags', () => { - const actual = vi.importActual('src/hooks/useFlags'); +vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => { + const actual = await vi.importActual( + 'src/features/IAM/hooks/useIsIAMEnabled' + ); return { ...actual, - useFlags: queryMocks.useFlags, + useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled, + }; +}); + +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); + return { + ...actual, + useProfile: queryMocks.useProfile, }; }); describe('UserRow', () => { + beforeEach(() => { + queryMocks.useIsIAMDelegationEnabled.mockReturnValue({ + isIAMDelegationEnabled: true, + }); + }); + it('renders a username and email', async () => { const user = accountUserFactory.build(); @@ -39,24 +55,22 @@ describe('UserRow', () => { expect(getByText(user.username)).toBeVisible(); expect(getByText(user.email)).toBeVisible(); }); + it('renders username, email, and user type for a Child user when isIAMDelegationEnabled flag is enabled', async () => { const user = accountUserFactory.build({ user_type: 'child', }); - server.use( - // Mock the active profile for the child account. - http.get('*/profile', () => { - return HttpResponse.json(profileFactory.build({ user_type: 'child' })); - }) - ); - - queryMocks.useFlags.mockReturnValue({ - iamDelegation: { enabled: true }, + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'child' }), }); const { getByText } = renderWithTheme( - wrapWithTableBody() + wrapWithTableBody(, { + flags: { + iamDelegation: { enabled: true }, + }, + }) ); expect(getByText(user.username)).toBeVisible(); @@ -72,19 +86,16 @@ describe('UserRow', () => { user_type: 'delegate', }); - server.use( - // Mock the active profile for the child account. - http.get('*/profile', () => { - return HttpResponse.json(profileFactory.build({ user_type: 'child' })); - }) - ); - - queryMocks.useFlags.mockReturnValue({ - iamDelegation: { enabled: true }, + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'child' }), }); const { getByText, queryByText } = renderWithTheme( - wrapWithTableBody() + wrapWithTableBody(, { + flags: { + iamDelegation: { enabled: true }, + }, + }) ); expect(getByText(delegateUser.username)).toBeVisible(); @@ -105,13 +116,12 @@ describe('UserRow', () => { expect(getByText('Never')).toBeVisible(); }); + it('renders a timestamp of the last_login if it was successful', async () => { // Because we are unit testing a timestamp, set our timezone to UTC - server.use( - http.get('*/profile', () => { - return HttpResponse.json(profileFactory.build({ timezone: 'utc' })); - }) - ); + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ timezone: 'utc' }), + }); const user = accountUserFactory.build({ last_login: { @@ -128,13 +138,12 @@ describe('UserRow', () => { expect(date).toBeVisible(); }); + it('renders a timestamp and "Failed" of the last_login if it was failed', async () => { // Because we are unit testing a timestamp, set our timezone to UTC - server.use( - http.get('*/profile', () => { - return HttpResponse.json(profileFactory.build({ timezone: 'utc' })); - }) - ); + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ timezone: 'utc' }), + }); const user = accountUserFactory.build({ last_login: { diff --git a/packages/manager/src/features/IAM/Users/UsersTable/Users.test.tsx b/packages/manager/src/features/IAM/Users/UsersTable/Users.test.tsx index 22f53e115c0..c299cee8ae4 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/Users.test.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/Users.test.tsx @@ -13,21 +13,13 @@ beforeAll(() => mockMatchMedia()); const navigate = vi.fn(); const queryMocks = vi.hoisted(() => ({ - useFlags: vi.fn().mockReturnValue({}), useNavigate: vi.fn(() => navigate), useProfile: vi.fn().mockReturnValue({}), useAccountUsers: vi.fn().mockReturnValue({}), useSearch: vi.fn().mockReturnValue({}), + useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}), })); -vi.mock('src/hooks/useFlags', () => { - const actual = vi.importActual('src/hooks/useFlags'); - return { - ...actual, - useFlags: queryMocks.useFlags, - }; -}); - vi.mock('@tanstack/react-router', async () => { const actual = await vi.importActual('@tanstack/react-router'); return { @@ -46,7 +38,23 @@ vi.mock('@linode/queries', async () => { }; }); +vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => { + const actual = await vi.importActual( + 'src/features/IAM/hooks/useIsIAMEnabled' + ); + return { + ...actual, + useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled, + }; +}); + describe('Users', () => { + beforeEach(() => { + queryMocks.useIsIAMDelegationEnabled.mockReturnValue({ + isIAMDelegationEnabled: true, + }); + }); + it('renders only table and search filter if profile is not a child', async () => { const user = accountUserFactory.build(); queryMocks.useAccountUsers.mockReturnValue({ @@ -88,14 +96,14 @@ describe('Users', () => { queryMocks.useProfile.mockReturnValue({ data: profileFactory.build({ user_type: 'child' }), }); - queryMocks.useFlags.mockReturnValue({ - iamDelegation: { enabled: true }, - }); const { getByPlaceholderText, getByLabelText } = renderWithTheme( , { initialRoute: '/iam', + flags: { + iamDelegation: { enabled: true }, + }, } ); diff --git a/packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableHead.test.tsx b/packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableHead.test.tsx index 0c9b62de2cd..6072ffb39c7 100644 --- a/packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableHead.test.tsx +++ b/packages/manager/src/features/IAM/Users/UsersTable/UsersLandingTableHead.test.tsx @@ -2,7 +2,6 @@ import { profileFactory } from '@linode/utilities'; import { waitFor } from '@testing-library/react'; import React from 'react'; -import { http, HttpResponse, server } from 'src/mocks/testServer'; import { mockMatchMedia, renderWithTheme, @@ -18,14 +17,25 @@ import type { Order } from '@linode/utilities'; beforeAll(() => mockMatchMedia()); const queryMocks = vi.hoisted(() => ({ - useFlags: vi.fn().mockReturnValue({}), + useProfile: vi.fn().mockReturnValue({}), + useIsIAMDelegationEnabled: vi.fn().mockReturnValue({}), })); -vi.mock('src/hooks/useFlags', () => { - const actual = vi.importActual('src/hooks/useFlags'); +vi.mock('@linode/queries', async () => { + const actual = await vi.importActual('@linode/queries'); return { ...actual, - useFlags: queryMocks.useFlags, + useProfile: queryMocks.useProfile, + }; +}); + +vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => { + const actual = await vi.importActual( + 'src/features/IAM/hooks/useIsIAMEnabled' + ); + return { + ...actual, + useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled, }; }); @@ -38,20 +48,23 @@ const defaultProps = { }; describe('UsersLandingTableHead', () => { - it('renders User type, Username, Email Address, and Last Login columns for a Child user when isIAMDelegationEnabled flag is enabled', async () => { - server.use( - // Mock the active profile for the child account. - http.get('*/profile', () => { - return HttpResponse.json(profileFactory.build({ user_type: 'child' })); - }) - ); + beforeEach(() => { + queryMocks.useIsIAMDelegationEnabled.mockReturnValue({ + isIAMDelegationEnabled: true, + }); + }); - queryMocks.useFlags.mockReturnValue({ - iamDelegation: { enabled: true }, + it('renders User type, Username, Email Address, and Last Login columns for a Child user when isIAMDelegationEnabled flag is enabled', async () => { + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'child' }), }); const { getByText } = renderWithTheme( - wrapWithTableBody() + wrapWithTableBody(, { + flags: { + iamDelegation: { enabled: true }, + }, + }) ); await waitFor(() => { @@ -63,21 +76,16 @@ describe('UsersLandingTableHead', () => { }); it('does not render User type column when isIAMDelegationEnabled flag is off and logged user is not a child', async () => { - server.use( - // Mock the active profile for the default account. - http.get('*/profile', () => { - return HttpResponse.json( - profileFactory.build({ user_type: 'default' }) - ); - }) - ); - - queryMocks.useFlags.mockReturnValue({ - iamDelegation: { enabled: false }, + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'default' }), }); const { getByText, queryByText } = renderWithTheme( - wrapWithTableBody() + wrapWithTableBody(, { + flags: { + iamDelegation: { enabled: false }, + }, + }) ); expect(queryByText('User Type')).not.toBeInTheDocument(); diff --git a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts index 9802fe6195f..77ac0bb4e56 100644 --- a/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts +++ b/packages/manager/src/features/IAM/hooks/useIsIAMEnabled.ts @@ -79,6 +79,10 @@ export const checkIAMEnabled = async ( */ export const useIsIAMDelegationEnabled = () => { const flags = useFlags(); + const { isIAMEnabled } = useIsIAMEnabled(); - return { isIAMDelegationEnabled: flags.iamDelegation?.enabled ?? false }; + return { + isIAMDelegationEnabled: + (flags.iamDelegation?.enabled && isIAMEnabled) ?? false, + }; };