Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/manager/.changeset/pr-13464-fixed-1772634680494.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Fixed
---

IAM Parent/Child - SwitchAccount Drawer: hide search if no child account ([#13464](https://github.com/linode/manager/pull/13464))
10 changes: 10 additions & 0 deletions packages/manager/src/assets/icons/no-results-state.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
65 changes: 57 additions & 8 deletions packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,31 @@ import { SwitchAccountDrawer } from './SwitchAccountDrawer';

const queryMocks = vi.hoisted(() => ({
useProfile: vi.fn().mockReturnValue({}),
useGetListMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}),
useMyDelegatedChildAccountsQuery: vi.fn().mockReturnValue({}),
useChildAccountsInfiniteQuery: vi.fn().mockReturnValue({}),
useIsIAMDelegationEnabled: vi
.fn()
.mockReturnValue({ isIAMDelegationEnabled: true }),
}));

vi.mock('@linode/queries', async () => {
const actual = await vi.importActual('@linode/queries');
return {
...actual,
useProfile: queryMocks.useProfile,
useGetListMyDelegatedChildAccountsQuery:
queryMocks.useGetListMyDelegatedChildAccountsQuery,
useMyDelegatedChildAccountsQuery:
queryMocks.useMyDelegatedChildAccountsQuery,
useChildAccountsInfiniteQuery: queryMocks.useChildAccountsInfiniteQuery,
};
});

vi.mock('src/features/IAM/hooks/useIsIAMEnabled', async () => {
const actual = await vi.importActual(
'src/features/IAM/hooks/useIsIAMEnabled'
);
return {
...actual,
useIsIAMDelegationEnabled: queryMocks.useIsIAMDelegationEnabled,
};
});

Expand All @@ -29,16 +44,34 @@ const props = {
};

describe('SwitchAccountDrawer', () => {
const accounts = accountFactory.buildList(5, {
company: 'Test Account 1',
});

beforeEach(() => {
queryMocks.useProfile.mockReturnValue({});
queryMocks.useGetListMyDelegatedChildAccountsQuery.mockReturnValue({
data: accountFactory.buildList(5, {
company: 'Test Account 1',
euuid: '123',
}),
queryMocks.useIsIAMDelegationEnabled.mockReturnValue({
isIAMDelegationEnabled: true,
});
queryMocks.useMyDelegatedChildAccountsQuery.mockReturnValue({
data: { data: accounts, results: accounts.length, page: 1, pages: 1 },
isLoading: false,
isRefetching: false,
});
queryMocks.useChildAccountsInfiniteQuery.mockReturnValue({
data: {
pages: [
{ data: accounts, results: accounts.length, page: 1, pages: 1 },
],
pageParams: [],
},
isInitialLoading: false,
isRefetching: false,
isFetchingNextPage: false,
hasNextPage: false,
fetchNextPage: vi.fn(),
refetch: vi.fn(),
});
});

it('should have a title', () => {
Expand Down Expand Up @@ -94,4 +127,20 @@ describe('SwitchAccountDrawer', () => {
expect(props.onClose).toHaveBeenCalledTimes(1);
});
});

it('should display an empty state when no child accounts are found', async () => {
queryMocks.useMyDelegatedChildAccountsQuery.mockReturnValue({
data: { data: [], results: 0, page: 1, pages: 1 },
isLoading: false,
isRefetching: false,
});
const { getByText } = renderWithTheme(<SwitchAccountDrawer {...props} />);

expect(getByText('You don’t have access to other accounts.')).toBeVisible();
expect(
getByText(
'You must be added to a delegation by an account administrator to have access to other accounts.'
)
).toBeVisible();
});
});
235 changes: 133 additions & 102 deletions packages/manager/src/features/Account/SwitchAccountDrawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
useMyDelegatedChildAccountsQuery,
} from '@linode/queries';
import {
Box,
Button,
Drawer,
LinkButton,
Expand All @@ -14,6 +15,7 @@ import {
import React, { useMemo, useState } from 'react';

import ErrorStateCloud from 'src/assets/icons/error-state-cloud.svg';
import NoResultsState from 'src/assets/icons/no-results-state.svg';
import { DebouncedSearchTextField } from 'src/components/DebouncedSearchTextField';
import { useParentChildAuthentication } from 'src/features/Account/SwitchAccounts/useParentChildAuthentication';
import { useSwitchToParentAccount } from 'src/features/Account/SwitchAccounts/useSwitchToParentAccount';
Expand Down Expand Up @@ -217,6 +219,7 @@ export const SwitchAccountDrawer = (props: Props) => {
const hasError = isIAMDelegationEnabled
? delegatedChildAccountsError
: childAccountInfiniteError;

return (
<Drawer onClose={handleClose} open={open} title="Switch Account">
{createTokenErrorReason && (
Expand All @@ -225,116 +228,144 @@ export const SwitchAccountDrawer = (props: Props) => {
{isParentTokenError.length > 0 && (
<Notice text={isParentTokenError[0].reason} variant="error" />
)}
<Typography
sx={(theme) => ({
margin: `${theme.spacingFunction(24)} 0`,
})}
>
Select an account to view and manage its settings and configurations
{isProxyOrDelegateUserType && (
<>
{' or '}
<LinkButton
aria-label="parent-account-link"
disabled={isSubmitting}
onClick={() => {
sendSwitchToParentAccountEvent();
handleSwitchToParentAccount();
}}
>
switch back to your account
</LinkButton>
</>
)}
.
</Typography>

{hasError ? (
<Stack alignItems="center" gap={1} justifyContent="center">
<ErrorStateCloud />
<Typography>Unable to load data.</Typography>
<Typography>
Try again or contact support if the issue persists.
{childAccounts &&
childAccounts.length === 0 &&
!Object.prototype.hasOwnProperty.call(filter, 'company') ? (
<Box alignItems="center" display="flex" flexDirection="column" mt={8}>
<NoResultsState />
<Typography sx={{ mt: 2, mb: 1 }} variant="h2">
You don’t have access to other accounts.
</Typography>
<Typography sx={{ textAlign: 'center', maxWidth: 300 }}>
You must be added to a delegation by an account administrator to
have access to other accounts.
</Typography>
<Button
buttonType="primary"
onClick={() => refetchFn()}
sx={(theme) => ({
marginTop: theme.spacingFunction(16),
})}
buttonType="outlined"
onClick={handleClose}
sx={{ mt: 4, alignSelf: 'flex-end' }}
>
Try again
Close
</Button>
</Stack>
</Box>
) : (
<>
<DebouncedSearchTextField
clearable
debounceTime={250}
hideLabel
key={`switch-search-${searchQuery}`}
label="Search"
onSearch={handleSearchQueryChange}
placeholder="Search"
sx={{ marginBottom: theme.spacingFunction(12) }}
value={searchQuery}
/>
{searchQuery &&
childAccounts &&
childAccounts.length === 0 &&
!isLoading && (
<Typography
sx={{
fontStyle: 'italic',
marginTop: theme.spacingFunction(6),
}}
>
No search results
</Typography>
<Typography
sx={(theme) => ({
margin: `${theme.spacingFunction(24)} 0`,
})}
>
Select an account to view and manage its settings and configurations
{isProxyOrDelegateUserType && (
<>
{' or '}
<LinkButton
aria-label="parent-account-link"
disabled={isSubmitting}
onClick={() => {
sendSwitchToParentAccountEvent();
handleSwitchToParentAccount();
}}
>
switch back to your account
</LinkButton>
</>
)}
.
</Typography>

{isIAMDelegationEnabled && (
<ChildAccountsTable
childAccounts={childAccounts}
currentTokenWithBearer={
isProxyOrDelegateUserType
? currentParentTokenWithBearer
: currentTokenWithBearer
}
filter={filter}
isLoading={isLoading}
isSwitchingChildAccounts={isSwitchingChildAccounts}
onClose={onClose}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onSwitchAccount={handleSwitchToChildAccount}
page={page}
pageSize={pageSize}
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
totalResults={delegatedChildAccounts?.results || 0}
userType={userType}
/>
)}
{!isIAMDelegationEnabled && (
<ChildAccountList
childAccounts={childAccounts}
currentTokenWithBearer={
isProxyOrDelegateUserType
? currentParentTokenWithBearer
: currentTokenWithBearer
}
fetchNextPage={fetchNextPage}
filter={filter}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isSwitchingChildAccounts={isSwitchingChildAccounts}
onClose={onClose}
onSwitchAccount={handleSwitchToChildAccount}
refetchFn={refetchFn}
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
userType={userType}
/>
{hasError ? (
<Stack alignItems="center" gap={1} justifyContent="center">
<ErrorStateCloud />
<Typography>Unable to load data.</Typography>
<Typography>
Try again or contact support if the issue persists.
</Typography>
<Button
buttonType="primary"
onClick={() => refetchFn()}
sx={(theme) => ({
marginTop: theme.spacingFunction(16),
})}
>
Try again
</Button>
</Stack>
) : (
<>
{((childAccounts && childAccounts.length !== 0) ||
searchQuery) && (
<DebouncedSearchTextField
clearable
debounceTime={250}
hideLabel
key={`switch-search-${searchQuery}`}
label="Search"
loading={isLoading}
onSearch={handleSearchQueryChange}
placeholder="Search"
sx={{ marginBottom: theme.spacingFunction(12) }}
value={searchQuery}
/>
)}
{isIAMDelegationEnabled &&
searchQuery &&
childAccounts &&
childAccounts.length === 0 &&
!isLoading && (
<Typography
sx={{
fontStyle: 'italic',
marginTop: theme.spacingFunction(6),
}}
>
No search results
</Typography>
)}

{isIAMDelegationEnabled && (
<ChildAccountsTable
childAccounts={childAccounts}
currentTokenWithBearer={
isProxyOrDelegateUserType
? currentParentTokenWithBearer
: currentTokenWithBearer
}
isLoading={isLoading}
isSwitchingChildAccounts={isSwitchingChildAccounts}
onClose={onClose}
onPageChange={handlePageChange}
onPageSizeChange={handlePageSizeChange}
onSwitchAccount={handleSwitchToChildAccount}
page={page}
pageSize={pageSize}
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
totalResults={delegatedChildAccounts?.results || 0}
userType={userType}
/>
)}
{!isIAMDelegationEnabled && (
<ChildAccountList
childAccounts={childAccounts}
currentTokenWithBearer={
isProxyOrDelegateUserType
? currentParentTokenWithBearer
: currentTokenWithBearer
}
fetchNextPage={fetchNextPage}
filter={filter}
hasNextPage={hasNextPage}
isFetchingNextPage={isFetchingNextPage}
isLoading={isLoading}
isSwitchingChildAccounts={isSwitchingChildAccounts}
onClose={onClose}
onSwitchAccount={handleSwitchToChildAccount}
refetchFn={refetchFn}
setIsSwitchingChildAccounts={setIsSwitchingChildAccounts}
userType={userType}
/>
)}
</>
)}
</>
)}
Expand Down
Loading