Skip to content
Snippets Groups Projects
Commit f7547d81 authored by Timothy Andrew's avatar Timothy Andrew Committed by Alfredo Sumaran
Browse files

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
No related branches found
No related tags found
1 merge request!581Restrict pushes / merges to a protected branch to specific people
Showing
with 274 additions and 7 deletions
// 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;
}
}
Loading
Loading
@@ -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;
Loading
Loading
Loading
Loading
@@ -25,6 +25,7 @@ def project
 
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
Loading
Loading
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
Loading
Loading
@@ -25,6 +25,7 @@ def create
 
def show
@matching_branches = @protected_branch.matching(@project.repository.branches)
gon.push(js_access_levels)
end
 
def update
Loading
Loading
@@ -58,8 +59,8 @@ def load_protected_branch
 
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
Loading
Loading
@@ -69,7 +70,9 @@ def load_protected_branches
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
 
Loading
Loading
Loading
Loading
@@ -7,7 +7,11 @@ def dropdown_tag(toggle_text, options: {}, &block)
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 = ""
Loading
Loading
@@ -47,6 +51,11 @@ def dropdown_toggle(toggle_text, data_attr, options = {})
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 = ""
Loading
Loading
- 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' }})
Loading
Loading
@@ -20,6 +20,7 @@
%th Last commit
%th Allowed to merge
%th Allowed to push
%th
- if can_admin_project
%th
%tbody
Loading
Loading
Loading
Loading
@@ -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
Loading
Loading
%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
Loading
Loading
@@ -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
Loading
Loading
@@ -23,3 +27,6 @@
- else
%p.settings-message.text-center
Couldn't find any matching branches.
:javascript
new ProtectedBranchesAccessSelect($(".edit_protected_branch"), true);
Loading
Loading
@@ -807,7 +807,13 @@
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
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment