-
Notifications
You must be signed in to change notification settings - Fork 368
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: [UIE-8136] - add new users table component (part 2)
- Loading branch information
1 parent
73f9186
commit e574b64
Showing
10 changed files
with
573 additions
and
21 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@linode/manager": Added | ||
--- | ||
|
||
useCreateUserMutation for adding new users ([#11402](https://github.com/linode/manager/pull/11402)) |
5 changes: 5 additions & 0 deletions
5
packages/manager/.changeset/pr-11402-upcoming-features-1733927050433.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
--- | ||
"@linode/manager": Upcoming Features | ||
--- | ||
|
||
proxy users table, removing users, adding users ([#11402](https://github.com/linode/manager/pull/11402)) |
73 changes: 73 additions & 0 deletions
73
packages/manager/src/features/IAM/Shared/UserDeleteConfirmation.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import { Notice, Typography } from '@linode/ui'; | ||
import { useSnackbar } from 'notistack'; | ||
import * as React from 'react'; | ||
|
||
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; | ||
import { ConfirmationDialog } from 'src/components/ConfirmationDialog/ConfirmationDialog'; | ||
import { useAccountUserDeleteMutation } from 'src/queries/account/users'; | ||
|
||
interface Props { | ||
onClose: () => void; | ||
onSuccess?: () => void; | ||
open: boolean; | ||
username: string; | ||
} | ||
|
||
export const UserDeleteConfirmation = (props: Props) => { | ||
const { onClose: _onClose, onSuccess, open, username } = props; | ||
|
||
const { enqueueSnackbar } = useSnackbar(); | ||
|
||
const { | ||
error, | ||
isPending, | ||
mutateAsync: deleteUser, | ||
reset, | ||
} = useAccountUserDeleteMutation(username); | ||
|
||
const onClose = () => { | ||
reset(); // resets the error state of the useMutation | ||
_onClose(); | ||
}; | ||
|
||
const onDelete = async () => { | ||
await deleteUser(); | ||
enqueueSnackbar(`User ${username} has been deleted successfully.`, { | ||
variant: 'success', | ||
}); | ||
if (onSuccess) { | ||
onSuccess(); | ||
} | ||
onClose(); | ||
}; | ||
|
||
return ( | ||
<ConfirmationDialog | ||
actions={ | ||
<ActionsPanel | ||
primaryButtonProps={{ | ||
label: 'Delete User', | ||
loading: isPending, | ||
onClick: onDelete, | ||
}} | ||
secondaryButtonProps={{ | ||
label: 'Cancel', | ||
onClick: onClose, | ||
}} | ||
style={{ padding: 0 }} | ||
/> | ||
} | ||
error={error?.[0].reason} | ||
onClose={onClose} | ||
open={open} | ||
title={`Delete user ${username}?`} | ||
> | ||
<Notice variant="warning"> | ||
<Typography sx={{ fontSize: '0.875rem' }}> | ||
<strong>Warning:</strong> Deleting this User is permanent and can’t be | ||
undone. | ||
</Typography> | ||
</Notice> | ||
</ConfirmationDialog> | ||
); | ||
}; |
74 changes: 74 additions & 0 deletions
74
packages/manager/src/features/IAM/Users/UsersTable/CreateUserDrawer.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,74 @@ | ||
import { fireEvent } from '@testing-library/react'; | ||
import React from 'react'; | ||
|
||
import { HttpResponse, http, server } from 'src/mocks/testServer'; | ||
import { renderWithTheme } from 'src/utilities/testHelpers'; | ||
|
||
import { CreateUserDrawer } from './CreateUserDrawer'; | ||
|
||
const props = { | ||
onClose: vi.fn(), | ||
open: true, | ||
}; | ||
|
||
const testEmail = 'testuser@example.com'; | ||
|
||
describe('CreateUserDrawer', () => { | ||
it('should render the drawer when open is true', () => { | ||
const { getByRole } = renderWithTheme(<CreateUserDrawer {...props} />); | ||
|
||
const dialog = getByRole('dialog'); | ||
expect(dialog).toBeInTheDocument(); | ||
}); | ||
|
||
it('should allow the user to fill out the form', () => { | ||
const { getByLabelText, getByRole } = renderWithTheme( | ||
<CreateUserDrawer {...props} /> | ||
); | ||
|
||
const dialog = getByRole('dialog'); | ||
expect(dialog).toBeInTheDocument(); | ||
|
||
fireEvent.change(getByLabelText(/username/i), { | ||
target: { value: 'testuser' }, | ||
}); | ||
fireEvent.change(getByLabelText(/email/i), { | ||
target: { value: testEmail }, | ||
}); | ||
|
||
expect(getByLabelText(/username/i)).toHaveValue('testuser'); | ||
expect(getByLabelText(/email/i)).toHaveValue(testEmail); | ||
}); | ||
|
||
it('should display an error message when submission fails', async () => { | ||
server.use( | ||
http.post('*/account/users', () => { | ||
return HttpResponse.json( | ||
{ error: [{ reason: 'An unexpected error occurred.' }] }, | ||
{ status: 500 } | ||
); | ||
}) | ||
); | ||
|
||
const { | ||
findByText, | ||
getByLabelText, | ||
getByRole, | ||
getByTestId, | ||
} = renderWithTheme(<CreateUserDrawer {...props} />); | ||
|
||
const dialog = getByRole('dialog'); | ||
expect(dialog).toBeInTheDocument(); | ||
|
||
fireEvent.change(getByLabelText(/username/i), { | ||
target: { value: 'testuser' }, | ||
}); | ||
fireEvent.change(getByLabelText(/email/i), { | ||
target: { value: testEmail }, | ||
}); | ||
fireEvent.click(getByTestId('submit')); | ||
|
||
const errorMessage = await findByText('An unexpected error occurred.'); | ||
expect(errorMessage).toBeInTheDocument(); | ||
}); | ||
}); |
147 changes: 147 additions & 0 deletions
147
packages/manager/src/features/IAM/Users/UsersTable/CreateUserDrawer.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
import { Box, FormControlLabel, Notice, TextField, Toggle } from '@linode/ui'; | ||
import * as React from 'react'; | ||
import { Controller, useForm } from 'react-hook-form'; | ||
import { useHistory } from 'react-router-dom'; | ||
|
||
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; | ||
import { Drawer } from 'src/components/Drawer'; | ||
import { useCreateUserMutation } from 'src/queries/account/users'; | ||
|
||
import type { User } from '@linode/api-v4/lib/account'; | ||
|
||
interface Props { | ||
onClose: () => void; | ||
open: boolean; | ||
} | ||
|
||
export const CreateUserDrawer = (props: Props) => { | ||
const { onClose, open } = props; | ||
const history = useHistory(); | ||
const { mutateAsync: createUserMutation } = useCreateUserMutation(); | ||
|
||
const { | ||
control, | ||
formState: { errors, isSubmitting }, | ||
handleSubmit, | ||
reset, | ||
setError, | ||
} = useForm({ | ||
defaultValues: { | ||
email: '', | ||
restricted: false, | ||
username: '', | ||
}, | ||
}); | ||
|
||
const onSubmit = async (data: { | ||
email: string; | ||
restricted: boolean; | ||
username: string; | ||
}) => { | ||
try { | ||
const user: User = await createUserMutation(data); | ||
handleClose(); | ||
|
||
if (user.restricted) { | ||
history.push(`/account/users/${data.username}/permissions`, { | ||
newUsername: user.username, | ||
}); | ||
} | ||
} catch (errors) { | ||
for (const error of errors) { | ||
setError(error?.field ?? 'root', { message: error.reason }); | ||
} | ||
} | ||
}; | ||
|
||
const handleClose = () => { | ||
reset(); | ||
onClose(); | ||
}; | ||
|
||
return ( | ||
<Drawer onClose={handleClose} open={open} title="Add a User"> | ||
{errors.root?.message && ( | ||
<Notice text={errors.root?.message} variant="error" /> | ||
)} | ||
<form onSubmit={handleSubmit(onSubmit)}> | ||
<Controller | ||
render={({ field, fieldState }) => ( | ||
<TextField | ||
data-qa-create-username | ||
errorText={fieldState.error?.message} | ||
label="Username" | ||
onBlur={field.onBlur} | ||
onChange={field.onChange} | ||
required | ||
trimmed | ||
value={field.value} | ||
/> | ||
)} | ||
control={control} | ||
name="username" | ||
rules={{ required: 'Username is required' }} | ||
/> | ||
|
||
<Controller | ||
render={({ field, fieldState }) => ( | ||
<TextField | ||
data-qa-create-email | ||
errorText={fieldState.error?.message} | ||
label="Email" | ||
onChange={field.onChange} | ||
required | ||
trimmed | ||
type="email" | ||
value={field.value} | ||
/> | ||
)} | ||
control={control} | ||
name="email" | ||
rules={{ required: 'Email is required' }} | ||
/> | ||
|
||
<Controller | ||
render={({ field }) => ( | ||
<FormControlLabel | ||
control={ | ||
<Toggle | ||
checked={!field.value} | ||
data-qa-create-restricted | ||
onChange={field.onChange} | ||
/> | ||
} | ||
label={`This user will have ${ | ||
field.value ? 'limited' : 'full' | ||
} access to account features. | ||
This can be changed later.`} | ||
sx={{ marginTop: 1 }} | ||
/> | ||
)} | ||
control={control} | ||
name="restricted" | ||
/> | ||
|
||
<Box sx={{ marginTop: 1 }}> | ||
<Notice | ||
text="The user will be sent an email to set their password" | ||
variant="warning" | ||
/> | ||
</Box> | ||
<ActionsPanel | ||
primaryButtonProps={{ | ||
'data-testid': 'submit', | ||
label: 'Add User', | ||
loading: isSubmitting, | ||
type: 'submit', | ||
}} | ||
secondaryButtonProps={{ | ||
'data-testid': 'cancel', | ||
label: 'Cancel', | ||
onClick: handleClose, | ||
}} | ||
/> | ||
</form> | ||
</Drawer> | ||
); | ||
}; |
68 changes: 68 additions & 0 deletions
68
packages/manager/src/features/IAM/Users/UsersTable/ProxyUserTable.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
import { Typography } from '@linode/ui'; | ||
import React from 'react'; | ||
|
||
import { Table } from 'src/components/Table'; | ||
import { TableBody } from 'src/components/TableBody'; | ||
import { PARENT_USER } from 'src/features/Account/constants'; | ||
import { useAccountUsers } from 'src/queries/account/users'; | ||
|
||
import { UsersLandingProxyTableHead } from './UsersLandingProxyTableHead'; | ||
import { UsersLandingTableBody } from './UsersLandingTableBody'; | ||
|
||
import type { Order } from './UsersLandingTableHead'; | ||
|
||
interface Props { | ||
handleDelete: (username: string) => void; | ||
isProxyUser: boolean; | ||
isRestrictedUser: boolean | undefined; | ||
order: Order; | ||
} | ||
|
||
export const ProxyUserTable = ({ | ||
handleDelete, | ||
isProxyUser, | ||
isRestrictedUser, | ||
order, | ||
}: Props) => { | ||
const { | ||
data: proxyUser, | ||
error: proxyUserError, | ||
isLoading: isLoadingProxyUser, | ||
} = useAccountUsers({ | ||
enabled: isProxyUser && !isRestrictedUser, | ||
filters: { user_type: 'proxy' }, | ||
}); | ||
|
||
const proxyNumCols = 3; | ||
|
||
return ( | ||
<> | ||
<Typography | ||
sx={(theme) => ({ | ||
marginBottom: theme.spacing(2), | ||
marginTop: theme.spacing(3), | ||
textTransform: 'capitalize', | ||
[theme.breakpoints.down('md')]: { | ||
marginLeft: theme.spacing(1), | ||
}, | ||
})} | ||
variant="h3" | ||
> | ||
{PARENT_USER} Settings | ||
</Typography> | ||
|
||
<Table aria-label="List of Parent Users"> | ||
<UsersLandingProxyTableHead order={order} /> | ||
<TableBody> | ||
<UsersLandingTableBody | ||
error={proxyUserError} | ||
isLoading={isLoadingProxyUser} | ||
numCols={proxyNumCols} | ||
onDelete={handleDelete} | ||
users={proxyUser?.data} | ||
/> | ||
</TableBody> | ||
</Table> | ||
</> | ||
); | ||
}; |
Oops, something went wrong.