From 85a95f5aa1f8e76e1af539080600365d1cf18658 Mon Sep 17 00:00:00 2001 From: Evan Purkhiser Date: Wed, 17 Jul 2019 18:55:07 -0700 Subject: [PATCH] ref(admin): Convert user edit page to react --- src/sentry/api/endpoints/user_details.py | 8 +- src/sentry/api/serializers/models/user.py | 1 + src/sentry/static/sentry/app/routes.jsx | 22 +- .../sentry/app/views/admin/adminUserEdit.jsx | 202 ++++++++++++++++++ .../sentry/app/views/admin/adminUsers.jsx | 5 +- .../templates/sentry/admin/users/edit.html | 58 ----- .../templates/sentry/admin/users/remove.html | 23 -- .../templates/sentry/missing_permissions.html | 15 -- src/sentry/web/decorators.py | 15 -- src/sentry/web/forms/__init__.py | 50 +---- src/sentry/web/frontend/admin.py | 64 ------ src/sentry/web/urls.py | 11 +- 12 files changed, 229 insertions(+), 245 deletions(-) create mode 100644 src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx delete mode 100644 src/sentry/templates/sentry/admin/users/edit.html delete mode 100644 src/sentry/templates/sentry/admin/users/remove.html delete mode 100644 src/sentry/templates/sentry/missing_permissions.html diff --git a/src/sentry/api/endpoints/user_details.py b/src/sentry/api/endpoints/user_details.py index 57ac1a9265b1a2..dc643d4732734b 100644 --- a/src/sentry/api/endpoints/user_details.py +++ b/src/sentry/api/endpoints/user_details.py @@ -86,14 +86,16 @@ def validate(self, attrs): return super(UserSerializer, self).validate(attrs) -class AdminUserSerializer(BaseUserSerializer): +class SuperuserUserSerializer(BaseUserSerializer): isActive = serializers.BooleanField(source='is_active') + isStaff = serializers.BooleanField(source='is_staff') + isSuperuser = serializers.BooleanField(source='is_superuser') class Meta: model = User # no idea wtf is up with django rest framework, but we need is_active # and isActive - fields = ('name', 'username', 'isActive') + fields = ('name', 'username', 'isActive', 'isStaff', 'isSuperuser') # write_only_fields = ('password',) @@ -130,7 +132,7 @@ def put(self, request, user): """ if is_active_superuser(request): - serializer_cls = AdminUserSerializer + serializer_cls = SuperuserUserSerializer else: serializer_cls = UserSerializer serializer = serializer_cls(user, data=request.data, partial=True) diff --git a/src/sentry/api/serializers/models/user.py b/src/sentry/api/serializers/models/user.py index 5d14a40e2e6395..5d8cfd5e12b6f6 100644 --- a/src/sentry/api/serializers/models/user.py +++ b/src/sentry/api/serializers/models/user.py @@ -78,6 +78,7 @@ def serialize(self, obj, attrs, user): 'has2fa': attrs['has2fa'], 'lastActive': obj.last_active, 'isSuperuser': obj.is_superuser, + 'isStaff': obj.is_staff, } if obj == user: diff --git a/src/sentry/static/sentry/app/routes.jsx b/src/sentry/static/sentry/app/routes.jsx index ede1a7a303ed0a..0e1821aa7e07b9 100644 --- a/src/sentry/static/sentry/app/routes.jsx +++ b/src/sentry/static/sentry/app/routes.jsx @@ -731,13 +731,21 @@ function routes() { } component={errorHandler(LazyLoad)} /> - - import(/* webpackChunkName: "AdminUsers" */ 'app/views/admin/adminUsers') - } - component={errorHandler(LazyLoad)} - /> + + + import(/* webpackChunkName: "AdminUsers" */ 'app/views/admin/adminUsers') + } + component={errorHandler(LazyLoad)} + /> + + import(/* webpackChunkName: "AdminUserEdit" */ 'app/views/admin/adminUserEdit') + } + component={errorHandler(LazyLoad)} + /> + diff --git a/src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx b/src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx new file mode 100644 index 00000000000000..96715d92cfa627 --- /dev/null +++ b/src/sentry/static/sentry/app/views/admin/adminUserEdit.jsx @@ -0,0 +1,202 @@ +import {browserHistory} from 'react-router'; +import PropTypes from 'prop-types'; +import React from 'react'; +import styled from 'react-emotion'; + +import {addErrorMessage, addSuccessMessage} from 'app/actionCreators/indicator'; +import {openModal} from 'app/actionCreators/modal'; +import {t, tct} from 'app/locale'; +import AsyncView from 'app/views/asyncView'; +import Button from 'app/components/button'; +import Form from 'app/views/settings/components/forms/form'; +import FormModel from 'app/views/settings/components/forms/model'; +import JsonForm from 'app/views/settings/components/forms/jsonForm'; +import RadioGroup from 'app/views/settings/components/forms/controls/radioGroup'; +import SentryTypes from 'app/sentryTypes'; +import space from 'app/styles/space'; + +const userEditForm = { + title: 'User details', + fields: [ + { + name: 'name', + type: 'string', + required: true, + label: t('Name'), + }, + { + name: 'username', + type: 'string', + required: true, + label: t('Username'), + help: t('The username is the unique id of the user in the system'), + }, + { + name: 'email', + type: 'string', + required: true, + label: t('Email'), + help: t('The users primary email address'), + }, + { + name: 'isActive', + type: 'boolean', + required: true, + label: t('Active'), + help: t( + 'Designates whether this user should be treated as active. Unselect this instead of deleting accounts.' + ), + }, + { + name: 'isStaff', + type: 'boolean', + required: true, + label: t('Admin'), + help: t('Designates whether this user can perform administrative functions.'), + }, + { + name: 'isSuperuser', + type: 'boolean', + required: true, + label: t('Superuser'), + help: t( + 'Designates whether this user has all permissions without explicitly assigning them.' + ), + }, + ], +}; + +const REMOVE_BUTTON_LABEL = { + disable: t('Disable User'), + delete: t('Permanently Delete User'), +}; + +class RemoveUserModal extends React.Component { + static propTypes = { + user: SentryTypes.User, + onRemove: PropTypes.func, + closeModal: PropTypes.func, + }; + + state = { + deleteType: 'disable', + }; + + onRemove = () => { + this.props.onRemove(this.state.deleteType); + this.props.closeModal(); + }; + + render() { + const {user} = this.props; + const {deleteType} = this.state; + + return ( + +

{tct('Removing user [user]', {user: {user.email}})}

+ this.setState({deleteType: type})} + choices={[ + ['disable', t('Disable the account.')], + ['delete', t('Permanently remove the user and their data.')], + ]} + /> + + + + +
+ ); + } +} + +class AdminUserEdit extends AsyncView { + get userEndpoint() { + const {params} = this.props; + return `/users/${params.id}/`; + } + + getEndpoints() { + return [['user', this.userEndpoint]]; + } + + async deleteUser() { + await this.api.requestPromise(this.userEndpoint, { + method: 'DELETE', + data: {hardDelete: true, organizations: []}, + }); + + addSuccessMessage(t("%s's account has been deleted.", this.state.user.email)); + browserHistory.replace('/manage/users/'); + } + + async deactivateUser() { + const response = await this.api.requestPromise(this.userEndpoint, { + method: 'PUT', + data: {isActive: false}, + }); + + this.setState({user: response}); + this.formModel.setInitialData(response); + addSuccessMessage(t("%s's account has been deactivated.", response.email)); + } + + removeUser = actionTypes => + actionTypes === 'delete' ? this.deleteUser() : this.deactivateUser(); + + formModel = new FormModel(); + + renderBody() { + const {user} = this.state; + const openDeleteModal = () => + openModal(opts => ( + + )); + + return ( + +

{t('Users')}

+

{t('Editing user: %s', user.email)}

+
{ + this.setState({user: data}); + addSuccessMessage('User account updated.'); + }} + extraButton={ + + } + > + + +
+ ); + } +} + +const ModalFooter = styled('div')` + display: grid; + grid-auto-flow: column; + grid-gap: ${space(1)}; + justify-content: end; + padding: 20px 30px; + margin: 20px -30px -30px; + border-top: 1px solid ${p => p.theme.borderLight}; +`; + +export default AdminUserEdit; diff --git a/src/sentry/static/sentry/app/views/admin/adminUsers.jsx b/src/sentry/static/sentry/app/views/admin/adminUsers.jsx index 9fa187d194aad0..0c1619c12e2100 100644 --- a/src/sentry/static/sentry/app/views/admin/adminUsers.jsx +++ b/src/sentry/static/sentry/app/views/admin/adminUsers.jsx @@ -2,8 +2,9 @@ import React from 'react'; import moment from 'moment'; -import ResultGrid from 'app/components/resultGrid'; import {t} from 'app/locale'; +import Link from 'app/components/links/link'; +import ResultGrid from 'app/components/resultGrid'; export const prettyDate = function(x) { return moment(x).format('ll'); @@ -14,7 +15,7 @@ class AdminUsers extends React.Component { return [ - {row.username} + {row.username}
{row.email !== row.username && {row.email}} diff --git a/src/sentry/templates/sentry/admin/users/edit.html b/src/sentry/templates/sentry/admin/users/edit.html deleted file mode 100644 index dd34a0914641da..00000000000000 --- a/src/sentry/templates/sentry/admin/users/edit.html +++ /dev/null @@ -1,58 +0,0 @@ -{% extends "sentry/bases/admin.html" %} - -{% load crispy_forms_tags %} -{% load i18n %} -{% load sentry_admin_helpers %} -{% load sentry_helpers %} - -{% block title %}{% trans "Change User" %} | {{ block.super }}{% endblock %} - -{% block main %} -
-

User Details {{ the_user.username }}

-
- {% csrf_token %} - {{ form|as_crispy_errors }} - {% for field in form %} - {{ field|as_crispy_field }} - {% endfor %} -
- - {% if the_user.id != request.user.id %} - {% trans "Remove User" %} - {% else %} - {% trans "Cannot remove yourself" %} - {% endif %} -
-
-
- {% if project_list %} -

{% trans "Projects" %}

- - - - - - - - - - - - - - {% for project, avg_events in project_list|with_event_counts %} - - - - - {% endfor %} - -
{% trans "Project" %}{% trans "Daily Events" %}
- {{ project.slug }} [view] - - ~{{ avg_events }} -
- {% endif %} -
-{% endblock %} diff --git a/src/sentry/templates/sentry/admin/users/remove.html b/src/sentry/templates/sentry/admin/users/remove.html deleted file mode 100644 index 72bd052991b76d..00000000000000 --- a/src/sentry/templates/sentry/admin/users/remove.html +++ /dev/null @@ -1,23 +0,0 @@ -{% extends "sentry/admin/users/edit.html" %} - -{% load crispy_forms_tags %} -{% load i18n %} - -{% block title %}{% trans "Remove User" %} | {{ block.super }}{% endblock %} - -{% block main %} -
-

Remove User {{ user.username }}

- -
- {% csrf_token %} - {{ form|as_crispy_errors }} - {% for field in form %} - {{ field|as_crispy_field }} - {% endfor %} -
- {% trans "Cancel" %} -
-
-
-{% endblock %} diff --git a/src/sentry/templates/sentry/missing_permissions.html b/src/sentry/templates/sentry/missing_permissions.html deleted file mode 100644 index 4af2c4f4c93c33..00000000000000 --- a/src/sentry/templates/sentry/missing_permissions.html +++ /dev/null @@ -1,15 +0,0 @@ -{% extends "sentry/layout.html" %} - -{% load i18n %} - -{% block title %}{% trans "Forbidden" %} | {{ block.super }}{% endblock %} - -{% block page_header %} -

{% trans "Forbidden" %}

-{% endblock %} - -{% block content %} -
-

{% trans "You do not have access to this page with your current permissions." %}

-
-{% endblock %} diff --git a/src/sentry/web/decorators.py b/src/sentry/web/decorators.py index 8b6687edc3bf8d..28d2569c49696d 100644 --- a/src/sentry/web/decorators.py +++ b/src/sentry/web/decorators.py @@ -6,9 +6,7 @@ from django.contrib import messages from django.utils.translation import ugettext_lazy as _ -from sentry.auth.superuser import is_active_superuser from sentry.utils import auth -from sentry.web.helpers import render_to_response ERR_BAD_SIGNATURE = _('The link you followed is invalid or expired.') @@ -39,16 +37,3 @@ def wrapped(request, *args, **kwargs): return func(request, *args, **kwargs) return wrapped - - -def requires_admin(func): - @wraps(func) - def wrapped(request, *args, **kwargs): - if not is_active_superuser(request): - if request.user.is_superuser: - auth.initiate_login(request, next_url=request.get_full_path()) - return HttpResponseRedirect(auth.get_login_url()) - return render_to_response('sentry/missing_permissions.html', {}, request, status=400) - return func(request, *args, **kwargs) - - return login_required(wrapped) diff --git a/src/sentry/web/forms/__init__.py b/src/sentry/web/forms/__init__.py index 85b14bb1143f97..5fcb6512149df8 100644 --- a/src/sentry/web/forms/__init__.py +++ b/src/sentry/web/forms/__init__.py @@ -8,56 +8,8 @@ from __future__ import absolute_import from django import forms -from django.utils.translation import ugettext_lazy as _ -from sentry.models import User, Activity -from sentry.web.forms.fields import RadioFieldRenderer, ReadOnlyTextField - - -class BaseUserForm(forms.ModelForm): - email = forms.EmailField() - name = forms.CharField(required=True, label=_('Name')) - - -class ChangeUserForm(BaseUserForm): - is_staff = forms.BooleanField( - required=False, - label=_('Admin'), - help_text=_("Designates whether this user can perform administrative functions.") - ) - is_superuser = forms.BooleanField( - required=False, - label=_('Superuser'), - help_text=_( - 'Designates whether this user has all permissions without ' - 'explicitly assigning them.' - ) - ) - - class Meta: - fields = ('name', 'username', 'email', 'is_active', 'is_staff', 'is_superuser') - model = User - - def __init__(self, *args, **kwargs): - super(ChangeUserForm, self).__init__(*args, **kwargs) - self.user = kwargs['instance'] - if self.user.is_managed: - self.fields['username'] = ReadOnlyTextField(label="Username (managed)") - - def clean_username(self): - if self.user.is_managed: - return self.user.username - return self.cleaned_data['username'] - - -class RemoveUserForm(forms.Form): - removal_type = forms.ChoiceField( - choices=( - ('1', _('Disable the account.')), - ('2', _('Permanently remove the user and their data.')), - ), - widget=forms.RadioSelect(renderer=RadioFieldRenderer) - ) +from sentry.models import Activity class NewNoteForm(forms.Form): diff --git a/src/sentry/web/frontend/admin.py b/src/sentry/web/frontend/admin.py index 39de912a3f99e3..0793daedb9d7b5 100644 --- a/src/sentry/web/frontend/admin.py +++ b/src/sentry/web/frontend/admin.py @@ -7,16 +7,9 @@ """ from __future__ import absolute_import, print_function -import six -from django.core.context_processors import csrf from django.http import HttpResponse, HttpResponseRedirect -from django.views.decorators.csrf import csrf_protect -from sentry.models import Project, User from sentry.plugins import plugins -from sentry.utils.http import absolute_uri -from sentry.web.decorators import requires_admin -from sentry.web.forms import (ChangeUserForm, RemoveUserForm) from sentry.utils import auth from sentry.web.helpers import render_to_response @@ -38,60 +31,3 @@ def configure_plugin(request, slug): 'view': view, }, request ) - - -@requires_admin -@csrf_protect -def edit_user(request, user_id): - try: - user = User.objects.get(pk=user_id) - except User.DoesNotExist: - return HttpResponseRedirect(absolute_uri('/manage/users/')) - - form = ChangeUserForm(request.POST or None, instance=user) - if form.is_valid(): - user = form.save() - return HttpResponseRedirect(absolute_uri('/manage/users/')) - - project_list = Project.objects.filter( - status=0, - organization__member_set__user=user, - ).order_by('-date_added') - - context = { - 'form': form, - 'the_user': user, - 'project_list': project_list, - } - context.update(csrf(request)) - - return render_to_response('sentry/admin/users/edit.html', context, request) - - -@requires_admin -@csrf_protect -def remove_user(request, user_id): - if six.text_type(user_id) == six.text_type(request.user.id): - return HttpResponseRedirect(absolute_uri('/manage/users/')) - - try: - user = User.objects.get(pk=user_id) - except User.DoesNotExist: - return HttpResponseRedirect(absolute_uri('/manage/users/')) - - form = RemoveUserForm(request.POST or None) - if form.is_valid(): - if form.cleaned_data['removal_type'] == '2': - user.delete() - else: - User.objects.filter(pk=user.pk).update(is_active=False) - - return HttpResponseRedirect(absolute_uri('/manage/users/')) - - context = csrf(request) - context.update({ - 'form': form, - 'the_user': user, - }) - - return render_to_response('sentry/admin/users/remove.html', context, request) diff --git a/src/sentry/web/urls.py b/src/sentry/web/urls.py index dcef60b66f0960..dfe4c8f5219e61 100644 --- a/src/sentry/web/urls.py +++ b/src/sentry/web/urls.py @@ -314,21 +314,14 @@ def init_all_applications(): url(r'^account/', generic_react_page_view), url(r'^onboarding/', generic_react_page_view), - # Admin - Users - url(r'^manage/users/(?P\d+)/$', - admin.edit_user, name='sentry-admin-edit-user'), - url( - r'^manage/users/(?P\d+)/remove/$', - admin.remove_user, - name='sentry-admin-remove-user' - ), - # Admin - Plugins url( r'^manage/plugins/(?P[\w_-]+)/$', admin.configure_plugin, name='sentry-admin-configure-plugin' ), + + # Admin url(r'^manage/', react_page_view, name='sentry-admin-overview'), # Legacy Redirects