Skip to content

Commit

Permalink
upcoming: [M3-7429] - Add Parent/Proxy 'Switch Account' button and dr…
Browse files Browse the repository at this point in the history
…awer to user profile dropdown menu (#10031)

* Add switch account button and basic drawer

* List child accounts in drawer

* Use render function

* Correct comments so as not to confuse myself

* Correct the button text

* Clean up and make styling more closely match mocks

* Make a component out of Switch Account button; add icon

* More styling and clean up

* Added changeset: Add parent/proxy 'Switch Account' button and drawer to user profile dropdown menu

* Clean up and add WIP unit tests

* Clean up and fix z-index so user menu is focused on drawer close

* Add tests for SwitchAccountDrawer

* Address feedback: use startIcon
  • Loading branch information
mjac0bs authored Jan 8, 2024
1 parent f7f0f28 commit 23ca92f
Show file tree
Hide file tree
Showing 8 changed files with 384 additions and 78 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@linode/manager": Upcoming Features
---

Add parent/proxy 'Switch Account' button and drawer to user profile dropdown menu ([#10031](https://github.com/linode/manager/pull/10031))
3 changes: 3 additions & 0 deletions packages/manager/src/assets/icons/swapSmall.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions packages/manager/src/features/Account/SwitchAccountButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import * as React from 'react';

import SwapIcon from 'src/assets/icons/swapSmall.svg';
import { Button, ButtonProps } from 'src/components/Button/Button';

export const SwitchAccountButton = (props: ButtonProps) => {
return (
<Button startIcon={<SwapIcon />} {...props}>
Switch Account
</Button>
);
};
89 changes: 89 additions & 0 deletions packages/manager/src/features/Account/SwitchAccountDrawer.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { fireEvent, within } from '@testing-library/react';
import * as React from 'react';

import { accountFactory } from 'src/factories/account';
import { accountUserFactory } from 'src/factories/accountUsers';
import { makeResourcePage } from 'src/mocks/serverHandlers';
import { rest, server } from 'src/mocks/testServer';
import { renderWithTheme } from 'src/utilities/testHelpers';

import { SwitchAccountDrawer } from './SwitchAccountDrawer';

const props = {
onClose: vi.fn(),
open: true,
username: 'mock-user',
};

describe('SwitchAccountDrawer', () => {
it('should have a title', () => {
const { getByText } = renderWithTheme(<SwitchAccountDrawer {...props} />);
expect(getByText('Switch Account')).toBeInTheDocument();
});

it('should display helper text about the accounts', () => {
const { getByText } = renderWithTheme(<SwitchAccountDrawer {...props} />);
expect(
getByText(
'Select an account to view and manage its settings and configurations',
{ exact: false }
)
).toBeInTheDocument();
});

it('should include a link to switch back to the parent account if the active user is a proxy user', async () => {
server.use(
rest.get('*/account/users/*', (req, res, ctx) => {
return res(ctx.json(accountUserFactory.build({ user_type: 'proxy' })));
})
);

const { findByLabelText, getByText } = renderWithTheme(
<SwitchAccountDrawer {...props} />
);

expect(
getByText(
'Select an account to view and manage its settings and configurations',
{ exact: false }
)
).toBeInTheDocument();
expect(await findByLabelText('parent-account-link')).toHaveTextContent(
'switch back to your account'
);
});

it('should display a list of child accounts', async () => {
server.use(
rest.get('*/account/child-accounts', (req, res, ctx) => {
return res(
ctx.json(
makeResourcePage(
accountFactory.buildList(5, { company: 'Child Co.' })
)
)
);
})
);

const { findByTestId } = renderWithTheme(
<SwitchAccountDrawer {...props} />
);

const childAccounts = await findByTestId('child-account-list');
expect(
within(childAccounts).getAllByText('Child Co.', { exact: false })
).toHaveLength(5);
});

it('should close when the close icon is clicked', () => {
const { getByLabelText } = renderWithTheme(
<SwitchAccountDrawer {...props} />
);

const closeIconButton = getByLabelText('Close drawer');
fireEvent.click(closeIconButton);

expect(props.onClose).toHaveBeenCalledTimes(1);
});
});
104 changes: 104 additions & 0 deletions packages/manager/src/features/Account/SwitchAccountDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import { Typography, styled } from '@mui/material';
import { AxiosHeaders } from 'axios';
import React from 'react';

import { StyledLinkButton } from 'src/components/Button/StyledLinkButton';
import { CircleProgress } from 'src/components/CircleProgress';
import { Drawer } from 'src/components/Drawer';
import { Notice } from 'src/components/Notice/Notice';
import { Stack } from 'src/components/Stack';
import { useFlags } from 'src/hooks/useFlags';
import { useChildAccounts } from 'src/queries/account';
import { useAccountUser } from 'src/queries/accountUsers';
import { useProfile } from 'src/queries/profile';
import { authentication } from 'src/utilities/storage';

interface Props {
onClose: () => void;
open: boolean;
username: string;
}

export const SwitchAccountDrawer = (props: Props) => {
const { onClose, open } = props;

const flags = useFlags();

const handleClose = () => {
onClose();
};

const { data: profile } = useProfile();
const { data: user } = useAccountUser(profile?.username ?? '');

// From proxy accounts, make a request on behalf of the parent account to fetch child accounts.
const headers =
flags.parentChildAccountAccess && user?.user_type === 'proxy'
? new AxiosHeaders({ Authorization: authentication.token.get() }) // TODO: Parent/Child - M3-7430: replace this token with the parent token in local storage.
: undefined;
const { data: childAccounts, error, isLoading } = useChildAccounts({
headers,
});

const renderChildAccounts = React.useCallback(() => {
if (isLoading) {
return <CircleProgress mini />;
}

if (childAccounts?.results === 0) {
return <Notice variant="info">There are no child accounts.</Notice>;
}

if (error) {
return (
<Notice variant="error">
There was an error loading child accounts.
</Notice>
);
}

return childAccounts?.data.map((childAccount, idx) => (
<StyledChildAccountLinkButton
onClick={() => {
// TODO: Parent/Child - M3-7430
// handleAccountSwitch();
}}
key={`child-account-link-button-${idx}`}
>
{childAccount.company}
</StyledChildAccountLinkButton>
));
}, [childAccounts, error, isLoading]);

return (
<Drawer onClose={handleClose} open={open} title="Switch Account">
<StyledTypography>
Select an account to view and manage its settings and configurations
{user?.user_type === 'proxy' && (
<>
{' '}
or {/* TODO: Parent/Child - M3-7430 */}
<StyledLinkButton
aria-label="parent-account-link"
onClick={() => null}
>
switch back to your account
</StyledLinkButton>
</>
)}
.
</StyledTypography>
<Stack alignItems={'flex-start'} data-testid="child-account-list">
{renderChildAccounts()}
</Stack>
</Drawer>
);
};

const StyledTypography = styled(Typography)(({ theme }) => ({
margin: `${theme.spacing(3)} 0`,
}));

const StyledChildAccountLinkButton = styled(StyledLinkButton)(({ theme }) => ({
marginBottom: theme.spacing(2),
}));
Loading

0 comments on commit 23ca92f

Please sign in to comment.