-
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
0bd360b
commit 8bf33b9
Showing
8 changed files
with
613 additions
and
16 deletions.
There are no files selected for viewing
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> | ||
); | ||
}; |
75 changes: 75 additions & 0 deletions
75
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,75 @@ | ||
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, | ||
refetch: vi.fn(), | ||
}; | ||
|
||
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 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(/error creating user./i); | ||
expect(errorMessage).toBeInTheDocument(); | ||
}); | ||
}); |
176 changes: 176 additions & 0 deletions
176
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,176 @@ | ||
import { createUser } from '@linode/api-v4/lib/account'; | ||
import { FormControlLabel, Notice, TextField, Toggle } from '@linode/ui'; | ||
import * as React from 'react'; | ||
import { withRouter } from 'react-router-dom'; | ||
|
||
import { ActionsPanel } from 'src/components/ActionsPanel/ActionsPanel'; | ||
import { Drawer } from 'src/components/Drawer'; | ||
import { getAPIErrorOrDefault } from 'src/utilities/errorUtils'; | ||
import { getAPIErrorFor } from 'src/utilities/getAPIErrorFor'; | ||
|
||
import type { User } from '@linode/api-v4/lib/account'; | ||
import type { APIError } from '@linode/api-v4/lib/types'; | ||
import type { RouteComponentProps } from 'react-router-dom'; | ||
|
||
interface Props { | ||
onClose: () => void; | ||
open: boolean; | ||
refetch: () => void; | ||
} | ||
|
||
interface State { | ||
email: string; | ||
errors: APIError[]; | ||
restricted: boolean; | ||
submitting: boolean; | ||
username: string; | ||
} | ||
|
||
interface CreateUserDrawerProps extends Props, RouteComponentProps<{}> {} | ||
|
||
class CreateUserDrawer extends React.Component<CreateUserDrawerProps, State> { | ||
handleChangeEmail = (e: React.ChangeEvent<HTMLInputElement>) => { | ||
this.setState({ | ||
email: e.target.value, | ||
}); | ||
}; | ||
|
||
handleChangeRestricted = () => { | ||
this.setState({ | ||
restricted: !this.state.restricted, | ||
}); | ||
}; | ||
|
||
handleChangeUsername = ( | ||
e: | ||
| React.ChangeEvent<HTMLInputElement> | ||
| React.FocusEvent<HTMLInputElement | HTMLTextAreaElement> | ||
) => { | ||
this.setState({ | ||
username: e.target.value, | ||
}); | ||
}; | ||
|
||
onSubmit = () => { | ||
const { | ||
history: { push }, | ||
onClose, | ||
refetch, | ||
} = this.props; | ||
const { email, restricted, username } = this.state; | ||
this.setState({ errors: [], submitting: true }); | ||
createUser({ email, restricted, username }) | ||
.then((user: User) => { | ||
this.setState({ submitting: false }); | ||
onClose(); | ||
if (user.restricted) { | ||
push(`/account/users/${username}/permissions`, { | ||
newUsername: user.username, | ||
}); | ||
} | ||
refetch(); | ||
}) | ||
.catch((errResponse) => { | ||
const errors = getAPIErrorOrDefault( | ||
errResponse, | ||
'Error creating user.' | ||
); | ||
this.setState({ errors, submitting: false }); | ||
}); | ||
}; | ||
|
||
state: State = { | ||
email: '', | ||
errors: [], | ||
restricted: false, | ||
submitting: false, | ||
username: '', | ||
}; | ||
|
||
componentDidUpdate(prevProps: CreateUserDrawerProps) { | ||
if (this.props.open === true && prevProps.open === false) { | ||
this.setState({ | ||
email: '', | ||
errors: [], | ||
restricted: false, | ||
submitting: false, | ||
username: '', | ||
}); | ||
} | ||
} | ||
|
||
render() { | ||
const { onClose, open } = this.props; | ||
const { email, errors, restricted, submitting, username } = this.state; | ||
|
||
const hasErrorFor = getAPIErrorFor( | ||
{ email: 'Email', username: 'Username' }, | ||
errors | ||
); | ||
const generalError = hasErrorFor('none'); | ||
|
||
return ( | ||
<Drawer onClose={onClose} open={open} title="Add a User"> | ||
{generalError && <Notice text={generalError} variant="error" />} | ||
<TextField | ||
data-qa-create-username | ||
errorText={hasErrorFor('username')} | ||
label="Username" | ||
onBlur={this.handleChangeUsername} | ||
onChange={this.handleChangeUsername} | ||
required | ||
trimmed | ||
value={username} | ||
/> | ||
<TextField | ||
data-qa-create-email | ||
errorText={hasErrorFor('email')} | ||
label="Email" | ||
onChange={this.handleChangeEmail} | ||
required | ||
trimmed | ||
type="email" | ||
value={email} | ||
/> | ||
<FormControlLabel | ||
control={ | ||
<Toggle | ||
checked={!restricted} | ||
data-qa-create-restricted | ||
onChange={this.handleChangeRestricted} | ||
/> | ||
} | ||
label={ | ||
restricted | ||
? `This user will have limited access to account features. | ||
This can be changed later.` | ||
: `This user will have full access to account features. | ||
This can be changed later.` | ||
} | ||
style={{ marginTop: 8 }} | ||
/> | ||
<div style={{ marginTop: 8 }}> | ||
<Notice | ||
text="The user will be sent an email to set their password" | ||
variant="warning" | ||
/> | ||
</div> | ||
<ActionsPanel | ||
primaryButtonProps={{ | ||
'data-testid': 'submit', | ||
label: 'Add User', | ||
loading: submitting, | ||
onClick: this.onSubmit, | ||
}} | ||
secondaryButtonProps={{ | ||
'data-testid': 'cancel', | ||
label: 'Cancel', | ||
onClick: onClose, | ||
}} | ||
/> | ||
</Drawer> | ||
); | ||
} | ||
} | ||
|
||
export default withRouter(CreateUserDrawer); |
Oops, something went wrong.