Commit bf945394 authored by Jiaan Louw's avatar Jiaan Louw Committed by Mikołaj Wawrzyniak

Add admin users vue app wrapper

The first implementation for the admin/users MVP.

This passes the the existing user data and admin user paths from Rails
to Vue via data attributes.

It also adds a blank app wrapper to begin filling in.

All this is behind the :vue_admin_users frontend feature flag
parent 3c2d7dc2
<script>
export default {
props: {
users: {
type: Array,
required: false,
default: () => [],
},
paths: {
type: Object,
required: true,
},
},
};
</script>
<template>
<div>
<!-- Temporary empty app -->
</div>
</template>
import Vue from 'vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import AdminUsersApp from './components/app.vue';
export default function(el = document.querySelector('#js-admin-users-app')) {
if (!el) {
return false;
}
const { users, paths } = el.dataset;
return new Vue({
el,
render: createElement =>
createElement(AdminUsersApp, {
props: {
users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }),
paths: convertObjectPropsToCamelCase(JSON.parse(paths)),
},
}),
});
}
...@@ -4,6 +4,7 @@ import Translate from '~/vue_shared/translate'; ...@@ -4,6 +4,7 @@ import Translate from '~/vue_shared/translate';
import ModalManager from './components/user_modal_manager.vue'; import ModalManager from './components/user_modal_manager.vue';
import csrf from '~/lib/utils/csrf'; import csrf from '~/lib/utils/csrf';
import initConfirmModal from '~/confirm_modal'; import initConfirmModal from '~/confirm_modal';
import initAdminUsersApp from '~/admin/users';
const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts'; const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal'; const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
...@@ -56,4 +57,5 @@ document.addEventListener('DOMContentLoaded', () => { ...@@ -56,4 +57,5 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
initConfirmModal(); initConfirmModal();
initAdminUsersApp();
}); });
# frozen_string_literal: true # frozen_string_literal: true
module UsersHelper module UsersHelper
def admin_users_data_attributes(users)
{
users: Admin::UserSerializer.new.represent(users).to_json,
paths: admin_users_paths.to_json
}
end
def user_link(user) def user_link(user)
link_to(user.name, user_path(user), link_to(user.name, user_path(user),
title: user.email, title: user.email,
...@@ -208,6 +215,22 @@ module UsersHelper ...@@ -208,6 +215,22 @@ module UsersHelper
private private
def admin_users_paths
{
edit: edit_admin_user_path(:id),
approve: approve_admin_user_path(:id),
reject: reject_admin_user_path(:id),
unblock: unblock_admin_user_path(:id),
block: block_admin_user_path(:id),
deactivate: deactivate_admin_user_path(:id),
activate: activate_admin_user_path(:id),
unlock: unlock_admin_user_path(:id),
delete: admin_user_path(:id),
delete_with_contributions: admin_user_path(:id),
admin_user: admin_user_path(:id)
}
end
def blocked_user_badge(user) def blocked_user_badge(user)
pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' } pending_approval_badge = { text: s_('AdminUsers|Pending approval'), variant: 'info' }
return pending_approval_badge if user.blocked_pending_approval? return pending_approval_badge if user.blocked_pending_approval?
......
...@@ -72,6 +72,10 @@ ...@@ -72,6 +72,10 @@
- if @users.empty? - if @users.empty?
.nothing-here-block.border-top-0 .nothing-here-block.border-top-0
= s_('AdminUsers|No users found') = s_('AdminUsers|No users found')
- elsif Feature.enabled?(:vue_admin_users)
#js-admin-users-app{ data: admin_users_data_attributes(@users) }
.gl-spinner-container.gl-my-7
%span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } }
- else - else
.table-holder .table-holder
.thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' } .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' }
......
---
name: vue_admin_users
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48922
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290737
milestone: '13.7'
type: development
group: group::compliance
default_enabled: false
...@@ -12,6 +12,7 @@ RSpec.describe "Admin::Users" do ...@@ -12,6 +12,7 @@ RSpec.describe "Admin::Users" do
let!(:current_user) { create(:admin, last_activity_on: 5.days.ago) } let!(:current_user) { create(:admin, last_activity_on: 5.days.ago) }
before do before do
stub_feature_flags(vue_admin_users: false)
sign_in(current_user) sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user)
end end
......
...@@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users::User' do ...@@ -9,6 +9,7 @@ RSpec.describe 'Admin::Users::User' do
before do before do
sign_in(current_user) sign_in(current_user)
gitlab_enable_admin_mode_sign_in(current_user) gitlab_enable_admin_mode_sign_in(current_user)
stub_feature_flags(vue_admin_users: false)
end end
describe 'GET /admin/users/:id' do describe 'GET /admin/users/:id' do
......
...@@ -15,6 +15,7 @@ RSpec.describe 'Admin::Users' do ...@@ -15,6 +15,7 @@ RSpec.describe 'Admin::Users' do
describe 'GET /admin/users' do describe 'GET /admin/users' do
before do before do
stub_feature_flags(vue_admin_users: false)
visit admin_users_path visit admin_users_path
end end
...@@ -418,6 +419,7 @@ RSpec.describe 'Admin::Users' do ...@@ -418,6 +419,7 @@ RSpec.describe 'Admin::Users' do
describe 'GET /admin/users/:id/edit' do describe 'GET /admin/users/:id/edit' do
before do before do
stub_feature_flags(vue_admin_users: false)
visit admin_users_path visit admin_users_path
click_link "edit_user_#{user.id}" click_link "edit_user_#{user.id}"
end end
......
{
"type": "object",
"properties": {
"edit": { "type": "string" },
"approve": { "type": "string" },
"reject": { "type": "string" },
"unblock": { "type": "string" },
"block": { "type": "string" },
"deactivate": { "type": "string" },
"activate": { "type": "string" },
"unlock": { "type": "string" },
"delete": { "type": "string" },
"delete_with_contributions": { "type": "string" },
"admin_user": { "type": "string" }
},
"required": [
"edit",
"approve",
"reject",
"unblock",
"block",
"deactivate",
"activate",
"unlock",
"delete",
"delete_with_contributions",
"admin_user"
],
"additionalProperties": false
}
import { createWrapper } from '@vue/test-utils';
import initAdminUsers from '~/admin/users';
import AdminUsersApp from '~/admin/users/components/app.vue';
import { users, paths } from './mock_data';
describe('initAdminUsersApp', () => {
let wrapper;
let el;
const findApp = () => wrapper.find(AdminUsersApp);
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-users', JSON.stringify(users));
el.setAttribute('data-paths', JSON.stringify(paths));
document.body.appendChild(el);
wrapper = createWrapper(initAdminUsers(el));
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
el.remove();
el = null;
});
it('parses and passes props', () => {
expect(findApp().props()).toMatchObject({
users,
paths,
});
});
});
export const users = [
{
id: 2177,
name: 'Nikki',
createdAt: '2020-11-13T12:26:54.177Z',
email: 'nikki@example.com',
username: 'nikki',
lastActivityOn: null,
avatarUrl:
'https://secure.gravatar.com/avatar/054f062d8b1a42b123f17e13a173cda8?s=80\\u0026d=identicon',
badges: [],
projectsCount: 0,
actions: [],
},
];
export const paths = {
edit: '/admin/users/id/edit',
approve: '/admin/users/id/approve',
reject: '/admin/users/id/reject',
unblock: '/admin/users/id/unblock',
block: '/admin/users/id/block',
deactivate: '/admin/users/id/deactivate',
activate: '/admin/users/id/activate',
unlock: '/admin/users/id/unlock',
delete: '/admin/users/id',
deleteWithContributions: '/admin/users/id',
adminUser: '/admin/users/id',
};
...@@ -333,4 +333,21 @@ RSpec.describe UsersHelper do ...@@ -333,4 +333,21 @@ RSpec.describe UsersHelper do
allow(helper).to receive(:can?).with(current_user, :read_user_profile, user).and_return(allowed) allow(helper).to receive(:can?).with(current_user, :read_user_profile, user).and_return(allowed)
end end
end end
describe '#admin_users_data_attributes' do
subject(:data) { helper.admin_users_data_attributes([user]) }
it 'users matches the serialized json' do
entity = double
expect_next_instance_of(Admin::UserSerializer) do |instance|
expect(instance).to receive(:represent).with([user]).and_return(entity)
end
expect(entity).to receive(:to_json).and_return("{\"username\":\"admin\"}")
expect(data[:users]).to eq "{\"username\":\"admin\"}"
end
it 'paths matches the schema' do
expect(data[:paths]).to match_schema('entities/admin_users_data_attributes_paths')
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment