Skip to content

Commit a031b32

Browse files
ref(admin): Convert user edit page to react (#14074)
1 parent 9e66ffc commit a031b32

File tree

12 files changed

+229
-245
lines changed

12 files changed

+229
-245
lines changed

src/sentry/api/endpoints/user_details.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -86,14 +86,16 @@ def validate(self, attrs):
8686
return super(UserSerializer, self).validate(attrs)
8787

8888

89-
class AdminUserSerializer(BaseUserSerializer):
89+
class SuperuserUserSerializer(BaseUserSerializer):
9090
isActive = serializers.BooleanField(source='is_active')
91+
isStaff = serializers.BooleanField(source='is_staff')
92+
isSuperuser = serializers.BooleanField(source='is_superuser')
9193

9294
class Meta:
9395
model = User
9496
# no idea wtf is up with django rest framework, but we need is_active
9597
# and isActive
96-
fields = ('name', 'username', 'isActive')
98+
fields = ('name', 'username', 'isActive', 'isStaff', 'isSuperuser')
9799
# write_only_fields = ('password',)
98100

99101

@@ -131,7 +133,7 @@ def put(self, request, user):
131133
"""
132134

133135
if is_active_superuser(request):
134-
serializer_cls = AdminUserSerializer
136+
serializer_cls = SuperuserUserSerializer
135137
else:
136138
serializer_cls = UserSerializer
137139
serializer = serializer_cls(user, data=request.data, partial=True)

src/sentry/api/serializers/models/user.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ def serialize(self, obj, attrs, user):
7878
'has2fa': attrs['has2fa'],
7979
'lastActive': obj.last_active,
8080
'isSuperuser': obj.is_superuser,
81+
'isStaff': obj.is_staff,
8182
}
8283

8384
if obj == user:

src/sentry/static/sentry/app/routes.jsx

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -731,13 +731,21 @@ function routes() {
731731
}
732732
component={errorHandler(LazyLoad)}
733733
/>
734-
<Route
735-
path="users/"
736-
componentPromise={() =>
737-
import(/* webpackChunkName: "AdminUsers" */ 'app/views/admin/adminUsers')
738-
}
739-
component={errorHandler(LazyLoad)}
740-
/>
734+
<Route path="users/">
735+
<IndexRoute
736+
componentPromise={() =>
737+
import(/* webpackChunkName: "AdminUsers" */ 'app/views/admin/adminUsers')
738+
}
739+
component={errorHandler(LazyLoad)}
740+
/>
741+
<Route
742+
path=":id"
743+
componentPromise={() =>
744+
import(/* webpackChunkName: "AdminUserEdit" */ 'app/views/admin/adminUserEdit')
745+
}
746+
component={errorHandler(LazyLoad)}
747+
/>
748+
</Route>
741749
<Route
742750
path="status/mail/"
743751
componentPromise={() =>
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
import {browserHistory} from 'react-router';
2+
import PropTypes from 'prop-types';
3+
import React from 'react';
4+
import styled from 'react-emotion';
5+
6+
import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator';
7+
import {openModal} from 'app/actionCreators/modal';
8+
import {t, tct} from 'app/locale';
9+
import AsyncView from 'app/views/asyncView';
10+
import Button from 'app/components/button';
11+
import Form from 'app/views/settings/components/forms/form';
12+
import FormModel from 'app/views/settings/components/forms/model';
13+
import JsonForm from 'app/views/settings/components/forms/jsonForm';
14+
import RadioGroup from 'app/views/settings/components/forms/controls/radioGroup';
15+
import SentryTypes from 'app/sentryTypes';
16+
import space from 'app/styles/space';
17+
18+
const userEditForm = {
19+
title: 'User details',
20+
fields: [
21+
{
22+
name: 'name',
23+
type: 'string',
24+
required: true,
25+
label: t('Name'),
26+
},
27+
{
28+
name: 'username',
29+
type: 'string',
30+
required: true,
31+
label: t('Username'),
32+
help: t('The username is the unique id of the user in the system'),
33+
},
34+
{
35+
name: 'email',
36+
type: 'string',
37+
required: true,
38+
label: t('Email'),
39+
help: t('The users primary email address'),
40+
},
41+
{
42+
name: 'isActive',
43+
type: 'boolean',
44+
required: true,
45+
label: t('Active'),
46+
help: t(
47+
'Designates whether this user should be treated as active. Unselect this instead of deleting accounts.'
48+
),
49+
},
50+
{
51+
name: 'isStaff',
52+
type: 'boolean',
53+
required: true,
54+
label: t('Admin'),
55+
help: t('Designates whether this user can perform administrative functions.'),
56+
},
57+
{
58+
name: 'isSuperuser',
59+
type: 'boolean',
60+
required: true,
61+
label: t('Superuser'),
62+
help: t(
63+
'Designates whether this user has all permissions without explicitly assigning them.'
64+
),
65+
},
66+
],
67+
};
68+
69+
const REMOVE_BUTTON_LABEL = {
70+
disable: t('Disable User'),
71+
delete: t('Permanently Delete User'),
72+
};
73+
74+
class RemoveUserModal extends React.Component {
75+
static propTypes = {
76+
user: SentryTypes.User,
77+
onRemove: PropTypes.func,
78+
closeModal: PropTypes.func,
79+
};
80+
81+
state = {
82+
deleteType: 'disable',
83+
};
84+
85+
onRemove = () => {
86+
this.props.onRemove(this.state.deleteType);
87+
this.props.closeModal();
88+
};
89+
90+
render() {
91+
const {user} = this.props;
92+
const {deleteType} = this.state;
93+
94+
return (
95+
<React.Fragment>
96+
<p>{tct('Removing user [user]', {user: <strong>{user.email}</strong>})}</p>
97+
<RadioGroup
98+
value={deleteType}
99+
onChange={type => this.setState({deleteType: type})}
100+
choices={[
101+
['disable', t('Disable the account.')],
102+
['delete', t('Permanently remove the user and their data.')],
103+
]}
104+
/>
105+
<ModalFooter>
106+
<Button priority="danger" onClick={this.onRemove}>
107+
{REMOVE_BUTTON_LABEL[deleteType]}
108+
</Button>
109+
<Button onClick={this.props.closeModal}>{t('Nevermind')}</Button>
110+
</ModalFooter>
111+
</React.Fragment>
112+
);
113+
}
114+
}
115+
116+
class AdminUserEdit extends AsyncView {
117+
get userEndpoint() {
118+
const {params} = this.props;
119+
return `/users/${params.id}/`;
120+
}
121+
122+
getEndpoints() {
123+
return [['user', this.userEndpoint]];
124+
}
125+
126+
async deleteUser() {
127+
await this.api.requestPromise(this.userEndpoint, {
128+
method: 'DELETE',
129+
data: {hardDelete: true, organizations: []},
130+
});
131+
132+
addSuccessMessage(t("%s's account has been deleted.", this.state.user.email));
133+
browserHistory.replace('/manage/users/');
134+
}
135+
136+
async deactivateUser() {
137+
const response = await this.api.requestPromise(this.userEndpoint, {
138+
method: 'PUT',
139+
data: {isActive: false},
140+
});
141+
142+
this.setState({user: response});
143+
this.formModel.setInitialData(response);
144+
addSuccessMessage(t("%s's account has been deactivated.", response.email));
145+
}
146+
147+
removeUser = actionTypes =>
148+
actionTypes === 'delete' ? this.deleteUser() : this.deactivateUser();
149+
150+
formModel = new FormModel();
151+
152+
renderBody() {
153+
const {user} = this.state;
154+
const openDeleteModal = () =>
155+
openModal(opts => (
156+
<RemoveUserModal user={user} onRemove={this.removeUser} {...opts} />
157+
));
158+
159+
return (
160+
<React.Fragment>
161+
<h3>{t('Users')}</h3>
162+
<p>{t('Editing user: %s', user.email)}</p>
163+
<Form
164+
model={this.formModel}
165+
initialData={user}
166+
apiMethod="PUT"
167+
apiEndpoint={this.userEndpoint}
168+
requireChanges
169+
onSubmitError={addErrorMessage}
170+
onSubmitSuccess={data => {
171+
this.setState({user: data});
172+
addSuccessMessage('User account updated.');
173+
}}
174+
extraButton={
175+
<Button
176+
type="button"
177+
onClick={openDeleteModal}
178+
style={{marginLeft: space(1)}}
179+
priority="danger"
180+
>
181+
{t('Remove User')}
182+
</Button>
183+
}
184+
>
185+
<JsonForm forms={[userEditForm]} />
186+
</Form>
187+
</React.Fragment>
188+
);
189+
}
190+
}
191+
192+
const ModalFooter = styled('div')`
193+
display: grid;
194+
grid-auto-flow: column;
195+
grid-gap: ${space(1)};
196+
justify-content: end;
197+
padding: 20px 30px;
198+
margin: 20px -30px -30px;
199+
border-top: 1px solid ${p => p.theme.borderLight};
200+
`;
201+
202+
export default AdminUserEdit;

src/sentry/static/sentry/app/views/admin/adminUsers.jsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22
import React from 'react';
33
import moment from 'moment';
44

5-
import ResultGrid from 'app/components/resultGrid';
65
import {t} from 'app/locale';
6+
import Link from 'app/components/links/link';
7+
import ResultGrid from 'app/components/resultGrid';
78

89
export const prettyDate = function(x) {
910
return moment(x).format('ll');
@@ -14,7 +15,7 @@ class AdminUsers extends React.Component {
1415
return [
1516
<td>
1617
<strong>
17-
<a href={`/manage/users/${row.id}/`}>{row.username}</a>
18+
<Link to={`/manage/users/${row.id}/`}>{row.username}</Link>
1819
</strong>
1920
<br />
2021
{row.email !== row.username && <small>{row.email}</small>}

src/sentry/templates/sentry/admin/users/edit.html

Lines changed: 0 additions & 58 deletions
This file was deleted.

src/sentry/templates/sentry/admin/users/remove.html

Lines changed: 0 additions & 23 deletions
This file was deleted.

src/sentry/templates/sentry/missing_permissions.html

Lines changed: 0 additions & 15 deletions
This file was deleted.

0 commit comments

Comments
 (0)