From d11cc593677e3eaa3a6c1a745b9deb7713195793 Mon Sep 17 00:00:00 2001 From: Mariah Jacobs <114685994+mjac0bs@users.noreply.github.com> Date: Mon, 29 Jan 2024 11:51:25 -0700 Subject: [PATCH] upcoming: [M3-7460] - Child View: Users & Grants page (#10076) * Add table and separate out partner users from all users * Display Manage Access button for proxy users; label tables * Rename variable and clean up * Added changeset: Users & Grants: add business partner table to child view * Display correct table cols; update variable names * Adjust styling; properly feature flag table headings * Disable Close Account button for child users * Disable Close Account button for proxy users; add tests * Add unit test for proxy user UserRow * Switch notice to button tooltip * Fix misleadingly passing Button.test.tsx * Update test coverage for Close Account button tooltip * Fix failing e2e test and add coverage for proxy user flow * Use user_type from and update tests * Address feedback: center button with header; update mocks * Address feedback: make table components * Remove bad any type * Address feedback: Add error, loading states to Close Account button * Rely on parent user type, no need for childAccounts * Address feedback: allow unrestricted proxy user to see themselves * Fix typo * Address feedback: filter users more efficiently with array reduce * Make linter happy with sort imports * Revert change to MSW for testing user_type --- ...r-10076-upcoming-features-1705592122151.md | 5 + .../e2e/core/account/user-permissions.spec.ts | 66 ++++++--- .../src/components/Button/Button.test.tsx | 11 +- .../Account/CloseAccountSetting.test.tsx | 89 ++++++++++-- .../features/Account/CloseAccountSetting.tsx | 28 ++-- .../manager/src/features/Account/constants.ts | 5 + .../manager/src/features/Users/UserDetail.tsx | 14 +- .../src/features/Users/UserPermissions.tsx | 3 +- .../src/features/Users/UserRow.test.tsx | 54 +++++-- .../manager/src/features/Users/UserRow.tsx | 26 ++-- .../src/features/Users/UsersActionMenu.tsx | 16 ++- .../src/features/Users/UsersLanding.tsx | 135 ++++++++++++------ .../Users/UsersLandingProxyTableHead.tsx | 42 ++++++ .../features/Users/UsersLandingTableBody.tsx | 46 ++++++ .../features/Users/UsersLandingTableHead.tsx | 60 ++++++++ 15 files changed, 480 insertions(+), 120 deletions(-) create mode 100644 packages/manager/.changeset/pr-10076-upcoming-features-1705592122151.md create mode 100644 packages/manager/src/features/Users/UsersLandingProxyTableHead.tsx create mode 100644 packages/manager/src/features/Users/UsersLandingTableBody.tsx create mode 100644 packages/manager/src/features/Users/UsersLandingTableHead.tsx diff --git a/packages/manager/.changeset/pr-10076-upcoming-features-1705592122151.md b/packages/manager/.changeset/pr-10076-upcoming-features-1705592122151.md new file mode 100644 index 00000000000..237cf8450af --- /dev/null +++ b/packages/manager/.changeset/pr-10076-upcoming-features-1705592122151.md @@ -0,0 +1,5 @@ +--- +"@linode/manager": Upcoming Features +--- + +Users & Grants: add business partner table to child view ([#10076](https://github.com/linode/manager/pull/10076)) diff --git a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts index af05a5638e6..ea9654cc200 100644 --- a/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts +++ b/packages/manager/cypress/e2e/core/account/user-permissions.spec.ts @@ -559,22 +559,30 @@ describe('User permission management', () => { }); }); - it('disables "Read Only" and "None" and defaults to "Read Write" Billing Access for "Proxy" account users with Parent/Child feature flag', () => { - const mockProfile = profileFactory.build({ + /** + * Confirm the Users & Grants and User Permissions pages flow for a child account viewing a proxy user. + * Confirm that "Business partner settings" and "User settings" sections are present on the Users & Grants page. + * Confirm that proxy accounts are listed under "Business partner settings". + * Confirm that clicking the "Manage Access" button navigates to the proxy user's User Permissions page at /account/users/:user/permissions. + * Confirm that no "Profile" tab is present on the proxy user's User Permissions page. + * Confirm that proxy accounts default to "Read Write" Billing Access and have disabled "Read Only" and "None" options. + */ + it('tests the users landing and user permissions flow for a child account viewing a proxy user ', () => { + const mockChildProfile = profileFactory.build({ username: 'proxy-user', + user_type: 'child', }); - const mockActiveUser = accountUserFactory.build({ - username: 'proxy-user', + const mockChildUser = accountUserFactory.build({ restricted: false, - user_type: 'proxy', + user_type: 'child', }); - const mockRestrictedUser = { - ...mockActiveUser, + const mockRestrictedProxyUser = accountUserFactory.build({ restricted: true, + user_type: 'proxy', username: 'restricted-proxy-user', - }; + }); const mockUserGrants = grantsFactory.build({ global: { account_access: 'read_write' }, @@ -586,33 +594,47 @@ describe('User permission management', () => { }).as('getFeatureFlags'); mockGetFeatureFlagClientstream().as('getClientStream'); - mockGetUsers([mockActiveUser, mockRestrictedUser]).as('getUsers'); - mockGetUser(mockActiveUser); - mockGetUserGrants(mockActiveUser.username, mockUserGrants); - mockGetProfile(mockProfile); - mockGetUser(mockRestrictedUser); - mockGetUserGrants(mockRestrictedUser.username, mockUserGrants); + mockGetUsers([mockRestrictedProxyUser]).as('getUsers'); + mockGetUser(mockChildUser); + mockGetUserGrants(mockChildUser.username, mockUserGrants); + mockGetProfile(mockChildProfile); + mockGetUser(mockRestrictedProxyUser); + mockGetUserGrants(mockRestrictedProxyUser.username, mockUserGrants); - // Navigate to Users & Grants page, find mock restricted user, click its "User Permissions" button. + // Navigate to Users & Grants page and confirm "Business partner settings" and "User settings" sections are visible. cy.visitWithLogin('/account/users'); cy.wait('@getUsers'); - cy.findByText(mockRestrictedUser.username) + cy.findByText('Business partner settings').should('be.visible'); + cy.findByText('User settings').should('be.visible'); + + // Find mock restricted proxy user under "Business partner settings", click its "Manage Access" button. + cy.findByLabelText('List of Business Partners') .should('be.visible') - .closest('tr') .within(() => { - ui.button - .findByTitle('User Permissions') + cy.findByText(mockRestrictedProxyUser.username) .should('be.visible') - .should('be.enabled') - .click(); + .closest('tr') + .within(() => { + ui.button + .findByTitle('Manage Access') + .should('be.visible') + .should('be.enabled') + .click(); + }); }); + // Confirm button navigates to the proxy user's User Permissions page at /account/users/:user/permissions. cy.url().should( 'endWith', - `/account/users/${mockRestrictedUser.username}/permissions` + `/account/users/${mockRestrictedProxyUser.username}/permissions` ); cy.wait(['@getClientStream', '@getFeatureFlags']); + cy.findByText('Business Partner Permissions').should('be.visible'); + + // Confirm that no "Profile" tab is present on the proxy user's User Permissions page. + expect(cy.findByText('User Profile').should('not.exist')); + cy.get('[data-qa-global-section]') .should('be.visible') .within(() => { diff --git a/packages/manager/src/components/Button/Button.test.tsx b/packages/manager/src/components/Button/Button.test.tsx index bb3c8bef442..dd5579bb308 100644 --- a/packages/manager/src/components/Button/Button.test.tsx +++ b/packages/manager/src/components/Button/Button.test.tsx @@ -1,4 +1,4 @@ -import { fireEvent, screen } from '@testing-library/react'; +import { fireEvent, screen, waitFor } from '@testing-library/react'; import React from 'react'; import { renderWithTheme } from 'src/utilities/testHelpers'; @@ -44,7 +44,7 @@ describe('Button', () => { it('should display the tooltip if disabled and tooltipText is true', async () => { const { getByTestId } = renderWithTheme( - ); @@ -53,6 +53,11 @@ describe('Button', () => { expect(button).toHaveAttribute('aria-describedby', 'button-tooltip'); fireEvent.mouseOver(button); - await expect(screen.getByText('Test')).toBeInTheDocument(); + + await waitFor(() => { + expect(screen.getByRole('tooltip')).toBeInTheDocument(); + }); + + expect(screen.getByText('Test tooltip')).toBeVisible(); }); }); diff --git a/packages/manager/src/features/Account/CloseAccountSetting.test.tsx b/packages/manager/src/features/Account/CloseAccountSetting.test.tsx index 23e2ac47e9f..de7cab700ec 100644 --- a/packages/manager/src/features/Account/CloseAccountSetting.test.tsx +++ b/packages/manager/src/features/Account/CloseAccountSetting.test.tsx @@ -1,21 +1,25 @@ +import { fireEvent, waitFor } from '@testing-library/react'; import * as React from 'react'; -import { accountFactory } from 'src/factories'; -import { makeResourcePage } from 'src/mocks/serverHandlers'; +import { profileFactory } from 'src/factories'; import { renderWithTheme } from 'src/utilities/testHelpers'; import CloseAccountSetting from './CloseAccountSetting'; +import { + CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, + PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, +} from './constants'; -// Mock the useChildAccounts hook to immediately return the expected data, circumventing the HTTP request and loading state. +// Mock the useProfile hook to immediately return the expected data, circumventing the HTTP request and loading state. const queryMocks = vi.hoisted(() => ({ - useChildAccounts: vi.fn().mockReturnValue({}), + useProfile: vi.fn().mockReturnValue({}), })); -vi.mock('src/queries/account', async () => { - const actual = await vi.importActual('src/queries/account'); +vi.mock('src/queries/profile', async () => { + const actual = await vi.importActual('src/queries/profile'); return { ...actual, - useChildAccounts: queryMocks.useChildAccounts, + useProfile: queryMocks.useProfile, }; }); @@ -34,25 +38,82 @@ describe('Close Account Settings', () => { const button = getByTestId('close-account-button'); const span = button.querySelector('span'); expect(button).toBeInTheDocument(); + expect(button).toBeEnabled(); expect(span).toHaveTextContent('Close Account'); }); - it('should render a disabled Close Account button and helper text when there is at least one child account', () => { - queryMocks.useChildAccounts.mockReturnValue({ - data: makeResourcePage(accountFactory.buildList(1)), + it('should render a disabled Close Account button with tooltip for a parent account user', async () => { + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'parent' }), }); - const { getByTestId, getByText } = renderWithTheme( + const { getByRole, getByTestId, getByText } = renderWithTheme( , { flags: { parentChildAccountAccess: true }, } ); - const notice = getByText( - 'Remove indirect customers before closing the account.' + const button = getByTestId('close-account-button'); + fireEvent.mouseOver(button); + + await waitFor(() => { + expect(getByRole('tooltip')).toBeInTheDocument(); + }); + + expect( + getByText(PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT) + ).toBeVisible(); + expect(button).toHaveAttribute('aria-describedby', 'button-tooltip'); + expect(button).not.toHaveAttribute('disabled'); + expect(button).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should render a disabled Close Account button with tooltip for a child account user', async () => { + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'child' }), + }); + + const { getByRole, getByTestId, getByText } = renderWithTheme( + , + { + flags: { parentChildAccountAccess: true }, + } + ); + const button = getByTestId('close-account-button'); + fireEvent.mouseOver(button); + + await waitFor(() => { + expect(getByRole('tooltip')).toBeInTheDocument(); + }); + + expect(getByText(CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT)).toBeVisible(); + expect(button).toHaveAttribute('aria-describedby', 'button-tooltip'); + expect(button).not.toHaveAttribute('disabled'); + expect(button).toHaveAttribute('aria-disabled', 'true'); + }); + + it('should render a disabled Close Account button with tooltip for a proxy account user', async () => { + queryMocks.useProfile.mockReturnValue({ + data: profileFactory.build({ user_type: 'proxy' }), + }); + + const { getByRole, getByTestId, getByText } = renderWithTheme( + , + { + flags: { parentChildAccountAccess: true }, + } ); const button = getByTestId('close-account-button'); - expect(notice).toBeInTheDocument(); + fireEvent.mouseOver(button); + + await waitFor(() => { + expect(getByRole('tooltip')).toBeInTheDocument(); + }); + + expect( + getByText(PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT) + ).toBeVisible(); + expect(button).toHaveAttribute('aria-describedby', 'button-tooltip'); expect(button).not.toHaveAttribute('disabled'); expect(button).toHaveAttribute('aria-disabled', 'true'); }); diff --git a/packages/manager/src/features/Account/CloseAccountSetting.tsx b/packages/manager/src/features/Account/CloseAccountSetting.tsx index 74128ee3bc7..5c11d329ab1 100644 --- a/packages/manager/src/features/Account/CloseAccountSetting.tsx +++ b/packages/manager/src/features/Account/CloseAccountSetting.tsx @@ -3,35 +3,41 @@ import * as React from 'react'; import { Accordion } from 'src/components/Accordion'; import { Button } from 'src/components/Button/Button'; -import { Notice } from 'src/components/Notice/Notice'; import { useFlags } from 'src/hooks/useFlags'; -import { useChildAccounts } from 'src/queries/account'; +import { useProfile } from 'src/queries/profile'; import CloseAccountDialog from './CloseAccountDialog'; +import { + CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, + PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT, +} from './constants'; const CloseAccountSetting = () => { const [dialogOpen, setDialogOpen] = React.useState(false); - const { data: childAccounts } = useChildAccounts({}); + const { data: profile } = useProfile(); const flags = useFlags(); - const closeAccountDisabled = - flags.parentChildAccountAccess && Boolean(childAccounts?.data?.length); + + // Disable the Close Account button for users with a Parent/Proxy/Child user type. + const isCloseAccountDisabled = Boolean( + flags.parentChildAccountAccess && profile?.user_type !== null + ); + const closeAccountButtonTooltipText = + isCloseAccountDisabled && profile?.user_type === 'child' + ? CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT + : PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT; return ( <> - {closeAccountDisabled && ( - - Remove indirect customers before closing the account. - - )} diff --git a/packages/manager/src/features/Account/constants.ts b/packages/manager/src/features/Account/constants.ts index ed2d1401fe5..71092b6607c 100644 --- a/packages/manager/src/features/Account/constants.ts +++ b/packages/manager/src/features/Account/constants.ts @@ -1,2 +1,7 @@ export const BUSINESS_PARTNER = 'business partner'; export const ADMINISTRATOR = 'administrator'; + +export const PARENT_PROXY_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT = + 'Remove indirect customers before closing the account.'; +export const CHILD_USER_CLOSE_ACCOUNT_TOOLTIP_TEXT = + 'Contact your business partner to close your account.'; diff --git a/packages/manager/src/features/Users/UserDetail.tsx b/packages/manager/src/features/Users/UserDetail.tsx index 19e41299c65..8e09a60acea 100644 --- a/packages/manager/src/features/Users/UserDetail.tsx +++ b/packages/manager/src/features/Users/UserDetail.tsx @@ -19,6 +19,7 @@ import { TabLinkList } from 'src/components/Tabs/TabLinkList'; import { TabPanels } from 'src/components/Tabs/TabPanels'; import { Tabs } from 'src/components/Tabs/Tabs'; import { queryKey } from 'src/queries/account'; +import { useAccountUser } from 'src/queries/accountUsers'; import { useProfile } from 'src/queries/profile'; import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; @@ -26,11 +27,12 @@ import UserPermissions from './UserPermissions'; import { UserProfile } from './UserProfile'; export const UserDetail = () => { - const { username: usernameParam } = useParams<{ username: string }>(); + const { username: currentUsername } = useParams<{ username: string }>(); const location = useLocation<{ newUsername: string; success: boolean }>(); const history = useHistory(); const { data: profile, refetch: refreshProfile } = useProfile(); + const { data: user } = useAccountUser(currentUsername ?? ''); const queryClient = useQueryClient(); @@ -62,17 +64,17 @@ export const UserDetail = () => { const tabs = [ /* NB: These must correspond to the routes inside the Switch */ { - routeName: `/account/users/${usernameParam}/profile`, + routeName: `/account/users/${currentUsername}/profile`, title: 'User Profile', }, { - routeName: `/account/users/${usernameParam}/permissions`, + routeName: `/account/users/${currentUsername}/permissions`, title: 'User Permissions', }, ]; React.useEffect(() => { - getUser(usernameParam) + getUser(currentUsername) .then((user) => { setOriginalUsername(user.username); setUsername(user.username); @@ -193,6 +195,8 @@ export const UserDetail = () => { history.push(tabs[index].routeName); }; + const isProxyUser = user?.user_type === 'proxy'; + if (error) { return ( @@ -221,7 +225,7 @@ export const UserDetail = () => { )} onChange={navToURL} > - + {!isProxyUser && } {createdUsername && ( { const { errors, restricted } = this.state; const hasErrorFor = getAPIErrorFor({ restricted: 'Restricted' }, errors); const generalError = hasErrorFor('none'); + const isProxyUser = this.state.userType === 'proxy'; return ( theme.spacing(4) }}> @@ -442,7 +443,7 @@ class UserPermissions extends React.Component { > - General Permissions + {isProxyUser ? 'Business Partner' : 'General'} Permissions diff --git a/packages/manager/src/features/Users/UserRow.test.tsx b/packages/manager/src/features/Users/UserRow.test.tsx index ae8b3e0aa5f..56dca8d729d 100644 --- a/packages/manager/src/features/Users/UserRow.test.tsx +++ b/packages/manager/src/features/Users/UserRow.test.tsx @@ -61,9 +61,9 @@ describe('UserRow', () => { ) ); }), - // Mock the active account, which must be of `parent` user type to see the Child Account Access column. - rest.get('*/account/users/*', (req, res, ctx) => { - return res(ctx.json(accountUserFactory.build({ user_type: 'parent' }))); + // Mock the active profile, which must be of `parent` user type to see the Child Account Access column. + rest.get('*/profile', (req, res, ctx) => { + return res(ctx.json(profileFactory.build({ user_type: 'parent' }))); }) ); @@ -88,9 +88,9 @@ describe('UserRow', () => { ) ); }), - // Mock the active account, which must be of `parent` user type to see the Child Account Access column. - rest.get('*/account/users/*', (req, res, ctx) => { - return res(ctx.json(accountUserFactory.build({ user_type: 'parent' }))); + // Mock the active profile, which must be of `parent` user type to see the Child Account Access column. + rest.get('*/profile', (req, res, ctx) => { + return res(ctx.json(profileFactory.build({ user_type: 'parent' }))); }) ); @@ -115,9 +115,9 @@ describe('UserRow', () => { ) ); }), - // Mock the active account, which must NOT be of `parent` user type to hide the Child Account Access column. - rest.get('*/account/users/*', (req, res, ctx) => { - return res(ctx.json(accountUserFactory.build({ user_type: null }))); + // Mock the active profile, which must NOT be of `parent` user type to hide the Child Account Access column. + rest.get('*/profile', (req, res, ctx) => { + return res(ctx.json(profileFactory.build({ user_type: null }))); }) ); @@ -126,7 +126,41 @@ describe('UserRow', () => { flags: { parentChildAccountAccess: true }, }) ); - expect(queryByText('Child Account Access')).not.toBeInTheDocument(); + expect(queryByText('Enabled')).not.toBeInTheDocument(); + }); + + it('renders only a username, email, and account access status for a Proxy user', async () => { + const mockLogin = { + login_datetime: '2022-02-09T16:19:26', + }; + const proxyUser = accountUserFactory.build({ + email: 'proxy@proxy.com', + last_login: mockLogin, + restricted: true, + user_type: 'proxy', + username: 'proxyUsername', + }); + + server.use( + // Mock the active profile for the child account. + rest.get('*/profile', (req, res, ctx) => { + return res(ctx.json(profileFactory.build({ user_type: 'child' }))); + }) + ); + + const { findByText, queryByText } = renderWithTheme( + wrapWithTableBody(, { + flags: { parentChildAccountAccess: true }, + }) + ); + + // Renders Username, Email, and Account Access fields for a proxy user. + expect(await findByText('proxyUsername')).toBeInTheDocument(); + expect(await findByText('proxy@proxy.com')).toBeInTheDocument(); + expect(await findByText('Limited')).toBeInTheDocument(); + + // Does not render the Last Login for a proxy user. + expect(queryByText('2022-02-09T16:19:26')).not.toBeInTheDocument(); }); it('renders "Never" if last_login is null', () => { diff --git a/packages/manager/src/features/Users/UserRow.tsx b/packages/manager/src/features/Users/UserRow.tsx index 06b44c0b2ca..3cf26e0d154 100644 --- a/packages/manager/src/features/Users/UserRow.tsx +++ b/packages/manager/src/features/Users/UserRow.tsx @@ -11,7 +11,7 @@ import { TableCell } from 'src/components/TableCell'; import { TableRow } from 'src/components/TableRow'; import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; -import { useAccountUser, useAccountUserGrants } from 'src/queries/accountUsers'; +import { useAccountUserGrants } from 'src/queries/accountUsers'; import { useProfile } from 'src/queries/profile'; import { capitalize } from 'src/utilities/capitalize'; @@ -28,10 +28,12 @@ export const UserRow = ({ onDelete, user }: Props) => { const flags = useFlags(); const { data: grants } = useAccountUserGrants(user.username); const { data: profile } = useProfile(); - const { data: activeUser } = useAccountUser(profile?.username ?? ''); + const isProxyUser = Boolean( + flags.parentChildAccountAccess && user.user_type === 'proxy' + ); const showChildAccountAccessCol = - flags.parentChildAccountAccess && activeUser?.user_type === 'parent'; + flags.parentChildAccountAccess && profile?.user_type === 'parent'; return ( @@ -54,13 +56,19 @@ export const UserRow = ({ onDelete, user }: Props) => { )} - - - - - + {!isProxyUser && ( + + + + + + )} - + ); diff --git a/packages/manager/src/features/Users/UsersActionMenu.tsx b/packages/manager/src/features/Users/UsersActionMenu.tsx index 5ab86577d56..4f0bdad2cec 100644 --- a/packages/manager/src/features/Users/UsersActionMenu.tsx +++ b/packages/manager/src/features/Users/UsersActionMenu.tsx @@ -8,11 +8,12 @@ import { InlineMenuAction } from 'src/components/InlineMenuAction/InlineMenuActi import { useProfile } from 'src/queries/profile'; interface Props { + isProxyUser: boolean; onDelete: (username: string) => void; username: string; } -export const UsersActionMenu = ({ onDelete, username }: Props) => { +export const UsersActionMenu = ({ isProxyUser, onDelete, username }: Props) => { const history = useHistory(); const theme = useTheme(); const matchesSmDown = useMediaQuery(theme.breakpoints.down('md')); @@ -20,7 +21,16 @@ export const UsersActionMenu = ({ onDelete, username }: Props) => { const { data: profile } = useProfile(); const profileUsername = profile?.username; - const actions: Action[] = [ + const proxyUserActions: Action[] = [ + { + onClick: () => { + history.push(`/account/users/${username}/permissions`); + }, + title: 'Manage Access', + }, + ]; + + const nonProxyUserActions: Action[] = [ { onClick: () => { history.push(`/account/users/${username}`); @@ -46,6 +56,8 @@ export const UsersActionMenu = ({ onDelete, username }: Props) => { }, ]; + const actions = isProxyUser ? proxyUserActions : nonProxyUserActions; + return ( // eslint-disable-next-line react/jsx-no-useless-fragment <> diff --git a/packages/manager/src/features/Users/UsersLanding.tsx b/packages/manager/src/features/Users/UsersLanding.tsx index 876242543da..c9ab394e75c 100644 --- a/packages/manager/src/features/Users/UsersLanding.tsx +++ b/packages/manager/src/features/Users/UsersLanding.tsx @@ -1,33 +1,32 @@ +import { User } from '@linode/api-v4'; import * as React from 'react'; import AddNewLink from 'src/components/AddNewLink'; import { Box } from 'src/components/Box'; import { DocumentTitleSegment } from 'src/components/DocumentTitle'; -import { Hidden } from 'src/components/Hidden'; import { PaginationFooter } from 'src/components/PaginationFooter/PaginationFooter'; import { Table } from 'src/components/Table'; import { TableBody } from 'src/components/TableBody'; -import { TableCell } from 'src/components/TableCell'; -import { TableHead } from 'src/components/TableHead'; -import { TableRow } from 'src/components/TableRow'; import { TableRowEmpty } from 'src/components/TableRowEmpty/TableRowEmpty'; import { TableRowError } from 'src/components/TableRowError/TableRowError'; import { TableRowLoading } from 'src/components/TableRowLoading/TableRowLoading'; -import { TableSortCell } from 'src/components/TableSortCell'; +import { Typography } from 'src/components/Typography'; import { useFlags } from 'src/hooks/useFlags'; import { useOrder } from 'src/hooks/useOrder'; import { usePagination } from 'src/hooks/usePagination'; -import { useAccountUser, useAccountUsers } from 'src/queries/accountUsers'; +import { useAccountUsers } from 'src/queries/accountUsers'; import { useProfile } from 'src/queries/profile'; import CreateUserDrawer from './CreateUserDrawer'; import { UserDeleteConfirmationDialog } from './UserDeleteConfirmationDialog'; import { UserRow } from './UserRow'; +import { UsersLandingProxyTableHead } from './UsersLandingProxyTableHead'; +import { UsersLandingTableBody } from './UsersLandingTableBody'; +import { UsersLandingTableHead } from './UsersLandingTableHead'; export const UsersLanding = () => { const flags = useFlags(); const { data: profile } = useProfile(); - const { data: activeUser } = useAccountUser(profile?.username ?? ''); const pagination = usePagination(1, 'account-users'); const order = useOrder(); @@ -44,10 +43,27 @@ export const UsersLanding = () => { ); const isRestrictedUser = profile?.restricted; - const showChildAccountAccessCol = - flags.parentChildAccountAccess && activeUser?.user_type === 'parent'; + + const showProxyUserTable = + flags.parentChildAccountAccess && + (profile?.user_type === 'child' || profile?.user_type === 'proxy'); + const showChildAccountAccessCol = Boolean( + flags.parentChildAccountAccess && profile?.user_type === 'parent' + ); const numCols = showChildAccountAccessCol ? 6 : 5; + const { nonProxyUsers, proxyUsers } = users?.data.reduce( + (acc: { nonProxyUsers: User[]; proxyUsers: User[] }, user: User) => { + if (user.user_type === 'proxy') { + acc.proxyUsers.push(user); + } else { + acc.nonProxyUsers.push(user); + } + return acc; + }, + { nonProxyUsers: [], proxyUsers: [] } + ) ?? { nonProxyUsers: [], proxyUsers: [] }; + const [isCreateDrawerOpen, setIsCreateDrawerOpen] = React.useState( false ); @@ -60,6 +76,7 @@ export const UsersLanding = () => { setSelectedUsername(username); }; + // TODO: Parent/Child - M3-7559 remove this function once feature is live in production. const renderTableContent = () => { if (isLoading) { return ( @@ -87,7 +104,55 @@ export const UsersLanding = () => { return ( - + {showProxyUserTable && ( + ({ + marginBottom: theme.spacing(2), + marginTop: theme.spacing(3), + [theme.breakpoints.down('md')]: { + marginLeft: theme.spacing(1), + }, + })} + variant="h3" + > + Business partner settings + + )} + {showProxyUserTable && ( + + + + + +
+ )} + ({ + alignItems: 'center', + display: 'flex', + justifyContent: showProxyUserTable ? 'space-between' : 'flex-end', + marginBottom: theme.spacing(2), + marginTop: theme.spacing(3), + })} + > + {showProxyUserTable && ( + ({ + [theme.breakpoints.down('md')]: { + marginLeft: theme.spacing(1), + }, + })} + variant="h3" + > + User settings + + )} { /> - - - - Username - - - - Email Address - - - Account Access - {showChildAccountAccessCol && ( - - Child Account Access - - )} - - Last Login - - - - - {renderTableContent()} + + + {flags.parentChildAccountAccess ? ( + + ) : ( + renderTableContent() + )} +