Skip to content

Commit

Permalink
feat: [UIE-8136] - add new users table component (part 2)
Browse files Browse the repository at this point in the history
  • Loading branch information
aaleksee-akamai committed Dec 11, 2024
1 parent 0bd360b commit 8bf33b9
Show file tree
Hide file tree
Showing 8 changed files with 613 additions and 16 deletions.
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))
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>
);
};
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();
});
});
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);
Loading

0 comments on commit 8bf33b9

Please sign in to comment.