about summary refs log tree commit diff
path: root/app/views/admin
diff options
context:
space:
mode:
authorEugen Rochko <eugen@zeonfederated.com>2022-07-05 02:41:40 +0200
committerGitHub <noreply@github.com>2022-07-05 02:41:40 +0200
commit44b2ee3485ba0845e5910cefcb4b1e2f84f34470 (patch)
treecc91189c9b36aaf0a04d339455c6d238992753a9 /app/views/admin
parent1b4054256f9d3302b44f71627a23bb0902578867 (diff)
Add customizable user roles (#18641)
* Add customizable user roles

* Various fixes and improvements

* Add migration for old settings and fix tootctl role management
Diffstat (limited to 'app/views/admin')
-rw-r--r--app/views/admin/accounts/index.html.haml55
-rw-r--r--app/views/admin/accounts/show.html.haml9
-rw-r--r--app/views/admin/action_logs/index.html.haml2
-rw-r--r--app/views/admin/instances/show.html.haml53
-rw-r--r--app/views/admin/roles/_form.html.haml37
-rw-r--r--app/views/admin/roles/_role.html.haml18
-rw-r--r--app/views/admin/roles/edit.html.haml8
-rw-r--r--app/views/admin/roles/index.html.haml17
-rw-r--r--app/views/admin/roles/new.html.haml4
-rw-r--r--app/views/admin/settings/edit.html.haml6
-rw-r--r--app/views/admin/tags/show.html.haml87
-rw-r--r--app/views/admin/users/roles/show.html.haml9
12 files changed, 194 insertions, 111 deletions
diff --git a/app/views/admin/accounts/index.html.haml b/app/views/admin/accounts/index.html.haml
index 60e4894d0..7560fac7a 100644
--- a/app/views/admin/accounts/index.html.haml
+++ b/app/views/admin/accounts/index.html.haml
@@ -4,45 +4,36 @@
 - content_for :header_tags do
   = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
 
-.filters
-  .filter-subset
-    %strong= t('admin.accounts.location.title')
-    %ul
-      %li= filter_link_to t('generic.all'), origin: nil
-      %li= filter_link_to t('admin.accounts.location.local'), origin: 'local'
-      %li= filter_link_to t('admin.accounts.location.remote'), origin: 'remote'
-  .filter-subset
-    %strong= t('admin.accounts.moderation.title')
-    %ul
-      %li= filter_link_to t('generic.all'), status: nil
-      %li= filter_link_to t('admin.accounts.moderation.active'), status: 'active'
-      %li= filter_link_to t('admin.accounts.moderation.suspended'), status: 'suspended'
-      %li= filter_link_to safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), status: 'pending'
-  .filter-subset
-    %strong= t('admin.accounts.role')
-    %ul
-      %li= filter_link_to t('admin.accounts.moderation.all'), permissions: nil
-      %li= filter_link_to t('admin.accounts.roles.staff'), permissions: 'staff'
-  .filter-subset
-    %strong= t 'generic.order_by'
-    %ul
-      %li= filter_link_to t('relationships.most_recent'), order: nil
-      %li= filter_link_to t('relationships.last_active'), order: 'active'
-
 = form_tag admin_accounts_url, method: 'GET', class: 'simple_form' do
-  .fields-group
-    - (AccountFilter::KEYS - %i(origin status permissions)).each do |key|
-      - if params[key].present?
-        = hidden_field_tag key, params[key]
+  .filters
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.location.title')
+      .input.select.optional
+        = select_tag :origin, options_for_select([[t('admin.accounts.location.local'), 'local'], [t('admin.accounts.location.remote'), 'remote']], params[:origin]), prompt: I18n.t('generic.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.moderation.title')
+      .input.select.optional
+        = select_tag :status, options_for_select([[t('admin.accounts.moderation.active'), 'active'], [t('admin.accounts.moderation.silenced'), 'silenced'], [t('admin.accounts.moderation.suspended'), 'suspended'], [safe_join([t('admin.accounts.moderation.pending'), "(#{number_with_delimiter(User.pending.count)})"], ' '), 'pending']], params[:status]), prompt: I18n.t('generic.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t('admin.accounts.role')
+      .input.select.optional
+        = select_tag :role_ids, options_from_collection_for_select(UserRole.assignable, :id, :name, params[:role_ids]), prompt: I18n.t('admin.accounts.moderation.all')
+    .filter-subset.filter-subset--with-select
+      %strong= t 'generic.order_by'
+      .input.select
+        = select_tag :order, options_for_select([[t('relationships.most_recent'), nil], [t('relationships.last_active'), 'active']], params[:order])
 
+  .fields-group
     - %i(username by_domain display_name email ip).each do |key|
       - unless key == :by_domain && params[:origin] != 'remote'
         .input.string.optional
           = text_field_tag key, params[key], class: 'string optional', placeholder: I18n.t("admin.accounts.#{key}")
 
-    .actions
-      %button.button= t('admin.accounts.search')
-      = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+  .actions
+    %button.button= t('admin.accounts.search')
+    = link_to t('admin.accounts.reset'), admin_accounts_path, class: 'button negative'
+
+%hr.spacer/
 
 = form_for(@form, url: batch_admin_accounts_path) do |f|
   = hidden_field_tag :page, params[:page] || 1
diff --git a/app/views/admin/accounts/show.html.haml b/app/views/admin/accounts/show.html.haml
index a69832b04..dc3b35956 100644
--- a/app/views/admin/accounts/show.html.haml
+++ b/app/views/admin/accounts/show.html.haml
@@ -92,10 +92,13 @@
 
           %tr
             %th= t('admin.accounts.role')
-            %td= t("admin.accounts.roles.#{@account.user&.role}")
             %td
-              = table_link_to 'angle-double-up', t('admin.accounts.promote'), promote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:promote, @account.user)
-              = table_link_to 'angle-double-down', t('admin.accounts.demote'), demote_admin_account_role_path(@account.id), method: :post, data: { confirm: t('admin.accounts.are_you_sure') } if can?(:demote, @account.user)
+              - if @account.user_role&.everyone?
+                = t('admin.accounts.no_role_assigned')
+              - else
+                = @account.user_role&.name
+            %td
+              = table_link_to 'vcard', t('admin.accounts.change_role.label'), admin_user_role_path(@account.user) if can?(:change_role, @account.user)
 
           %tr
             %th{ rowspan: can?(:create, :email_domain_block) ? 3 : 2 }= t('admin.accounts.email')
diff --git a/app/views/admin/action_logs/index.html.haml b/app/views/admin/action_logs/index.html.haml
index f611bfe9d..d8b7132f5 100644
--- a/app/views/admin/action_logs/index.html.haml
+++ b/app/views/admin/action_logs/index.html.haml
@@ -11,7 +11,7 @@
     .filter-subset.filter-subset--with-select
       %strong= t('admin.action_logs.filter_by_user')
       .input.select.optional
-        = select_tag :account_id, options_from_collection_for_select(Account.joins(:user).merge(User.staff), :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
+        = select_tag :account_id, options_from_collection_for_select(@auditable_accounts, :id, :username, params[:account_id]), prompt: I18n.t('admin.accounts.moderation.all')
 
     .filter-subset.filter-subset--with-select
       %strong= t('admin.action_logs.filter_by_action')
diff --git a/app/views/admin/instances/show.html.haml b/app/views/admin/instances/show.html.haml
index ef4de602d..ab290912e 100644
--- a/app/views/admin/instances/show.html.haml
+++ b/app/views/admin/instances/show.html.haml
@@ -4,32 +4,33 @@
 - content_for :header_tags do
   = javascript_pack_tag 'admin', async: true, crossorigin: 'anonymous'
 
-- content_for :heading_actions do
-  = l(@time_period.first)
-  = ' - '
-  = l(@time_period.last)
-
-%p
-  = fa_icon 'info fw'
-  = t('admin.instances.totals_time_period_hint_html')
-
-.dashboard
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
+- if current_user.can?(:view_dashboard)
+  - content_for :heading_actions do
+    = l(@time_period.first)
+    = ' - '
+    = l(@time_period.last)
+
+  %p
+    = fa_icon 'info fw'
+    = t('admin.instances.totals_time_period_hint_html')
+
+  .dashboard
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_accounts_measure'), href: admin_accounts_path(origin: 'remote', by_domain: @instance.domain)
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_statuses', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_statuses_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_media_attachments', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_media_attachments_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_follows', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_follows_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_followers', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_followers_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'instance_reports', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, label: t('admin.instances.dashboard.instance_reports_measure'), href: admin_reports_path(by_target_domain: @instance.domain)
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'instance_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_accounts_dimension')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'instance_languages', start_at: @time_period.first, end_at: @time_period.last, params: { domain: @instance.domain }, limit: 8, label: t('admin.instances.dashboard.instance_languages_dimension')
 
 %hr.spacer/
 
diff --git a/app/views/admin/roles/_form.html.haml b/app/views/admin/roles/_form.html.haml
new file mode 100644
index 000000000..68607ce68
--- /dev/null
+++ b/app/views/admin/roles/_form.html.haml
@@ -0,0 +1,37 @@
+= simple_form_for @role, url: @role.new_record? ? admin_roles_path : admin_role_path(@role) do |f|
+  = render 'shared/error_messages', object: @role
+
+  - if @role.everyone?
+    .flash-message.info
+      = t('admin.roles.everyone_full_description_html')
+  - else
+    .fields-group
+      = f.input :name, wrapper: :with_label
+
+    .fields-group
+      = f.input :position, wrapper: :with_label
+
+    .fields-group
+      = f.input :color, wrapper: :with_label, input_html: { placeholder: '#000000' }
+
+    %hr.spacer/
+
+    .fields-group
+      = f.input :highlighted, wrapper: :with_label
+
+    %hr.spacer/
+
+  .field-group
+    .input.with_block_label
+      %label= t('simple_form.labels.user_role.permissions_as_keys')
+      %span.hint= t('simple_form.hints.user_role.permissions_as_keys')
+
+    - (@role.everyone? ? UserRole::Flags::CATEGORIES.slice(:invites) : UserRole::Flags::CATEGORIES).each do |category, permissions|
+      %h4= t(category, scope: 'admin.roles.categories')
+
+      = f.input :permissions_as_keys, collection: permissions, wrapper: :with_block_label, include_blank: false, label_method: lambda { |privilege| safe_join([t("admin.roles.privileges.#{privilege}"), content_tag(:span, t("admin.roles.privileges.#{privilege}_description"), class: 'hint')]) }, required: false, as: :check_boxes, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li', label: false, hint: false
+
+  %hr.spacer/
+
+  .actions
+    = f.button :button, @role.new_record? ? t('admin.roles.add_new') : t('generic.save_changes'), type: :submit
diff --git a/app/views/admin/roles/_role.html.haml b/app/views/admin/roles/_role.html.haml
new file mode 100644
index 000000000..6804f4f15
--- /dev/null
+++ b/app/views/admin/roles/_role.html.haml
@@ -0,0 +1,18 @@
+.announcements-list__item
+  = link_to edit_admin_role_path(role), class: 'announcements-list__item__title' do
+    %span.user-role{ class: "user-role-#{role.id}" }
+      = fa_icon 'users fw'
+
+      - if role.everyone?
+        = t('admin.roles.everyone')
+      - else
+        = role.name
+
+  .announcements-list__item__action-bar
+    .announcements-list__item__meta
+      - if role.everyone?
+        = t('admin.roles.everyone_full_description_html')
+      - else
+        = link_to t('admin.roles.assigned_users', count: role.users.count), admin_accounts_path(role_id: role.id)
+        •
+        %abbr{ title: role.permissions_as_keys.map { |privilege| I18n.t("admin.roles.privileges.#{privilege}") }.join(', ') }= t('admin.roles.permissions_count', count: role.permissions_as_keys.size)
diff --git a/app/views/admin/roles/edit.html.haml b/app/views/admin/roles/edit.html.haml
new file mode 100644
index 000000000..659ccb8dc
--- /dev/null
+++ b/app/views/admin/roles/edit.html.haml
@@ -0,0 +1,8 @@
+- content_for :page_title do
+  = t('admin.roles.edit', name: @role.everyone? ? t('admin.roles.everyone') : @role.name)
+
+- content_for :heading_actions do
+  = link_to t('admin.roles.delete'), admin_role_path(@role), method: :delete, data: { confirm: t('admin.accounts.are_you_sure') }, class: 'button button--destructive' if can?(:destroy, @role)
+
+= render partial: 'form'
+
diff --git a/app/views/admin/roles/index.html.haml b/app/views/admin/roles/index.html.haml
new file mode 100644
index 000000000..4f6c511b4
--- /dev/null
+++ b/app/views/admin/roles/index.html.haml
@@ -0,0 +1,17 @@
+- content_for :page_title do
+  = t('admin.roles.title')
+
+- content_for :heading_actions do
+  = link_to t('admin.roles.add_new'), new_admin_role_path, class: 'button' if can?(:create, :user_role)
+
+%p= t('admin.roles.description_html')
+
+%hr.spacer/
+
+.applications-list
+  = render partial: 'role', collection: @roles.select(&:everyone?)
+
+%hr.spacer/
+
+.applications-list
+  = render partial: 'role', collection: @roles.reject(&:everyone?)
diff --git a/app/views/admin/roles/new.html.haml b/app/views/admin/roles/new.html.haml
new file mode 100644
index 000000000..821079271
--- /dev/null
+++ b/app/views/admin/roles/new.html.haml
@@ -0,0 +1,4 @@
+- content_for :page_title do
+  = t('admin.roles.add_new')
+
+= render partial: 'form'
diff --git a/app/views/admin/settings/edit.html.haml b/app/views/admin/settings/edit.html.haml
index 33bfc43d3..d7896bbc0 100644
--- a/app/views/admin/settings/edit.html.haml
+++ b/app/views/admin/settings/edit.html.haml
@@ -62,9 +62,6 @@
       = f.input :show_known_fediverse_at_about_page, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_known_fediverse_at_about_page.title'), hint: t('admin.settings.show_known_fediverse_at_about_page.desc_html')
 
   .fields-group
-    = f.input :show_staff_badge, as: :boolean, wrapper: :with_label, label: t('admin.settings.show_staff_badge.title'), hint: t('admin.settings.show_staff_badge.desc_html')
-
-  .fields-group
     = f.input :open_deletion, as: :boolean, wrapper: :with_label, label: t('admin.settings.registrations.deletion.title'), hint: t('admin.settings.registrations.deletion.desc_html')
 
   - unless whitelist_mode?
@@ -91,9 +88,6 @@
 
   %hr.spacer/
 
-  .fields-group
-    = f.input :min_invite_role, wrapper: :with_label, collection: %i(disabled user moderator admin), label: t('admin.settings.registrations.min_invite_role.title'), label_method: lambda { |role| role == :disabled ? t('admin.settings.registrations.min_invite_role.disabled') : t("admin.accounts.roles.#{role}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
-
   .fields-row
     .fields-row__column.fields-row__column-6.fields-group
       = f.input :show_domain_blocks, wrapper: :with_label, collection: %i(disabled users all), label: t('admin.settings.domain_blocks.title'), label_method: lambda { |value| t("admin.settings.domain_blocks.#{value}") }, include_blank: false, collection_wrapper_tag: 'ul', item_wrapper_tag: 'li'
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index df72bd5f5..fd9acce4a 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -4,49 +4,50 @@
 - content_for :page_title do
   = "##{@tag.name}"
 
-- content_for :heading_actions do
-  = l(@time_period.first)
-  = ' - '
-  = l(@time_period.last)
-
-.dashboard
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
-  .dashboard__item
-    = react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
-  .dashboard__item
-    = react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
-  .dashboard__item
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
-      - if @tag.usable?
-        %span= t('admin.trends.tags.usable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_usable')
-        = fa_icon 'lock fw'
-
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
-      - if @tag.trendable?
-        %span= t('admin.trends.tags.trendable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_trendable')
-        = fa_icon 'lock fw'
-
-
-    = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
-      - if @tag.listable?
-        %span= t('admin.trends.tags.listable')
-        = fa_icon 'check fw'
-      - else
-        %span= t('admin.trends.tags.not_listable')
-        = fa_icon 'lock fw'
-
-%hr.spacer/
+- if current_user.can?(:view_dashboard)
+  - content_for :heading_actions do
+    = l(@time_period.first)
+    = ' - '
+    = l(@time_period.last)
+
+  .dashboard
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_accounts', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_accounts_measure'), href: tag_url(@tag), target: '_blank'
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_uses', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_uses_measure')
+    .dashboard__item
+      = react_admin_component :counter, measure: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, label: t('admin.trends.tags.dashboard.tag_servers_measure')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'tag_servers', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_servers_dimension')
+    .dashboard__item
+      = react_admin_component :dimension, dimension: 'tag_languages', start_at: @time_period.first, end_at: @time_period.last, params: { id: @tag.id }, limit: 8, label: t('admin.trends.tags.dashboard.tag_languages_dimension')
+    .dashboard__item
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.usable? ? 'positive' : 'negative'] do
+        - if @tag.usable?
+          %span= t('admin.trends.tags.usable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_usable')
+          = fa_icon 'lock fw'
+
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.trendable? ? 'positive' : 'negative'] do
+        - if @tag.trendable?
+          %span= t('admin.trends.tags.trendable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_trendable')
+          = fa_icon 'lock fw'
+
+
+      = link_to admin_tag_path(@tag.id), class: ['dashboard__quick-access', @tag.listable? ? 'positive' : 'negative'] do
+        - if @tag.listable?
+          %span= t('admin.trends.tags.listable')
+          = fa_icon 'check fw'
+        - else
+          %span= t('admin.trends.tags.not_listable')
+          = fa_icon 'lock fw'
+
+  %hr.spacer/
 
 = simple_form_for @tag, url: admin_tag_path(@tag.id) do |f|
   = render 'shared/error_messages', object: @tag
diff --git a/app/views/admin/users/roles/show.html.haml b/app/views/admin/users/roles/show.html.haml
new file mode 100644
index 000000000..821618060
--- /dev/null
+++ b/app/views/admin/users/roles/show.html.haml
@@ -0,0 +1,9 @@
+- content_for :page_title do
+  = t('admin.accounts.change_role.title', username: @user.account.username)
+
+= simple_form_for @user, url: admin_user_role_path(@user) do |f|
+  .fields-group
+    = f.association :role, wrapper: :with_block_label, collection: UserRole.assignable, label_method: :name, include_blank: I18n.t('admin.accounts.change_role.no_role')
+
+  .actions
+    = f.button :button, t('generic.save_changes'), type: :submit