Commit f7547d81 authored by Timothy Andrew's avatar Timothy Andrew Committed by Alfredo Sumaran

Implement frontend to allow specific people to access protected branches.

1. While creating a protected branch, you can set a single user / role
  for each setting ("Allowed to Merge", "Allowed to Push").

2. More users / roles can be set subsequently.

3. Repurposed 'users_select.js.coffee` for the needs of this page.

4. Move protected branch settings to the `show` page.

    - Too many settings on the single index page can be overwhelming. Also,
      if the number of users that can access a protected branch is large,
      the amount of space between protected branches in the table can be
      unwieldy.

    - This is the simplest design I can think of - we can use this
      until we have someone from the frontend/ux team take a look at
      this.

    - Move protected branches javascript under a `protected_branches`
      directory.

    - The dropdowns don't show access levels / users that have already been
      selected.

    - Allow deleting access levels using two new access level controllers.
parent d78ab154
// Modified version of `UsersSelect` for use with access selection for protected branches.
//
// - Selections are sent via AJAX if `saveOnSelect` is `true`
// - If `saveOnSelect` is `false`, the dropdown element must have a `field-name` data
// attribute. The DOM must contain two fields - "#{field-name}[access_level]" and "#{field_name}[user_id]"
// where the selections will be stored.
class ProtectedBranchesAccessSelect {
constructor(container, saveOnSelect, selectDefault) {
this.container = container;
this.saveOnSelect = saveOnSelect;
this.selectDefault = selectDefault;
this.usersPath = "/autocomplete/users.json";
this.setupDropdown(".allowed-to-merge", gon.merge_access_levels, gon.selected_merge_access_levels);
this.setupDropdown(".allowed-to-push", gon.push_access_levels, gon.selected_push_access_levels);
}
setupDropdown(className, accessLevels, selectedAccessLevels) {
this.container.find(className).each((i, element) => {
var dropdown = $(element).glDropdown({
clicked: _.chain(this.onSelect).partial(element).bind(this).value(),
data: (term, callback) => {
this.getUsers(term, (users) => {
users = _(users).map((user) => _(user).extend({ type: "user" }));
accessLevels = _(accessLevels).map((accessLevel) => _(accessLevel).extend({ type: "role" }));
var accessLevelsWithUsers = accessLevels.concat("divider", users);
callback(_(accessLevelsWithUsers).reject((item) => _.contains(selectedAccessLevels, item.id)));
});
},
filterable: true,
filterRemote: true,
search: { fields: ['name', 'username'] },
selectable: true,
toggleLabel: (selected) => $(element).data('default-label'),
renderRow: (user) => {
if (user.before_divider != null) {
return "<li> <a href='#'>" + user.text + " </a> </li>";
}
var username = user.username ? "@" + user.username : null;
var avatar = user.avatar_url ? user.avatar_url : false;
var img = avatar ? "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />" : '';
var listWithName = "<li> <a href='#' class='dropdown-menu-user-link'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
var listWithUserName = username ? "<span class='dropdown-menu-user-username'> " + username + " </span>" : '';
var listClosingTags = "</a> </li>";
return listWithName + listWithUserName + listClosingTags;
}
});
if (this.selectDefault) {
$(dropdown).find('.dropdown-toggle-text').text(accessLevels[0].text);
}
});
}
onSelect(dropdown, selected, element, e) {
$(dropdown).find('.dropdown-toggle-text').text(selected.text || selected.name);
var access_level = selected.type == 'user' ? 40 : selected.id;
var user_id = selected.type == 'user' ? selected.id : null;
if (this.saveOnSelect) {
$.ajax({
type: "POST",
url: $(dropdown).data('url'),
dataType: "json",
data: {
_method: 'PATCH',
id: $(dropdown).data('id'),
protected_branch: {
["" + ($(dropdown).data('type')) + "_attributes"]: [{
access_level: access_level,
user_id: user_id
}]
}
},
success: function() {
var row;
row = $(e.target);
row.closest('tr').effect('highlight');
row.closest('td').find('.access-levels-list').append("<li>" + selected.name + "</li>");
location.reload();
},
error: function() {
new Flash("Failed to update branch!", "alert");
}
});
} else {
var fieldName = $(dropdown).data('field-name');
$("input[name='" + fieldName + "[access_level]']").val(access_level);
$("input[name='" + fieldName + "[user_id]']").val(user_id);
}
}
getUsers(query, callback) {
var url = this.buildUrl(this.usersPath);
return $.ajax({
url: url,
data: {
search: query,
per_page: 20,
active: true,
project_id: gon.current_project_id
},
dataType: "json"
}).done(function(users) {
callback(users);
});
}
buildUrl(url) {
if (gon.relative_url_root != null) {
url = gon.relative_url_root.replace(/\/$/, '') + url;
}
return url;
}
}
......@@ -662,6 +662,15 @@ pre.light-well {
}
}
a.allowed-to-merge, a.allowed-to-push {
cursor: pointer;
cursor: hand;
}
.protected-branch-push-access-list, .protected-branch-merge-access-list {
a { color: #fff; }
}
.protected-branches-list {
a {
color: $gl-gray;
......
......@@ -25,6 +25,7 @@ class Projects::ApplicationController < ApplicationController
project_path = "#{namespace}/#{id}"
@project = Project.find_with_namespace(project_path)
gon.current_project_id = @project.id if @project
if can?(current_user, :read_project, @project) && !@project.pending_delete?
if @project.path_with_namespace != project_path
......
class Projects::ProtectedBranches::ApplicationController < Projects::ApplicationController
protected
def load_protected_branch
@protected_branch = @project.protected_branches.find(params[:protected_branch_id])
end
end
module Projects
module ProtectedBranches
class MergeAccessLevelsController < Projects::ProtectedBranches::ApplicationController
before_action :load_protected_branch
def destroy
@merge_access_level = @protected_branch.merge_access_levels.find(params[:id])
@merge_access_level.destroy
flash[:notice] = "Successfully deleted. #{@merge_access_level.humanize} will not be able to merge into this protected branch."
redirect_to namespace_project_protected_branch_path(@project.namespace, @project, @protected_branch)
end
end
end
end
module Projects
module ProtectedBranches
class PushAccessLevelsController < Projects::ProtectedBranches::ApplicationController
before_action :load_protected_branch
def destroy
@push_access_level = @protected_branch.push_access_levels.find(params[:id])
@push_access_level.destroy
flash[:notice] = "Successfully deleted. #{@push_access_level.humanize} will not be able to push to this protected branch."
redirect_to namespace_project_protected_branch_path(@project.namespace, @project, @protected_branch)
end
end
end
end
......@@ -25,6 +25,7 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def show
@matching_branches = @protected_branch.matching(@project.repository.branches)
gon.push(js_access_levels)
end
def update
......@@ -58,8 +59,8 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def protected_branch_params
params.require(:protected_branch).permit(:name,
merge_access_levels_attributes: [:access_level, :id],
push_access_levels_attributes: [:access_level, :id])
merge_access_levels_attributes: [:access_level, :id, :user_id],
push_access_levels_attributes: [:access_level, :id, :user_id])
end
def load_protected_branches
......@@ -69,7 +70,9 @@ class Projects::ProtectedBranchesController < Projects::ApplicationController
def access_levels_options
{
push_access_levels: ProtectedBranch::PushAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } }
merge_access_levels: ProtectedBranch::MergeAccessLevel.human_access_levels.map { |id, text| { id: id, text: text, before_divider: true } },
selected_merge_access_levels: @protected_branch.merge_access_levels.map { |access_level| access_level.user_id || access_level.access_level },
selected_push_access_levels: @protected_branch.push_access_levels.map { |access_level| access_level.user_id || access_level.access_level }
}
end
......
......@@ -7,7 +7,11 @@ module DropdownsHelper
data_attr = options[:data].merge(data_attr)
end
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
if options.has_key?(:toggle_link)
dropdown_output = dropdown_toggle_link(toggle_text, data_attr, options)
else
dropdown_output = dropdown_toggle(toggle_text, data_attr, options)
end
dropdown_output << content_tag(:div, class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.has_key?(:dropdown_class)}") do
output = ""
......@@ -47,6 +51,11 @@ module DropdownsHelper
end
end
def dropdown_toggle_link(toggle_text, data_attr, options = {})
output = content_tag(:a, toggle_text, class: "dropdown-toggle-text #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), data: data_attr)
output.html_safe
end
def dropdown_title(title, back: false)
content_tag :div, class: "dropdown-title" do
title_output = ""
......
- url = namespace_project_protected_branch_path(@project.namespace, @project, @protected_branch)
%h5 Access Settings
.form-group.allowed-to-merge-container
.prepend-left-10
%h5.label-light.append-bottom-20 Allowed to merge
- if @protected_branch.merge_access_levels.present?
.table-responsive
%table.table.protected-branch-merge-access-list
%colgroup
%col{ width: "70%" }
%col{ width: "30%" }
%thead
%tr
%th User / Role
%th
%tbody
- @protected_branch.merge_access_levels.each do |access_level|
%tr
%td= access_level.humanize
%td
%button.btn.btn-sm.btn-warning.pull-right= link_to "Delete", namespace_project_protected_branch_merge_access_level_path(@project.namespace, @project, @protected_branch, access_level), method: :delete, data: { confirm: "Are you sure?" }
- else
%p.settings-message.text-center
No merge access settings have been created yet.
= dropdown_tag("Add new", options: { toggle_class: 'allowed-to-merge btn btn-success btn-sm', toggle_link: true,
dropdown_class: 'dropdown-menu-selectable dropdown-menu-user', filter: true,
data: { url: url, type: 'merge_access_levels' }})
.form-group.allowed-to-push-container
.prepend-left-10.prepend-top-10
%h5.label-light.append-bottom-20 Allowed to push
- if @protected_branch.push_access_levels.present?
.table-responsive
%table.table.protected-branch-push-access-list
%colgroup
%col{ width: "70%" }
%col{ width: "30%" }
%thead
%tr
%th User / Role
%th
%tbody
- @protected_branch.push_access_levels.each do |access_level|
%tr
%td= access_level.humanize
%td
%button.btn.btn-sm.btn-warning.pull-right= link_to "Delete", namespace_project_protected_branch_push_access_level_path(@project.namespace, @project, @protected_branch, access_level), method: :delete, data: { confirm: "Are you sure?" }
- else
%p.settings-message.text-center
No push access settings have been created yet.
= dropdown_tag("Add new",
options: { toggle_class: 'allowed-to-push btn btn-success btn-sm', toggle_link: true,
dropdown_class: 'dropdown-menu-selectable dropdown-menu-user', filter: true,
data: { url: url, type: 'push_access_levels' }})
......@@ -20,6 +20,7 @@
%th Last commit
%th Allowed to merge
%th Allowed to push
%th
- if can_admin_project
%th
%tbody
......
......@@ -14,7 +14,10 @@
- else
(branch was removed from repository)
= render partial: 'update_protected_branch', locals: { protected_branch: protected_branch }
= render partial: 'protected_branch_access_summary', locals: { protected_branch: protected_branch }
%td
= link_to "Settings", namespace_project_protected_branch_path(@project.namespace, @project, protected_branch), class: "btn btn-info"
- if can_admin_project
%td
......
%td
- access_by_type = protected_branch.merge_access_level_frequencies
- tooltip_text = protected_branch.merge_access_levels.map { |access_level| "<li>#{access_level.humanize}</li>" }.join
%span.has-tooltip{ title: tooltip_text, data: { container: "body", html: 1 } }= [pluralize(access_by_type[:user], 'user'), pluralize(access_by_type[:role], 'role')].to_sentence
%td
- access_by_type = protected_branch.push_access_level_frequencies
- tooltip_text = protected_branch.push_access_levels.map { |access_level| "<li>#{access_level.humanize}</li>" }.join
%span.has-tooltip{ title: tooltip_text, data: { container: "body", html: 1 } }= [pluralize(access_by_type[:user], 'user'), pluralize(access_by_type[:role], 'role')].to_sentence
......@@ -5,7 +5,11 @@
%h4.prepend-top-0
= @protected_branch.name
.col-lg-9
.col-lg-9.edit_protected_branch
= render 'access_settings'
%hr
%h5 Matching Branches
- if @matching_branches.present?
.table-responsive
......@@ -23,3 +27,6 @@
- else
%p.settings-message.text-center
Couldn't find any matching branches.
:javascript
new ProtectedBranchesAccessSelect($(".edit_protected_branch"), true);
......@@ -807,7 +807,13 @@ Rails.application.routes.draw do
end
end
resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :protected_branches, only: [:index, :show, :create, :update, :destroy, :patch], constraints: { id: Gitlab::Regex.git_reference_regex } do
scope module: :protected_branches do
resources :merge_access_levels, only: [:destroy]
resources :push_access_levels, only: [:destroy]
end
end
resources :variables, only: [:index, :show, :update, :create, :destroy]
resources :triggers, only: [:index, :create, :destroy]
resource :mirror, only: [:show, :update] do
......
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