Merge branch 'master' into rs-sign_in
This commit is contained in:
commit
75f3f6e1d6
|
@ -362,6 +362,7 @@ db:migrate:reset-mysql:
|
|||
- git fetch origin v8.14.10
|
||||
- git checkout -f FETCH_HEAD
|
||||
- bundle install $BUNDLE_INSTALL_FLAGS
|
||||
- cp config/gitlab.yml.example config/gitlab.yml
|
||||
- bundle exec rake db:drop db:create db:schema:load db:seed_fu
|
||||
- git checkout $CI_COMMIT_SHA
|
||||
- bundle install $BUNDLE_INSTALL_FLAGS
|
||||
|
|
|
@ -1 +1 @@
|
|||
0.15.0
|
||||
0.16.0
|
||||
|
|
2
Gemfile
2
Gemfile
|
@ -386,7 +386,7 @@ gem 'vmstat', '~> 2.3.0'
|
|||
gem 'sys-filesystem', '~> 1.1.6'
|
||||
|
||||
# Gitaly GRPC client
|
||||
gem 'gitaly', '~> 0.13.0'
|
||||
gem 'gitaly', '~> 0.14.0'
|
||||
|
||||
gem 'toml-rb', '~> 0.3.15', require: false
|
||||
|
||||
|
|
|
@ -278,7 +278,7 @@ GEM
|
|||
po_to_json (>= 1.0.0)
|
||||
rails (>= 3.2.0)
|
||||
gherkin-ruby (0.3.2)
|
||||
gitaly (0.13.0)
|
||||
gitaly (0.14.0)
|
||||
google-protobuf (~> 3.1)
|
||||
grpc (~> 1.0)
|
||||
github-linguist (4.7.6)
|
||||
|
@ -980,7 +980,7 @@ DEPENDENCIES
|
|||
gettext (~> 3.2.2)
|
||||
gettext_i18n_rails (~> 1.8.0)
|
||||
gettext_i18n_rails_js (~> 1.2.0)
|
||||
gitaly (~> 0.13.0)
|
||||
gitaly (~> 0.14.0)
|
||||
github-linguist (~> 4.7.0)
|
||||
gitlab-flowdock-git-hook (~> 1.0.1)
|
||||
gitlab-markup (~> 1.5.1)
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
import DropLab from './droplab/drop_lab';
|
||||
import ISetter from './droplab/plugins/input_setter';
|
||||
|
||||
// Todo: Remove this when fixing issue in input_setter plugin
|
||||
const InputSetter = Object.assign({}, ISetter);
|
||||
|
||||
class CloseReopenReportToggle {
|
||||
constructor(opts = {}) {
|
||||
this.dropdownTrigger = opts.dropdownTrigger;
|
||||
this.dropdownList = opts.dropdownList;
|
||||
this.button = opts.button;
|
||||
}
|
||||
|
||||
initDroplab() {
|
||||
this.reopenItem = this.dropdownList.querySelector('.reopen-item');
|
||||
this.closeItem = this.dropdownList.querySelector('.close-item');
|
||||
|
||||
this.droplab = new DropLab();
|
||||
|
||||
const config = this.setConfig();
|
||||
|
||||
this.droplab.init(this.dropdownTrigger, this.dropdownList, [InputSetter], config);
|
||||
}
|
||||
|
||||
updateButton(isClosed) {
|
||||
this.toggleButtonType(isClosed);
|
||||
|
||||
this.button.blur();
|
||||
}
|
||||
|
||||
toggleButtonType(isClosed) {
|
||||
const [showItem, hideItem] = this.getButtonTypes(isClosed);
|
||||
|
||||
showItem.classList.remove('hidden');
|
||||
showItem.classList.add('droplab-item-selected');
|
||||
|
||||
hideItem.classList.add('hidden');
|
||||
hideItem.classList.remove('droplab-item-selected');
|
||||
|
||||
showItem.click();
|
||||
}
|
||||
|
||||
getButtonTypes(isClosed) {
|
||||
return isClosed ? [this.reopenItem, this.closeItem] : [this.closeItem, this.reopenItem];
|
||||
}
|
||||
|
||||
setDisable(shouldDisable = true) {
|
||||
if (shouldDisable) {
|
||||
this.button.setAttribute('disabled', 'true');
|
||||
this.dropdownTrigger.setAttribute('disabled', 'true');
|
||||
} else {
|
||||
this.button.removeAttribute('disabled');
|
||||
this.dropdownTrigger.removeAttribute('disabled');
|
||||
}
|
||||
}
|
||||
|
||||
setConfig() {
|
||||
const config = {
|
||||
InputSetter: [
|
||||
{
|
||||
input: this.button,
|
||||
valueAttribute: 'data-text',
|
||||
inputAttribute: 'data-value',
|
||||
},
|
||||
{
|
||||
input: this.button,
|
||||
valueAttribute: 'data-text',
|
||||
inputAttribute: 'title',
|
||||
},
|
||||
{
|
||||
input: this.button,
|
||||
valueAttribute: 'data-button-class',
|
||||
inputAttribute: 'class',
|
||||
},
|
||||
{
|
||||
input: this.dropdownTrigger,
|
||||
valueAttribute: 'data-toggle-class',
|
||||
inputAttribute: 'class',
|
||||
},
|
||||
{
|
||||
input: this.button,
|
||||
valueAttribute: 'data-url',
|
||||
inputAttribute: 'href',
|
||||
},
|
||||
{
|
||||
input: this.button,
|
||||
valueAttribute: 'data-method',
|
||||
inputAttribute: 'data-method',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
return config;
|
||||
}
|
||||
}
|
||||
|
||||
export default CloseReopenReportToggle;
|
|
@ -1,5 +1,8 @@
|
|||
import DropLab from './droplab/drop_lab';
|
||||
import InputSetter from './droplab/plugins/input_setter';
|
||||
import ISetter from './droplab/plugins/input_setter';
|
||||
|
||||
// Todo: Remove this when fixing issue in input_setter plugin
|
||||
const InputSetter = Object.assign({}, ISetter);
|
||||
|
||||
class CommentTypeToggle {
|
||||
constructor(opts = {}) {
|
||||
|
|
|
@ -30,6 +30,7 @@ class GfmAutoComplete {
|
|||
this.input.each((i, input) => {
|
||||
const $input = $(input);
|
||||
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
|
||||
$input.on('change.atwho', () => input.dispatchEvent(new Event('input')));
|
||||
// This triggers at.js again
|
||||
// Needed for quick actions with suffixes (ex: /label ~)
|
||||
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import CloseReopenReportToggle from '../close_reopen_report_toggle';
|
||||
|
||||
function initCloseReopenReport() {
|
||||
const container = document.querySelector('.js-issuable-close-dropdown');
|
||||
|
||||
if (!container) return undefined;
|
||||
|
||||
const dropdownTrigger = container.querySelector('.js-issuable-close-toggle');
|
||||
const dropdownList = container.querySelector('.js-issuable-close-menu');
|
||||
const button = container.querySelector('.js-issuable-close-button');
|
||||
|
||||
const closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
closeReopenReportToggle.initDroplab();
|
||||
|
||||
return closeReopenReportToggle;
|
||||
}
|
||||
|
||||
const IssuablesHelper = {
|
||||
initCloseReopenReport,
|
||||
};
|
||||
|
||||
export default IssuablesHelper;
|
|
@ -6,6 +6,7 @@ import '~/lib/utils/text_utility';
|
|||
import './flash';
|
||||
import TaskList from './task_list';
|
||||
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
|
||||
import IssuablesHelper from './helpers/issuables_helper';
|
||||
|
||||
class Issue {
|
||||
constructor() {
|
||||
|
@ -28,6 +29,11 @@ class Issue {
|
|||
Issue.initMergeRequests();
|
||||
Issue.initRelatedBranches();
|
||||
|
||||
this.closeButtons = $('a.btn-close');
|
||||
this.reopenButtons = $('a.btn-reopen');
|
||||
|
||||
this.initCloseReopenReport();
|
||||
|
||||
if (Issue.createMrDropdownWrap) {
|
||||
this.createMergeRequestDropdown = new CreateMergeRequestDropdown(Issue.createMrDropdownWrap);
|
||||
}
|
||||
|
@ -35,13 +41,8 @@ class Issue {
|
|||
|
||||
initIssueBtnEventListeners() {
|
||||
const issueFailMessage = 'Unable to update this issue at this time.';
|
||||
const closeButtons = $('a.btn-close');
|
||||
const isClosedBadge = $('div.status-box-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
const reopenButtons = $('a.btn-reopen');
|
||||
|
||||
return closeButtons.add(reopenButtons).on('click', (e) => {
|
||||
return $(document).on('click', 'a.btn-close, a.btn-reopen', (e) => {
|
||||
var $button, shouldSubmit, url;
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
|
@ -50,7 +51,9 @@ class Issue {
|
|||
if (shouldSubmit) {
|
||||
Issue.submitNoteForm($button.closest('form'));
|
||||
}
|
||||
$button.prop('disabled', true);
|
||||
|
||||
this.disableCloseReopenButton($button);
|
||||
|
||||
url = $button.attr('href');
|
||||
return $.ajax({
|
||||
type: 'PUT',
|
||||
|
@ -58,15 +61,19 @@ class Issue {
|
|||
})
|
||||
.fail(() => new Flash(issueFailMessage))
|
||||
.done((data) => {
|
||||
const isClosedBadge = $('div.status-box-closed');
|
||||
const isOpenBadge = $('div.status-box-open');
|
||||
const projectIssuesCounter = $('.issue_counter');
|
||||
|
||||
if ('id' in data) {
|
||||
$(document).trigger('issuable:change');
|
||||
|
||||
const isClosed = $button.hasClass('btn-close');
|
||||
closeButtons.toggleClass('hidden', isClosed);
|
||||
reopenButtons.toggleClass('hidden', !isClosed);
|
||||
isClosedBadge.toggleClass('hidden', !isClosed);
|
||||
isOpenBadge.toggleClass('hidden', isClosed);
|
||||
|
||||
this.toggleCloseReopenButton(isClosed);
|
||||
|
||||
let numProjectIssues = Number(projectIssuesCounter.text().replace(/[^\d]/, ''));
|
||||
numProjectIssues = isClosed ? numProjectIssues - 1 : numProjectIssues + 1;
|
||||
projectIssuesCounter.text(gl.text.addDelimiter(numProjectIssues));
|
||||
|
@ -83,12 +90,34 @@ class Issue {
|
|||
} else {
|
||||
new Flash(issueFailMessage);
|
||||
}
|
||||
|
||||
$button.prop('disabled', false);
|
||||
})
|
||||
.then(() => {
|
||||
this.disableCloseReopenButton($button, false);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initCloseReopenReport() {
|
||||
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
|
||||
|
||||
if (this.closeButtons) this.closeButtons = this.closeButtons.not('.issuable-close-button');
|
||||
if (this.reopenButtons) this.reopenButtons = this.reopenButtons.not('.issuable-close-button');
|
||||
}
|
||||
|
||||
disableCloseReopenButton($button, shouldDisable) {
|
||||
if (this.closeReopenReportToggle) {
|
||||
this.closeReopenReportToggle.setDisable(shouldDisable);
|
||||
} else {
|
||||
$button.prop('disabled', shouldDisable);
|
||||
}
|
||||
}
|
||||
|
||||
toggleCloseReopenButton(isClosed) {
|
||||
if (this.closeReopenReportToggle) this.closeReopenReportToggle.updateButton(isClosed);
|
||||
this.closeButtons.toggleClass('hidden', isClosed);
|
||||
this.reopenButtons.toggleClass('hidden', !isClosed);
|
||||
}
|
||||
|
||||
static submitNoteForm(form) {
|
||||
var noteText;
|
||||
noteText = form.find("textarea.js-note-text").val();
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
import 'vendor/jquery.waitforimages';
|
||||
import TaskList from './task_list';
|
||||
import './merge_request_tabs';
|
||||
import IssuablesHelper from './helpers/issuables_helper';
|
||||
|
||||
(function() {
|
||||
this.MergeRequest = (function() {
|
||||
|
@ -21,9 +22,12 @@ import './merge_request_tabs';
|
|||
return _this.showAllCommits();
|
||||
};
|
||||
})(this));
|
||||
|
||||
this.initTabs();
|
||||
this.initMRBtnListeners();
|
||||
this.initCommitMessageListeners();
|
||||
this.closeReopenReportToggle = IssuablesHelper.initCloseReopenReport();
|
||||
|
||||
if ($("a.btn-close").length) {
|
||||
this.taskList = new TaskList({
|
||||
dataType: 'merge_request',
|
||||
|
@ -64,11 +68,15 @@ import './merge_request_tabs';
|
|||
if (shouldSubmit && $this.data('submitted')) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.closeReopenReportToggle) this.closeReopenReportToggle.setDisable();
|
||||
|
||||
if (shouldSubmit) {
|
||||
if ($this.hasClass('btn-comment-and-close') || $this.hasClass('btn-comment-and-reopen')) {
|
||||
e.preventDefault();
|
||||
e.stopImmediatePropagation();
|
||||
return _this.submitNoteForm($this.closest('form'), $this);
|
||||
|
||||
_this.submitNoteForm($this.closest('form'), $this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
@ -62,7 +62,7 @@ import findAndFollowLink from './shortcuts_dashboard_navigation';
|
|||
if (Cookies.get(performanceBarCookieName) === 'true') {
|
||||
Cookies.remove(performanceBarCookieName, { path: '/' });
|
||||
} else {
|
||||
Cookies.set(performanceBarCookieName, true, { path: '/' });
|
||||
Cookies.set(performanceBarCookieName, 'true', { path: '/' });
|
||||
}
|
||||
gl.utils.refreshCurrentPage();
|
||||
};
|
||||
|
|
|
@ -20,17 +20,29 @@
|
|||
color: $text;
|
||||
border-color: $border;
|
||||
|
||||
> .icon {
|
||||
color: $text;
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $hover-background;
|
||||
border-color: $hover-border;
|
||||
color: $hover-text;
|
||||
|
||||
> .icon {
|
||||
color: $hover-text;
|
||||
}
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: $active-background;
|
||||
border-color: $active-border;
|
||||
color: $hover-text;
|
||||
|
||||
> .icon {
|
||||
color: $hover-text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -163,7 +175,8 @@
|
|||
@include btn-orange;
|
||||
}
|
||||
|
||||
&.btn-close {
|
||||
&.btn-close,
|
||||
&.btn-close-color {
|
||||
@include btn-outline($white-light, $orange-600, $orange-500, $orange-500, $white-light, $orange-600, $orange-600, $orange-700);
|
||||
}
|
||||
|
||||
|
@ -181,7 +194,8 @@
|
|||
float: right;
|
||||
}
|
||||
|
||||
&.btn-reopen {
|
||||
&.btn-reopen,
|
||||
.btn-reopen-color {
|
||||
/* should be same as parent class for now */
|
||||
}
|
||||
|
||||
|
|
|
@ -295,9 +295,74 @@
|
|||
}
|
||||
}
|
||||
|
||||
.filtered-search-box-input-container .dropdown-menu,
|
||||
.filtered-search-box-input-container .dropdown-menu-nav,
|
||||
.comment-type-dropdown .dropdown-menu {
|
||||
.droplab-dropdown {
|
||||
.description {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.dropdown-toggle > i {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
li {
|
||||
padding: $gl-btn-padding $gl-btn-padding 2px;
|
||||
cursor: pointer;
|
||||
|
||||
> a,
|
||||
> button {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-radius: 0;
|
||||
text-overflow: inherit;
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
border: inherit;
|
||||
text-align: left;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
&.btn .fa:not(:last-child) {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $dropdown-hover-color;
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
&.droplab-item-selected i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
.icon {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 8px;
|
||||
padding: 0;
|
||||
border-top: $gray-darkest;
|
||||
}
|
||||
}
|
||||
|
||||
.droplab-dropdown .dropdown-menu,
|
||||
.droplab-dropdown .dropdown-menu-nav {
|
||||
display: none;
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
|
|
|
@ -70,6 +70,13 @@
|
|||
|
||||
.input-token {
|
||||
max-width: 200px;
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.input-token:only-child,
|
||||
|
@ -156,6 +163,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.droplab-dropdown li.filtered-search-token {
|
||||
padding: 0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.filtered-search-term {
|
||||
.name {
|
||||
background-color: inherit;
|
||||
|
|
|
@ -265,7 +265,7 @@ $diff-view-modes-border: #c1c1c1;
|
|||
/*
|
||||
* Fonts
|
||||
*/
|
||||
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
|
||||
$monospace_font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
|
||||
$regular_font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
|
||||
/*
|
||||
|
|
|
@ -31,6 +31,12 @@ $new-sidebar-width: 220px;
|
|||
&:hover {
|
||||
background-color: $border-color;
|
||||
}
|
||||
|
||||
.project-title,
|
||||
.group-title {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
.settings-avatar {
|
||||
|
|
|
@ -799,3 +799,28 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.issuable-close-button,
|
||||
.issuable-close-toggle {
|
||||
@include transition(border-color, color);
|
||||
}
|
||||
|
||||
.issuable-close-dropdown {
|
||||
.dropdown-menu {
|
||||
min-width: 270px;
|
||||
left: auto;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 10px;
|
||||
|
||||
.text {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle > .icon {
|
||||
margin: 0 3px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -356,7 +356,6 @@
|
|||
color: $white-light;
|
||||
padding-right: 2px;
|
||||
margin-top: 2px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -366,56 +365,6 @@
|
|||
width: 298px;
|
||||
}
|
||||
|
||||
.description {
|
||||
display: inline-block;
|
||||
white-space: normal;
|
||||
margin-left: 8px;
|
||||
padding-right: 33px;
|
||||
}
|
||||
|
||||
li {
|
||||
padding-top: 6px;
|
||||
|
||||
& > a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: inherit;
|
||||
border-radius: 0;
|
||||
text-overflow: inherit;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: inherit;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: $dropdown-hover-color;
|
||||
color: $white-light;
|
||||
}
|
||||
|
||||
&.droplab-item-selected i {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
i {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
i {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
padding-top: 2px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 0 8px;
|
||||
padding: 0;
|
||||
border-top: $gray-darkest;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-xs-max) {
|
||||
display: flex;
|
||||
|
|
|
@ -126,6 +126,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
|
|||
:metrics_port,
|
||||
:metrics_sample_interval,
|
||||
:metrics_timeout,
|
||||
:performance_bar_allowed_group_id,
|
||||
:performance_bar_enabled,
|
||||
:recaptcha_enabled,
|
||||
:recaptcha_private_key,
|
||||
:recaptcha_site_key,
|
||||
|
|
|
@ -9,7 +9,7 @@ class ApplicationController < ActionController::Base
|
|||
include SentryHelper
|
||||
include WorkhorseHelper
|
||||
include EnforcesTwoFactorAuthentication
|
||||
include Peek::Rblineprof::CustomControllerHelpers
|
||||
include WithPerformanceBar
|
||||
|
||||
before_action :authenticate_user_from_private_token!
|
||||
before_action :authenticate_user_from_rss_token!
|
||||
|
@ -68,21 +68,6 @@ class ApplicationController < ActionController::Base
|
|||
end
|
||||
end
|
||||
|
||||
def peek_enabled?
|
||||
return false unless Gitlab::PerformanceBar.enabled?
|
||||
return false unless current_user
|
||||
|
||||
if RequestStore.active?
|
||||
if RequestStore.store.key?(:peek_enabled)
|
||||
RequestStore.store[:peek_enabled]
|
||||
else
|
||||
RequestStore.store[:peek_enabled] = cookies[:perf_bar_enabled].present?
|
||||
end
|
||||
else
|
||||
cookies[:perf_bar_enabled].present?
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
# This filter handles both private tokens and personal access tokens
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
module WithPerformanceBar
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include Peek::Rblineprof::CustomControllerHelpers
|
||||
end
|
||||
|
||||
def peek_enabled?
|
||||
return false unless Gitlab::PerformanceBar.enabled?(current_user)
|
||||
|
||||
if RequestStore.active?
|
||||
RequestStore.fetch(:peek_enabled) { cookies[:perf_bar_enabled].present? }
|
||||
else
|
||||
cookies[:perf_bar_enabled].present?
|
||||
end
|
||||
end
|
||||
end
|
|
@ -17,8 +17,7 @@ class Projects::MergeRequests::ApplicationController < Projects::ApplicationCont
|
|||
end
|
||||
|
||||
def merge_request_params
|
||||
params.require(:merge_request)
|
||||
.permit(merge_request_params_attributes)
|
||||
params.require(:merge_request).permit(merge_request_params_attributes)
|
||||
end
|
||||
|
||||
def merge_request_params_attributes
|
||||
|
|
|
@ -245,6 +245,53 @@ module IssuablesHelper
|
|||
@counts[cache_key][state]
|
||||
end
|
||||
|
||||
def close_issuable_url(issuable)
|
||||
issuable_url(issuable, close_reopen_params(issuable, :close))
|
||||
end
|
||||
|
||||
def reopen_issuable_url(issuable)
|
||||
issuable_url(issuable, close_reopen_params(issuable, :reopen))
|
||||
end
|
||||
|
||||
def close_reopen_issuable_url(issuable, should_inverse = false)
|
||||
issuable.closed? ^ should_inverse ? reopen_issuable_url(issuable) : close_issuable_url(issuable)
|
||||
end
|
||||
|
||||
def issuable_url(issuable, *options)
|
||||
case issuable
|
||||
when Issue
|
||||
issue_url(issuable, *options)
|
||||
when MergeRequest
|
||||
merge_request_url(issuable, *options)
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_button_visibility(issuable, closed)
|
||||
case issuable
|
||||
when Issue
|
||||
issue_button_visibility(issuable, closed)
|
||||
when MergeRequest
|
||||
merge_request_button_visibility(issuable, closed)
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_close_reopen_button_method(issuable)
|
||||
case issuable
|
||||
when Issue
|
||||
''
|
||||
when MergeRequest
|
||||
'put'
|
||||
end
|
||||
end
|
||||
|
||||
def issuable_author_is_current_user(issuable)
|
||||
issuable.author == current_user
|
||||
end
|
||||
|
||||
def issuable_display_type(issuable)
|
||||
issuable.model_name.human.downcase
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def sidebar_gutter_collapsed?
|
||||
|
@ -270,8 +317,6 @@ module IssuablesHelper
|
|||
issue_template_names
|
||||
when MergeRequest
|
||||
merge_request_template_names
|
||||
else
|
||||
raise 'Unknown issuable type!'
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -301,4 +346,12 @@ module IssuablesHelper
|
|||
container: (is_collapsed ? 'body' : nil)
|
||||
}
|
||||
end
|
||||
|
||||
def close_reopen_params(issuable, action)
|
||||
{
|
||||
issuable.model_name.to_s.underscore => { state_event: action }
|
||||
}.tap do |params|
|
||||
params[:format] = :json if issuable.is_a?(Issue)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -234,6 +234,7 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
koding_url: nil,
|
||||
max_artifacts_size: Settings.artifacts['max_size'],
|
||||
max_attachment_size: Settings.gitlab['max_attachment_size'],
|
||||
performance_bar_allowed_group_id: nil,
|
||||
plantuml_enabled: false,
|
||||
plantuml_url: nil,
|
||||
recaptcha_enabled: false,
|
||||
|
@ -336,6 +337,48 @@ class ApplicationSetting < ActiveRecord::Base
|
|||
super(levels.map { |level| Gitlab::VisibilityLevel.level_value(level) })
|
||||
end
|
||||
|
||||
def performance_bar_allowed_group_id=(group_full_path)
|
||||
group_full_path = nil if group_full_path.blank?
|
||||
|
||||
if group_full_path.nil?
|
||||
if group_full_path != performance_bar_allowed_group_id
|
||||
super(group_full_path)
|
||||
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
|
||||
end
|
||||
return
|
||||
end
|
||||
|
||||
group = Group.find_by_full_path(group_full_path)
|
||||
|
||||
if group
|
||||
if group.id != performance_bar_allowed_group_id
|
||||
super(group.id)
|
||||
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
|
||||
end
|
||||
else
|
||||
super(nil)
|
||||
Gitlab::PerformanceBar.expire_allowed_user_ids_cache
|
||||
end
|
||||
end
|
||||
|
||||
def performance_bar_allowed_group
|
||||
Group.find_by_id(performance_bar_allowed_group_id)
|
||||
end
|
||||
|
||||
# Return true if the Performance Bar is enabled for a given group
|
||||
def performance_bar_enabled
|
||||
performance_bar_allowed_group_id.present?
|
||||
end
|
||||
|
||||
# - If `enable` is true, we early return since the actual attribute that holds
|
||||
# the enabling/disabling is `performance_bar_allowed_group_id`
|
||||
# - If `enable` is false, we set `performance_bar_allowed_group_id` to `nil`
|
||||
def performance_bar_enabled=(enable)
|
||||
return if enable
|
||||
|
||||
self.performance_bar_allowed_group_id = nil
|
||||
end
|
||||
|
||||
# Choose one of the available repository storage options. Currently all have
|
||||
# equal weighting.
|
||||
def pick_repository_storage
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
module EachBatch
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
module ClassMethods
|
||||
# Iterates over the rows in a relation in batches, similar to Rails'
|
||||
# `in_batches` but in a more efficient way.
|
||||
#
|
||||
# Unlike `in_batches` provided by Rails this method does not support a
|
||||
# custom start/end range, nor does it provide support for the `load:`
|
||||
# keyword argument.
|
||||
#
|
||||
# This method will yield an ActiveRecord::Relation to the supplied block, or
|
||||
# return an Enumerator if no block is given.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# User.each_batch do |relation|
|
||||
# relation.update_all(updated_at: Time.now)
|
||||
# end
|
||||
#
|
||||
# The supplied block is also passed an optional batch index:
|
||||
#
|
||||
# User.each_batch do |relation, index|
|
||||
# puts index # => 1, 2, 3, ...
|
||||
# end
|
||||
#
|
||||
# You can also specify an alternative column to use for ordering the rows:
|
||||
#
|
||||
# User.each_batch(column: :created_at) do |relation|
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# This will produce SQL queries along the lines of:
|
||||
#
|
||||
# User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
|
||||
# (0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
|
||||
#
|
||||
# of - The number of rows to retrieve per batch.
|
||||
# column - The column to use for ordering the batches.
|
||||
def each_batch(of: 1000, column: primary_key)
|
||||
unless column
|
||||
raise ArgumentError,
|
||||
'the column: argument must be set to a column name to use for ordering rows'
|
||||
end
|
||||
|
||||
start = except(:select)
|
||||
.select(column)
|
||||
.reorder(column => :asc)
|
||||
.take
|
||||
|
||||
return unless start
|
||||
|
||||
start_id = start[column]
|
||||
arel_table = self.arel_table
|
||||
|
||||
1.step do |index|
|
||||
stop = except(:select)
|
||||
.select(column)
|
||||
.where(arel_table[column].gteq(start_id))
|
||||
.reorder(column => :asc)
|
||||
.offset(of)
|
||||
.limit(1)
|
||||
.take
|
||||
|
||||
relation = where(arel_table[column].gteq(start_id))
|
||||
|
||||
if stop
|
||||
stop_id = stop[column]
|
||||
start_id = stop_id
|
||||
relation = relation.where(arel_table[column].lt(stop_id))
|
||||
end
|
||||
|
||||
# Any ORDER BYs are useless for this relation and can lead to less
|
||||
# efficient UPDATE queries, hence we get rid of it.
|
||||
yield relation.except(:order), index
|
||||
|
||||
break unless stop
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,5 +1,5 @@
|
|||
class GitlabIssueTrackerService < IssueTrackerService
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
|
||||
validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
class JiraService < IssueTrackerService
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
|
||||
validates :url, url: true, presence: true, if: :activated?
|
||||
validates :api_url, url: true, allow_blank: true
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module ChatNames
|
||||
class AuthorizeUserService
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
|
||||
def initialize(service, params)
|
||||
@service = service
|
||||
|
|
|
@ -35,11 +35,12 @@ module MergeRequests
|
|||
# target branch manually
|
||||
def close_merge_requests
|
||||
commit_ids = @commits.map(&:id)
|
||||
merge_requests = @project.merge_requests.opened.where(target_branch: @branch_name).to_a
|
||||
merge_requests = @project.merge_requests.preload(:merge_request_diff).opened.where(target_branch: @branch_name).to_a
|
||||
merge_requests = merge_requests.select(&:diff_head_commit)
|
||||
|
||||
merge_requests = merge_requests.select do |merge_request|
|
||||
commit_ids.include?(merge_request.diff_head_sha)
|
||||
commit_ids.include?(merge_request.diff_head_sha) &&
|
||||
merge_request.merge_request_diff.state != 'empty'
|
||||
end
|
||||
|
||||
filter_merge_requests(merge_requests).each do |merge_request|
|
||||
|
|
|
@ -332,6 +332,22 @@
|
|||
%strong.cred WARNING:
|
||||
Environment variable `prometheus_multiproc_dir` does not exist or is not pointing to a valid directory.
|
||||
|
||||
%fieldset
|
||||
%legend Profiling - Performance Bar
|
||||
%p
|
||||
Enable the Performance Bar for a given group.
|
||||
= link_to icon('question-circle'), help_page_path('administration/monitoring/performance/performance_bar')
|
||||
.form-group
|
||||
.col-sm-offset-2.col-sm-10
|
||||
.checkbox
|
||||
= f.label :performance_bar_enabled do
|
||||
= f.check_box :performance_bar_enabled
|
||||
Enable the Performance Bar
|
||||
.form-group
|
||||
= f.label :performance_bar_allowed_group_id, 'Allowed group', class: 'control-label col-sm-2'
|
||||
.col-sm-10
|
||||
= f.text_field :performance_bar_allowed_group_id, class: 'form-control', placeholder: 'my-org/my-group', value: @application_setting.performance_bar_allowed_group&.full_path
|
||||
|
||||
%fieldset
|
||||
%legend Background Jobs
|
||||
%p
|
||||
|
|
|
@ -12,10 +12,12 @@
|
|||
.content-wrapper{ class: "#{(layout_nav_class unless show_new_nav?)}" }
|
||||
.alert-wrapper
|
||||
= render "layouts/broadcast"
|
||||
- if show_new_nav?
|
||||
- if content_for?(:new_global_flash)
|
||||
= yield :new_global_flash
|
||||
= render "layouts/nav/breadcrumbs"
|
||||
= render "layouts/flash"
|
||||
= yield :flash_message
|
||||
- if show_new_nav?
|
||||
= render "layouts/nav/breadcrumbs"
|
||||
%div{ class: "#{(container_class unless @no_container)} #{@content_class}" }
|
||||
.content{ id: "content-body" }
|
||||
= yield
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
= icon('wrench')
|
||||
.project-title Admin Area
|
||||
%ul.sidebar-top-level-items
|
||||
= nav_link(controller: %w(dashboard admin projects users groups builds runners cohorts), html_options: {class: 'home'}) do
|
||||
= nav_link(controller: %w(dashboard admin projects users groups jobs runners cohorts), html_options: {class: 'home'}) do
|
||||
= link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
|
||||
%span
|
||||
Overview
|
||||
|
@ -26,7 +26,7 @@
|
|||
= link_to admin_groups_path, title: 'Groups' do
|
||||
%span
|
||||
Groups
|
||||
= nav_link path: 'builds#index' do
|
||||
= nav_link path: 'jobs#index' do
|
||||
= link_to admin_jobs_path, title: 'Jobs' do
|
||||
%span
|
||||
Jobs
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
.nav-sidebar
|
||||
= link_to group_path(@group), title: 'Group', class: 'context-header' do
|
||||
= link_to group_path(@group), title: @group.name, class: 'context-header' do
|
||||
.avatar-container.s40.group-avatar
|
||||
= image_tag group_icon(@group), class: "avatar s40 avatar-tile"
|
||||
.group-title
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
.nav-sidebar
|
||||
- can_edit = can?(current_user, :admin_project, @project)
|
||||
= link_to project_path(@project), title: 'Project', class: 'context-header' do
|
||||
= link_to project_path(@project), title: @project.name, class: 'context-header' do
|
||||
.avatar-container.s40.project-avatar
|
||||
= project_icon(@project, alt: @project.name, class: 'avatar s40 avatar-tile')
|
||||
.project-title
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
- @no_container = true
|
||||
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
|
||||
|
||||
= content_for :flash_message do
|
||||
= content_for flash_message_container do
|
||||
- if current_user && can?(current_user, :download_code, @project)
|
||||
= render 'shared/no_ssh'
|
||||
= render 'shared/no_password'
|
||||
|
|
|
@ -30,24 +30,23 @@
|
|||
.dropdown-menu.dropdown-menu-align-right.hidden-lg
|
||||
%ul
|
||||
- if can_update_issue
|
||||
%li
|
||||
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'issuable-edit'
|
||||
%li
|
||||
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
|
||||
%li
|
||||
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
|
||||
%li= link_to 'Edit', edit_project_issue_path(@project, @issue)
|
||||
- unless current_user == @issue.author
|
||||
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
|
||||
- if can_update_issue
|
||||
%li= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
|
||||
%li= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
|
||||
- if can_report_spam
|
||||
%li
|
||||
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
|
||||
%li= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'btn-spam', title: 'Submit as spam'
|
||||
- if can_update_issue || can_report_spam
|
||||
%li.divider
|
||||
%li
|
||||
= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
|
||||
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
|
||||
|
||||
- if can_update_issue
|
||||
= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped issuable-edit'
|
||||
= link_to 'Close issue', issue_path(@issue, issue: { state_event: :close }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
|
||||
= link_to 'Reopen issue', issue_path(@issue, issue: { state_event: :reopen }, format: 'json'), class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
|
||||
|
||||
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
|
||||
|
||||
- if can_report_spam
|
||||
= link_to 'Submit as spam', mark_as_spam_project_issue_path(@project, @issue), method: :post, class: 'hidden-xs hidden-sm btn btn-grouped btn-spam', title: 'Submit as spam'
|
||||
= link_to new_project_issue_path(@project), class: 'hidden-xs hidden-sm btn btn-grouped new-issue-link btn-new btn-inverted', title: 'New issue', id: 'new_issue_link' do
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
- can_update_merge_request = can?(current_user, :update_merge_request, @merge_request)
|
||||
|
||||
- if @merge_request.closed_without_fork?
|
||||
.alert.alert-danger
|
||||
%p The source project of this merge request has been removed.
|
||||
|
@ -15,21 +17,24 @@
|
|||
.issuable-meta
|
||||
= issuable_meta(@merge_request, @project, "Merge request")
|
||||
|
||||
- if can?(current_user, :update_merge_request, @merge_request)
|
||||
.issuable-actions
|
||||
.clearfix.issue-btn-group.dropdown
|
||||
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
|
||||
Options
|
||||
= icon('caret-down')
|
||||
.dropdown-menu.dropdown-menu-align-right.hidden-lg
|
||||
%ul
|
||||
.issuable-actions
|
||||
.clearfix.issue-btn-group.dropdown
|
||||
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
|
||||
Options
|
||||
= icon('caret-down')
|
||||
.dropdown-menu.dropdown-menu-align-right.hidden-lg
|
||||
%ul
|
||||
- if can_update_merge_request
|
||||
%li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
|
||||
- unless current_user == @merge_request.author
|
||||
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
|
||||
- if can_update_merge_request
|
||||
%li{ class: merge_request_button_visibility(@merge_request, true) }
|
||||
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
|
||||
%li{ class: merge_request_button_visibility(@merge_request, false) }
|
||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
|
||||
%li
|
||||
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: 'issuable-edit'
|
||||
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-close #{merge_request_button_visibility(@merge_request, true)}", title: 'Close merge request'
|
||||
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-grouped btn-reopen reopen-mr-link #{merge_request_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
|
||||
= link_to edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit" do
|
||||
Edit
|
||||
|
||||
- if can_update_merge_request
|
||||
= link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "hidden-xs hidden-sm btn btn-grouped issuable-edit"
|
||||
|
||||
= render 'shared/issuable/close_reopen_button', issuable: @merge_request, can_update: can_update_merge_request
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
- @no_container = true
|
||||
- flash_message_container = show_new_nav? ? :new_global_flash : :flash_message
|
||||
|
||||
= content_for :meta_tags do
|
||||
= auto_discovery_link_tag(:atom, project_path(@project, rss_url_options), title: "#{@project.name} activity")
|
||||
|
||||
= content_for :flash_message do
|
||||
= content_for flash_message_container do
|
||||
- if current_user && can?(current_user, :download_code, @project)
|
||||
= render 'shared/no_ssh'
|
||||
= render 'shared/no_password'
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
- is_current_user = issuable_author_is_current_user(issuable)
|
||||
- display_issuable_type = issuable_display_type(issuable)
|
||||
- button_method = issuable_close_reopen_button_method(issuable)
|
||||
|
||||
- if can_update && is_current_user
|
||||
= link_to "Close #{display_issuable_type}", close_issuable_url(issuable), method: button_method,
|
||||
class: "hidden-xs hidden-sm btn btn-grouped btn-close #{issuable_button_visibility(issuable, true)}", title: "Close #{display_issuable_type}"
|
||||
= link_to "Reopen #{display_issuable_type}", reopen_issuable_url(issuable), method: button_method,
|
||||
class: "hidden-xs hidden-sm btn btn-grouped btn-reopen #{issuable_button_visibility(issuable, false)}", title: "Reopen #{display_issuable_type}"
|
||||
- elsif can_update && !is_current_user
|
||||
= render 'shared/issuable/close_reopen_report_toggle', issuable: issuable
|
||||
- else
|
||||
= link_to 'Report abuse', new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
|
||||
class: 'hidden-xs hidden-sm btn btn-grouped btn-close-color', title: 'Report abuse'
|
|
@ -0,0 +1,49 @@
|
|||
- display_issuable_type = issuable_display_type(issuable)
|
||||
- button_action = issuable.closed? ? 'reopen' : 'close'
|
||||
- display_button_action = button_action.capitalize
|
||||
- button_responsive_class = 'hidden-xs hidden-sm'
|
||||
- button_class = "#{button_responsive_class} btn btn-grouped js-issuable-close-button issuable-close-button"
|
||||
- toggle_class = "#{button_responsive_class} btn btn-nr dropdown-toggle js-issuable-close-toggle"
|
||||
- button_method = issuable_close_reopen_button_method(issuable)
|
||||
|
||||
.pull-left.btn-group.prepend-left-10.issuable-close-dropdown.droplab-dropdown.js-issuable-close-dropdown
|
||||
= link_to "#{display_button_action} #{display_issuable_type}", close_reopen_issuable_url(issuable),
|
||||
method: button_method, class: "#{button_class} btn-#{button_action}", title: "#{display_button_action} #{display_issuable_type}"
|
||||
|
||||
= button_tag type: 'button', class: "#{toggle_class} btn-#{button_action}-color",
|
||||
data: { 'dropdown-trigger' => '#issuable-close-menu' }, 'aria-label' => 'Toggle dropdown' do
|
||||
= icon('caret-down', class: 'toggle-icon icon')
|
||||
|
||||
%ul#issuable-close-menu.js-issuable-close-menu.dropdown-menu{ class: button_responsive_class, data: { dropdown: true } }
|
||||
%li.close-item{ class: "#{issuable_button_visibility(issuable, true) || 'droplab-item-selected'}",
|
||||
data: { text: "Close #{display_issuable_type}", url: close_issuable_url(issuable),
|
||||
button_class: "#{button_class} btn-close", toggle_class: "#{toggle_class} btn-close-color", method: button_method } }
|
||||
%button.btn.btn-transparent
|
||||
= icon('check', class: 'icon')
|
||||
.description
|
||||
%strong.title
|
||||
Close
|
||||
= display_issuable_type
|
||||
|
||||
%li.reopen-item{ class: "#{issuable_button_visibility(issuable, false) || 'droplab-item-selected'}",
|
||||
data: { text: "Reopen #{display_issuable_type}", url: reopen_issuable_url(issuable),
|
||||
button_class: "#{button_class} btn-reopen", toggle_class: "#{toggle_class} btn-reopen-color", method: button_method } }
|
||||
%button.btn.btn-transparent
|
||||
= icon('check', class: 'icon')
|
||||
.description
|
||||
%strong.title
|
||||
Reopen
|
||||
= display_issuable_type
|
||||
|
||||
%li.divider.droplab-item-ignore
|
||||
|
||||
%li.report-item{ data: { text: 'Report abuse', url: new_abuse_report_path(user_id: issuable.author.id, ref_url: issuable_url(issuable)),
|
||||
button_class: "#{button_class} btn-close-color", toggle_class: "#{toggle_class} btn-close-color", method: '' } }
|
||||
%button.btn.btn-transparent
|
||||
= icon('check', class: 'icon')
|
||||
.description
|
||||
%strong.title Report abuse
|
||||
%p.text
|
||||
Report
|
||||
= display_issuable_type.pluralize
|
||||
that are abusive, inappropriate or spam.
|
|
@ -19,7 +19,7 @@
|
|||
content_class: "filtered-search-history-dropdown-content",
|
||||
title: "Recent searches" }) do
|
||||
.js-filtered-search-history-dropdown{ data: { project_full_path: @project.full_path } }
|
||||
.filtered-search-box-input-container
|
||||
.filtered-search-box-input-container.droplab-dropdown
|
||||
.scroll-container
|
||||
%ul.tokens-container.list-unstyled
|
||||
%li.input-token
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
- noteable_name = @note.noteable.human_class_name
|
||||
|
||||
.pull-left.btn-group.append-right-10.comment-type-dropdown.js-comment-type-dropdown
|
||||
.pull-left.btn-group.append-right-10.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
|
||||
%input.btn.btn-nr.btn-create.comment-btn.js-comment-button.js-comment-submit-button{ type: 'submit', value: 'Comment' }
|
||||
|
||||
- if @note.can_be_discussion_note?
|
||||
|
@ -9,8 +9,8 @@
|
|||
|
||||
%ul#resolvable-comment-menu.dropdown-menu{ data: { dropdown: true } }
|
||||
%li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => 'Comment', 'close-text' => "Comment & close #{noteable_name}", 'reopen-text' => "Comment & reopen #{noteable_name}" } }
|
||||
%a{ href: '#' }
|
||||
= icon('check')
|
||||
%button.btn.btn-transparent
|
||||
= icon('check', class: 'icon')
|
||||
.description
|
||||
%strong Comment
|
||||
%p
|
||||
|
@ -19,8 +19,8 @@
|
|||
%li.divider.droplab-item-ignore
|
||||
|
||||
%li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => 'Start discussion', 'close-text' => "Start discussion & close #{noteable_name}", 'reopen-text' => "Start discussion & reopen #{noteable_name}" } }
|
||||
%a{ href: '#' }
|
||||
= icon('check')
|
||||
%button.btn.btn-transparent
|
||||
= icon('check', class: 'icon')
|
||||
.description
|
||||
%strong Start discussion
|
||||
%p
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: "#20628 Enable implicit grant in GitLab as OAuth Provider"
|
||||
merge_request: 12384
|
||||
author: Mateusz Pytel
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Change order of monospace fonts to fix bug on some linux distros
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow to enable the performance bar per user or Feature group
|
||||
merge_request: 12362
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Allow admins to retrieve user agent details for an issue or snippet
|
||||
merge_request: 12655
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Don't mark empty MRs as merged on push to the target branch
|
||||
merge_request:
|
||||
author:
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Fixed GFM references not being included when updating issues inline
|
||||
merge_request:
|
||||
author:
|
|
@ -166,9 +166,10 @@ module Gitlab
|
|||
config.after_initialize do
|
||||
Rails.application.reload_routes!
|
||||
|
||||
named_routes_set = Gitlab::Application.routes.named_routes
|
||||
project_url_helpers = Module.new do
|
||||
named_routes_set.helper_names.each do |name|
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
Gitlab::Application.routes.named_routes.helper_names.each do |name|
|
||||
next unless name.include?('namespace_project')
|
||||
|
||||
define_method(name.sub('namespace_project', 'project')) do |project, *args|
|
||||
|
@ -177,14 +178,7 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
named_routes_set.url_helpers_module.include project_url_helpers
|
||||
named_routes_set.url_helpers_module.extend project_url_helpers
|
||||
|
||||
Gitlab::Routing.url_helpers.include project_url_helpers
|
||||
Gitlab::Routing.url_helpers.extend project_url_helpers
|
||||
|
||||
GitlabRoutingHelper.include project_url_helpers
|
||||
GitlabRoutingHelper.extend project_url_helpers
|
||||
Gitlab::Routing.add_helpers(project_url_helpers)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -657,7 +657,10 @@ test:
|
|||
client_id: 'YOUR_AUTH0_CLIENT_ID',
|
||||
client_secret: 'YOUR_AUTH0_CLIENT_SECRET',
|
||||
namespace: 'YOUR_AUTH0_DOMAIN' } }
|
||||
|
||||
- { name: 'authentiq',
|
||||
app_id: 'YOUR_CLIENT_ID',
|
||||
app_secret: 'YOUR_CLIENT_SECRET',
|
||||
args: { scope: 'aq:name email~rs address aq:push' } }
|
||||
ldap:
|
||||
enabled: false
|
||||
servers:
|
||||
|
|
|
@ -87,9 +87,7 @@ Doorkeeper.configure do
|
|||
# "password" => Resource Owner Password Credentials Grant Flow
|
||||
# "client_credentials" => Client Credentials Grant Flow
|
||||
#
|
||||
# If not specified, Doorkeeper enables all the four grant flows.
|
||||
#
|
||||
grant_flows %w(authorization_code password client_credentials)
|
||||
grant_flows %w(authorization_code implicit password client_credentials)
|
||||
|
||||
# Under some circumstances you might want to have applications auto-approved,
|
||||
# so that the user skips the authorization step.
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
class AddPerformanceBarAllowedGroupIdToApplicationSettings < ActiveRecord::Migration
|
||||
include Gitlab::Database::MigrationHelpers
|
||||
|
||||
DOWNTIME = false
|
||||
|
||||
def change
|
||||
add_column :application_settings, :performance_bar_allowed_group_id, :integer
|
||||
end
|
||||
end
|
|
@ -126,6 +126,7 @@ ActiveRecord::Schema.define(version: 20170724184243) do
|
|||
t.boolean "prometheus_metrics_enabled", default: false, null: false
|
||||
t.boolean "help_page_hide_commercial_content", default: false
|
||||
t.string "help_page_support_url"
|
||||
t.integer "performance_bar_allowed_group_id"
|
||||
end
|
||||
|
||||
create_table "audit_events", force: :cascade do |t|
|
||||
|
|
|
@ -167,6 +167,7 @@ have access to GitLab administration tools and settings.
|
|||
- [Operations](administration/operations.md): Keeping GitLab up and running.
|
||||
- [Polling](administration/polling.md): Configure how often the GitLab UI polls for updates.
|
||||
- [Request Profiling](administration/monitoring/performance/request_profiling.md): Get a detailed profile on slow requests.
|
||||
- [Performance Bar](administration/monitoring/performance/performance_bar.md): Get performance information for the current page.
|
||||
|
||||
### Customization
|
||||
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 182 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 158 KiB |
Binary file not shown.
After Width: | Height: | Size: 161 KiB |
|
@ -0,0 +1,35 @@
|
|||
# Performance Bar
|
||||
|
||||
A Performance Bar can be displayed, to dig into the performance of a page. When
|
||||
activated, it looks as follows:
|
||||
|
||||
![Performance Bar](img/performance_bar.png)
|
||||
|
||||
It allows you to:
|
||||
|
||||
- see the current host serving the page
|
||||
- see the timing of the page (backend, frontend)
|
||||
- the number of DB queries, the time it took, and the detail of these queries
|
||||
![SQL profiling using the Performance Bar](img/performance_bar_sql_queries.png)
|
||||
- the number of calls to Redis, and the time it took
|
||||
- the number of background jobs created by Sidekiq, and the time it took
|
||||
- the number of Ruby GC calls, and the time it took
|
||||
- profile the code used to generate the page, line by line
|
||||
![Line profiling using the Performance Bar](img/performance_bar_line_profiling.png)
|
||||
|
||||
## Enable the Performance Bar via the Admin panel
|
||||
|
||||
GitLab Performance Bar is disabled by default. To enable it for a given group,
|
||||
navigate to the Admin area in **Settings > Profiling - Performance Bar**
|
||||
(`/admin/application_settings`).
|
||||
|
||||
The only required setting you need to set is the full path of the group that
|
||||
will be allowed to display the Performance Bar.
|
||||
Make sure _Enable the Performance Bar_ is checked and hit
|
||||
**Save** to save the changes.
|
||||
|
||||
---
|
||||
|
||||
![GitLab Performance Bar Admin Settings](img/performance_bar_configuration_settings.png)
|
||||
|
||||
---
|
|
@ -17,6 +17,7 @@ following locations:
|
|||
- [Deploy Keys](deploy_keys.md)
|
||||
- [Environments](environments.md)
|
||||
- [Events](events.md)
|
||||
- [Feature flags](features.md)
|
||||
- [Gitignores templates](templates/gitignores.md)
|
||||
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
|
||||
- [Groups](groups.md)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Features API
|
||||
# Features flags API
|
||||
|
||||
All methods require administrator authorization.
|
||||
|
||||
|
@ -61,7 +61,8 @@ POST /features/:name
|
|||
| `feature_group` | string | no | A Feature group name |
|
||||
| `user` | string | no | A GitLab username |
|
||||
|
||||
Note that `feature_group` and `user` are mutually exclusive.
|
||||
Note that you can enable or disable a feature for both a `feature_group` and a
|
||||
`user` with a single API call.
|
||||
|
||||
```bash
|
||||
curl --data "value=30" --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/features/new_library
|
||||
|
|
|
@ -964,3 +964,30 @@ Example response:
|
|||
## Comments on issues
|
||||
|
||||
Comments are done via the [notes](notes.md) resource.
|
||||
|
||||
## Get user agent details
|
||||
|
||||
Available only for admins.
|
||||
|
||||
```
|
||||
GET /projects/:id/issues/:issue_iid/user_agent_detail
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|--------------------------------------|
|
||||
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
|
||||
| `issue_iid` | integer | yes | The internal ID of a project's issue |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/issues/93/user_agent_detail
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_agent": "AppleWebKit/537.36",
|
||||
"ip_address": "127.0.0.1",
|
||||
"akismet_submitted": false
|
||||
}
|
||||
```
|
||||
|
|
|
@ -1,59 +1,55 @@
|
|||
# GitLab as an OAuth2 provider
|
||||
|
||||
This document covers using the OAuth2 protocol to access GitLab.
|
||||
This document covers using the [OAuth2](https://oauth.net/2/) protocol to allow other services access Gitlab resources on user's behalf.
|
||||
|
||||
If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
|
||||
If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [OAuth2 provider](../integration/oauth_provider.md)
|
||||
documentation.
|
||||
|
||||
OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
|
||||
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper).
|
||||
|
||||
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
|
||||
## Supported OAuth2 Flows
|
||||
|
||||
## Web Application Flow
|
||||
Gitlab currently supports following authorization flows:
|
||||
|
||||
This is the most common type of flow and is used by server-side clients that wish to access GitLab on a user's behalf.
|
||||
* *Web Application Flow* - Most secure and common type of flow, designed for the applications with secure server-side.
|
||||
* *Implicit Flow* - This flow is designed for user-agent only apps (e.g. single page web application running on GitLab Pages).
|
||||
* *Resource Owner Password Credentials Flow* - To be used **only** for securely hosted, first-party services.
|
||||
|
||||
>**Note:**
|
||||
This flow **should not** be used for client-side clients as you would leak your `client_secret`. Client-side clients should use the Implicit Grant (which is currently unsupported).
|
||||
Please refer to [OAuth RFC](https://tools.ietf.org/html/rfc6749) to find out in details how all those flows work and pick the right one for your use case.
|
||||
|
||||
For more detailed information, check out the [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1)
|
||||
Both *web application* and *implicit* flows require `application` to be registered first via `/profile/applications` page
|
||||
in your user's account. During registration, by enabling proper scopes you can limit the range of resources which the `application` can access. Upon creation
|
||||
you'll obtain `application` credentials: _Application ID_ and _Client Secret_ - **keep them secure**.
|
||||
|
||||
In the following sections you will be introduced to the three steps needed for this flow.
|
||||
>**Important:** OAuth specification advises sending `state` parameter with each request to `/oauth/authorize`. We highly recommended to send a unique
|
||||
value with each request and validate it against the one in redirect request. This is important to prevent [CSRF attacks]. The `state` param really should
|
||||
have been a requirement in the standard!
|
||||
|
||||
### 1. Registering the client
|
||||
In the following sections you will find detailed instructions on how to obtain authorization with each flow.
|
||||
|
||||
First, you should create an application (`/profile/applications`) in your user's account.
|
||||
Each application gets a unique App ID and App Secret parameters.
|
||||
### Web Application Flow
|
||||
|
||||
>**Note:**
|
||||
**You should not share/leak your App ID or App Secret.**
|
||||
Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.1) for a detailed flow description
|
||||
|
||||
### 2. Requesting authorization
|
||||
#### 1. Requesting authorization code
|
||||
|
||||
To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint:
|
||||
To request the authorization code, you should redirect the user to the `/oauth/authorize` endpoint with following GET parameters:
|
||||
|
||||
```
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=your_unique_state_hash
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=code&state=YOUR_UNIQUE_STATE_HASH
|
||||
```
|
||||
|
||||
This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided.
|
||||
This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect will
|
||||
include the GET `code` parameter, for example:
|
||||
|
||||
The redirect will include the GET `code` parameter, for example:
|
||||
|
||||
```
|
||||
http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
|
||||
```
|
||||
`http://myapp.com/oauth/redirect?code=1234567890&state=YOUR_UNIQUE_STATE_HASH`
|
||||
|
||||
You should then use the `code` to request an access token.
|
||||
|
||||
>**Important:**
|
||||
It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
|
||||
validate that value is returned and matches in the redirect request.
|
||||
This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
|
||||
`state` really should have been a requirement in the standard!
|
||||
#### 2. Requesting access token
|
||||
|
||||
### 3. Requesting the access token
|
||||
|
||||
Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example, we are using Ruby's `rest-client`:
|
||||
Once you have the authorization code you can request an `access_token` using the code, to do that you can use any HTTP client. In the following example,
|
||||
we are using Ruby's `rest-client`:
|
||||
|
||||
```
|
||||
parameters = 'client_id=APP_ID&client_secret=APP_SECRET&code=RETURNED_CODE&grant_type=authorization_code&redirect_uri=REDIRECT_URI'
|
||||
|
@ -72,28 +68,40 @@ The `redirect_uri` must match the `redirect_uri` used in the original authorizat
|
|||
|
||||
You can now make requests to the API with the access token returned.
|
||||
|
||||
### Use the access token to access the API
|
||||
|
||||
The access token allows you to make requests to the API on a behalf of a user.
|
||||
### Implicit Grant
|
||||
|
||||
Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.2) for a detailed flow description.
|
||||
|
||||
Unlike the web flow, the client receives an `access token` immediately as a result of the authorization request. The flow does not use client secret
|
||||
or authorization code because all of the application code and storage is easily accessible, therefore __secrets__ can leak easily.
|
||||
|
||||
>**Important:** Avoid using this flow for applications that store data outside of the Gitlab instance. If you do, make sure to verify `application id`
|
||||
associated with access token before granting access to the data
|
||||
(see [/oauth/token/info](https://github.com/doorkeeper-gem/doorkeeper/wiki/API-endpoint-descriptions-and-examples#get----oauthtokeninfo)).
|
||||
|
||||
|
||||
#### 1. Requesting access token
|
||||
|
||||
To request the access token, you should redirect the user to the `/oauth/authorize` endpoint using `token` response type:
|
||||
|
||||
```
|
||||
GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
|
||||
https://gitlab.example.com/oauth/authorize?client_id=APP_ID&redirect_uri=REDIRECT_URI&response_type=token&state=YOUR_UNIQUE_STATE_HASH
|
||||
```
|
||||
|
||||
Or you can put the token to the Authorization header:
|
||||
This will ask the user to approve the applications access to their account and then redirect back to the `REDIRECT_URI` you provided. The redirect
|
||||
will include a fragment with `access_token` as well as token details in GET parameters, for example:
|
||||
|
||||
```
|
||||
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
|
||||
http://myapp.com/oauth/redirect#access_token=ABCDExyz123&state=YOUR_UNIQUE_STATE_HASH&token_type=bearer&expires_in=3600
|
||||
```
|
||||
|
||||
## Resource Owner Password Credentials
|
||||
### Resource Owner Password Credentials
|
||||
|
||||
## Deprecation Notice
|
||||
Check [RFC spec](http://tools.ietf.org/html/rfc6749#section-4.3) for a detailed flow description.
|
||||
|
||||
1. Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication turned on.
|
||||
2. These users can access the API using [personal access tokens] instead.
|
||||
|
||||
---
|
||||
> **Deprecation notice:** Starting in GitLab 8.11, the Resource Owner Password Credentials has been *disabled* for users with two-factor authentication
|
||||
turned on. These users can access the API using [personal access tokens] instead.
|
||||
|
||||
In this flow, a token is requested in exchange for the resource owner credentials (username and password).
|
||||
The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
|
||||
|
@ -101,12 +109,16 @@ client is part of the device operating system or a highly privileged application
|
|||
available (such as an authorization code).
|
||||
|
||||
>**Important:**
|
||||
Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens] are a better choice.
|
||||
Never store the users credentials and only use this grant type when your client is deployed to a trusted environment, in 99% of cases [personal access tokens]
|
||||
are a better choice.
|
||||
|
||||
Even though this grant type requires direct client access to the resource owner credentials, the resource owner credentials are used
|
||||
for a single request and are exchanged for an access token. This grant type can eliminate the need for the client to store the
|
||||
resource owner credentials for future use, by exchanging the credentials with a long-lived access token or refresh token.
|
||||
You can do POST request to `/oauth/token` with parameters:
|
||||
|
||||
#### 1. Requesting access token
|
||||
|
||||
POST request to `/oauth/token` with parameters:
|
||||
|
||||
```
|
||||
{
|
||||
|
@ -134,4 +146,18 @@ access_token = client.password.get_token('user@example.com', 'secret')
|
|||
puts access_token.token
|
||||
```
|
||||
|
||||
## Access Gitlab API with `access token`
|
||||
|
||||
The `access token` allows you to make requests to the API on a behalf of a user. You can pass the token either as GET parameter
|
||||
```
|
||||
GET https://gitlab.example.com/api/v4/user?access_token=OAUTH-TOKEN
|
||||
```
|
||||
|
||||
or you can put the token to the Authorization header:
|
||||
|
||||
```
|
||||
curl --header "Authorization: Bearer OAUTH-TOKEN" https://gitlab.example.com/api/v4/user
|
||||
```
|
||||
|
||||
[personal access tokens]: ../user/profile/personal_access_tokens.md
|
||||
[CSRF attacks]: http://www.oauthsecurity.com/#user-content-authorization-code-flow
|
|
@ -119,3 +119,35 @@ Parameters:
|
|||
|
||||
- `id` (required) - The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user
|
||||
- `snippet_id` (required) - The ID of a project's snippet
|
||||
|
||||
## Get user agent details
|
||||
|
||||
> **Notes:**
|
||||
> [Introduced][ce-29508] in GitLab 9.4.
|
||||
|
||||
|
||||
Available only for admins.
|
||||
|
||||
```
|
||||
GET /projects/:id/snippets/:snippet_id/user_agent_detail
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|--------------------------------------|
|
||||
| `id` | Integer | yes | The ID of a snippet |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/1/snippets/1/user_agent_detail
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_agent": "AppleWebKit/537.36",
|
||||
"ip_address": "127.0.0.1",
|
||||
"akismet_submitted": false
|
||||
}
|
||||
```
|
||||
|
||||
[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
|
||||
|
|
|
@ -234,3 +234,35 @@ Example response:
|
|||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Get user agent details
|
||||
|
||||
> **Notes:**
|
||||
> [Introduced][ce-29508] in GitLab 9.4.
|
||||
|
||||
|
||||
Available only for admins.
|
||||
|
||||
```
|
||||
GET /snippets/:id/user_agent_detail
|
||||
```
|
||||
|
||||
| Attribute | Type | Required | Description |
|
||||
|-------------|---------|----------|--------------------------------------|
|
||||
| `id` | Integer | yes | The ID of a snippet |
|
||||
|
||||
```bash
|
||||
curl --request GET --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/snippets/1/user_agent_detail
|
||||
```
|
||||
|
||||
Example response:
|
||||
|
||||
```json
|
||||
{
|
||||
"user_agent": "AppleWebKit/537.36",
|
||||
"ip_address": "127.0.0.1",
|
||||
"akismet_submitted": false
|
||||
}
|
||||
```
|
||||
|
||||
[ce-[ce-29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508]: https://gitlab.com/gitlab-org/gitlab-ce/issues/29508
|
||||
|
|
|
@ -55,6 +55,7 @@
|
|||
- [Single Table Inheritance](single_table_inheritance.md)
|
||||
- [Background Migrations](background_migrations.md)
|
||||
- [Storing SHA1 Hashes As Binary](sha1_as_binary.md)
|
||||
- [Iterating Tables In Batches](iterating_tables_in_batches.md)
|
||||
|
||||
## i18n
|
||||
|
||||
|
|
|
@ -3,5 +3,19 @@
|
|||
Starting from GitLab 9.3 we support feature flags via
|
||||
[Flipper](https://github.com/jnunemaker/flipper/). You should use the `Feature`
|
||||
class (defined in `lib/feature.rb`) in your code to get, set and list feature
|
||||
flags. During runtime you can set the values for the gates via the
|
||||
[admin API](../api/features.md).
|
||||
flags.
|
||||
|
||||
During runtime you can set the values for the gates via the
|
||||
[features API](../api/features.md) (accessible to admins only).
|
||||
|
||||
## Feature groups
|
||||
|
||||
Starting from GitLab 9.4 we support feature groups via
|
||||
[Flipper groups](https://github.com/jnunemaker/flipper/blob/v0.10.2/docs/Gates.md#2-group).
|
||||
|
||||
Feature groups must be defined statically in `lib/feature.rb` (in the
|
||||
`.register_feature_groups` method), but their implementation can obviously be
|
||||
dynamic (querying the DB etc.).
|
||||
|
||||
Once defined in `lib/feature.rb`, you will be able to activate a
|
||||
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
# Iterating Tables In Batches
|
||||
|
||||
Rails provides a method called `in_batches` that can be used to iterate over
|
||||
rows in batches. For example:
|
||||
|
||||
```ruby
|
||||
User.in_batches(of: 10) do |relation|
|
||||
relation.update_all(updated_at: Time.now)
|
||||
end
|
||||
```
|
||||
|
||||
Unfortunately this method is implemented in a way that is not very efficient,
|
||||
both query and memory usage wise.
|
||||
|
||||
To work around this you can include the `EachBatch` module into your models,
|
||||
then use the `each_batch` class method. For example:
|
||||
|
||||
```ruby
|
||||
class User < ActiveRecord::Base
|
||||
include EachBatch
|
||||
end
|
||||
|
||||
User.each_batch(of: 10) do |relation|
|
||||
relation.update_all(updated_at: Time.now)
|
||||
end
|
||||
```
|
||||
|
||||
This will end up producing queries such as:
|
||||
|
||||
```
|
||||
User Load (0.7ms) SELECT "users"."id" FROM "users" WHERE ("users"."id" >= 41654) ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1000
|
||||
(0.7ms) SELECT COUNT(*) FROM "users" WHERE ("users"."id" >= 41654) AND ("users"."id" < 42687)
|
||||
```
|
||||
|
||||
The API of this method is similar to `in_batches`, though it doesn't support
|
||||
all of the arguments that `in_batches` supports. You should always use
|
||||
`each_batch` _unless_ you have a specific need for `in_batches`.
|
|
@ -39,6 +39,9 @@ mysql> SET storage_engine=INNODB;
|
|||
# If you have MySQL < 5.7.7 and want to enable utf8mb4 character set support with your GitLab install, you must set the following NOW:
|
||||
mysql> SET GLOBAL innodb_file_per_table=1, innodb_file_format=Barracuda, innodb_large_prefix=1;
|
||||
|
||||
# If you use MySQL with replication, or just have MySQL configured with binary logging, you need to run the following to allow the use of `TRIGGER`:
|
||||
mysql> SET GLOBAL log_bin_trust_function_creators = 1;
|
||||
|
||||
# Create the GitLab production database
|
||||
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_general_ci`;
|
||||
|
||||
|
@ -60,7 +63,15 @@ mysql> \q
|
|||
```
|
||||
|
||||
You are done installing the database for now and can go back to the rest of the installation.
|
||||
Please proceed to the rest of the installation before running through the utf8mb4 support section.
|
||||
Please proceed to the rest of the installation **before** running through the steps below.
|
||||
|
||||
### `log_bin_trust_function_creators`
|
||||
|
||||
If you use MySQL with replication, or just have MySQL configured with binary logging, all of your MySQL servers will need to have `log_bin_trust_function_creators` enabled to allow the use of `TRIGGER` in migrations. You have already set this global variable in the steps above, but to make it persistent, add the following to your `my.cnf` file:
|
||||
|
||||
```
|
||||
log_bin_trust_function_creators=1
|
||||
```
|
||||
|
||||
### MySQL utf8mb4 support
|
||||
|
||||
|
|
|
@ -164,6 +164,19 @@ permissions on the database:
|
|||
```bash
|
||||
mysql -u root -p -e "GRANT TRIGGER ON \`gitlabhq_production\`.* TO 'git'@'localhost';"
|
||||
```
|
||||
|
||||
If you use MySQL with replication, or just have MySQL configured with binary logging,
|
||||
you will need to also run the following on all of your MySQL servers:
|
||||
|
||||
```bash
|
||||
mysql -u root -p -e "SET GLOBAL log_bin_trust_function_creators = 1;"
|
||||
```
|
||||
|
||||
You can make this setting permanent by adding it to your `my.cnf`:
|
||||
|
||||
```
|
||||
log_bin_trust_function_creators=1
|
||||
```
|
||||
|
||||
### 11. Update configuration files
|
||||
|
||||
|
|
|
@ -888,5 +888,11 @@ module API
|
|||
expose :dependencies, using: Dependency
|
||||
end
|
||||
end
|
||||
|
||||
class UserAgentDetail < Grape::Entity
|
||||
expose :user_agent
|
||||
expose :ip_address
|
||||
expose :submitted, as: :akismet_submitted
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -14,14 +14,12 @@ module API
|
|||
end
|
||||
end
|
||||
|
||||
def gate_target(params)
|
||||
if params[:feature_group]
|
||||
Feature.group(params[:feature_group])
|
||||
elsif params[:user]
|
||||
User.find_by_username(params[:user])
|
||||
else
|
||||
gate_value(params)
|
||||
end
|
||||
def gate_targets(params)
|
||||
targets = []
|
||||
targets << Feature.group(params[:feature_group]) if params[:feature_group]
|
||||
targets << User.find_by_username(params[:user]) if params[:user]
|
||||
|
||||
targets
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -42,18 +40,25 @@ module API
|
|||
requires :value, type: String, desc: '`true` or `false` to enable/disable, an integer for percentage of time'
|
||||
optional :feature_group, type: String, desc: 'A Feature group name'
|
||||
optional :user, type: String, desc: 'A GitLab username'
|
||||
mutually_exclusive :feature_group, :user
|
||||
end
|
||||
post ':name' do
|
||||
feature = Feature.get(params[:name])
|
||||
target = gate_target(params)
|
||||
targets = gate_targets(params)
|
||||
value = gate_value(params)
|
||||
|
||||
case value
|
||||
when true
|
||||
feature.enable(target)
|
||||
if targets.present?
|
||||
targets.each { |target| feature.enable(target) }
|
||||
else
|
||||
feature.enable
|
||||
end
|
||||
when false
|
||||
feature.disable(target)
|
||||
if targets.present?
|
||||
targets.each { |target| feature.disable(target) }
|
||||
else
|
||||
feature.disable
|
||||
end
|
||||
else
|
||||
feature.enable_percentage_of_time(value)
|
||||
end
|
||||
|
|
|
@ -241,6 +241,22 @@ module API
|
|||
|
||||
present paginate(merge_requests), with: Entities::MergeRequestBasic, current_user: current_user, project: user_project
|
||||
end
|
||||
|
||||
desc 'Get the user agent details for an issue' do
|
||||
success Entities::UserAgentDetail
|
||||
end
|
||||
params do
|
||||
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
|
||||
end
|
||||
get ":id/issues/:issue_iid/user_agent_detail" do
|
||||
authenticated_as_admin!
|
||||
|
||||
issue = find_project_issue(params[:issue_iid])
|
||||
|
||||
return not_found!('UserAgentDetail') unless issue.user_agent_detail
|
||||
|
||||
present issue.user_agent_detail, with: Entities::UserAgentDetail
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -131,6 +131,22 @@ module API
|
|||
content_type 'text/plain'
|
||||
present snippet.content
|
||||
end
|
||||
|
||||
desc 'Get the user agent details for a project snippet' do
|
||||
success Entities::UserAgentDetail
|
||||
end
|
||||
params do
|
||||
requires :snippet_id, type: Integer, desc: 'The ID of a project snippet'
|
||||
end
|
||||
get ":id/snippets/:snippet_id/user_agent_detail" do
|
||||
authenticated_as_admin!
|
||||
|
||||
snippet = Snippet.find_by!(id: params[:id])
|
||||
|
||||
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
|
||||
|
||||
present snippet.user_agent_detail, with: Entities::UserAgentDetail
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -140,6 +140,22 @@ module API
|
|||
content_type 'text/plain'
|
||||
present snippet.content
|
||||
end
|
||||
|
||||
desc 'Get the user agent details for a snippet' do
|
||||
success Entities::UserAgentDetail
|
||||
end
|
||||
params do
|
||||
requires :id, type: Integer, desc: 'The ID of a snippet'
|
||||
end
|
||||
get ":id/user_agent_detail" do
|
||||
authenticated_as_admin!
|
||||
|
||||
snippet = Snippet.find_by!(id: params[:id])
|
||||
|
||||
return not_found!('UserAgentDetail') unless snippet.user_agent_detail
|
||||
|
||||
present snippet.user_agent_detail, with: Entities::UserAgentDetail
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,7 +4,7 @@ module Gitlab
|
|||
# Abstract class for badge metadata
|
||||
#
|
||||
class Metadata
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
include ActionView::Helpers::AssetTagHelper
|
||||
include ActionView::Helpers::UrlHelper
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
module Gitlab
|
||||
module Conflict
|
||||
class File
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
include IconsHelper
|
||||
|
||||
MissingResolution = Class.new(ResolutionError)
|
||||
|
|
|
@ -4,7 +4,7 @@ module Gitlab
|
|||
class RepositoryPush
|
||||
attr_reader :author_id, :ref, :action
|
||||
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
include DiffHelper
|
||||
|
||||
delegate :namespace, :name_with_namespace, to: :project, prefix: :project
|
||||
|
|
|
@ -41,10 +41,6 @@ module Gitlab
|
|||
commit_id: sha
|
||||
)
|
||||
when :BLOB
|
||||
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
|
||||
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
|
||||
# which is what we use below to keep a consistent behavior.
|
||||
detect = CharlockHolmes::EncodingDetector.new(8000).detect(entry.data)
|
||||
new(
|
||||
id: entry.oid,
|
||||
name: name,
|
||||
|
@ -53,7 +49,7 @@ module Gitlab
|
|||
mode: entry.mode.to_s(8),
|
||||
path: path,
|
||||
commit_id: sha,
|
||||
binary: detect && detect[:type] == :binary
|
||||
binary: binary?(entry.data)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
@ -87,14 +83,28 @@ module Gitlab
|
|||
end
|
||||
|
||||
def raw(repository, sha)
|
||||
blob = repository.lookup(sha)
|
||||
Gitlab::GitalyClient.migrate(:git_blob_raw) do |is_enabled|
|
||||
if is_enabled
|
||||
Gitlab::GitalyClient::Blob.new(repository).get_blob(oid: sha, limit: MAX_DATA_DISPLAY_SIZE)
|
||||
else
|
||||
blob = repository.lookup(sha)
|
||||
|
||||
new(
|
||||
id: blob.oid,
|
||||
size: blob.size,
|
||||
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
||||
binary: blob.binary?
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
new(
|
||||
id: blob.oid,
|
||||
size: blob.size,
|
||||
data: blob.content(MAX_DATA_DISPLAY_SIZE),
|
||||
binary: blob.binary?
|
||||
)
|
||||
def binary?(data)
|
||||
# EncodingDetector checks the first 1024 * 1024 bytes for NUL byte, libgit2 checks
|
||||
# only the first 8000 (https://github.com/libgit2/libgit2/blob/2ed855a9e8f9af211e7274021c2264e600c0f86b/src/filter.h#L15),
|
||||
# which is what we use below to keep a consistent behavior.
|
||||
detect = CharlockHolmes::EncodingDetector.new(8000).detect(data)
|
||||
detect && detect[:type] == :binary
|
||||
end
|
||||
|
||||
# Recursive search of blob id by path
|
||||
|
@ -165,8 +175,17 @@ module Gitlab
|
|||
return if @data == '' # don't mess with submodule blobs
|
||||
return @data if @loaded_all_data
|
||||
|
||||
Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
|
||||
@data = begin
|
||||
if is_enabled
|
||||
Gitlab::GitalyClient::Blob.new(repository).get_blob(oid: id, limit: -1).data
|
||||
else
|
||||
repository.lookup(id).content
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@loaded_all_data = true
|
||||
@data = repository.lookup(id).content
|
||||
@loaded_size = @data.bytesize
|
||||
@binary = nil
|
||||
end
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
module Gitlab
|
||||
module GitalyClient
|
||||
class Blob
|
||||
def initialize(repository)
|
||||
@gitaly_repo = repository.gitaly_repository
|
||||
end
|
||||
|
||||
def get_blob(oid:, limit:)
|
||||
request = Gitaly::GetBlobRequest.new(
|
||||
repository: @gitaly_repo,
|
||||
oid: oid,
|
||||
limit: limit
|
||||
)
|
||||
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_blob, request)
|
||||
|
||||
blob = response.first
|
||||
return unless blob.oid.present?
|
||||
|
||||
data = response.reduce(blob.data.dup) { |memo, msg| memo << msg.data.dup }
|
||||
|
||||
Gitlab::Git::Blob.new(
|
||||
id: blob.oid,
|
||||
size: blob.size,
|
||||
data: data,
|
||||
binary: Gitlab::Git::Blob.binary?(data)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -34,7 +34,7 @@ module Gitlab
|
|||
|
||||
write_csv do |csv|
|
||||
ActiveRecord::Base.transaction do
|
||||
User.with_two_factor.in_batches do |relation|
|
||||
User.with_two_factor.in_batches do |relation| # rubocop: disable Cop/InBatches
|
||||
rows = relation.pluck(:id, :encrypted_otp_secret, :encrypted_otp_secret_iv, :encrypted_otp_secret_salt)
|
||||
rows.each do |row|
|
||||
user = %i[id ciphertext iv salt].zip(row).to_h
|
||||
|
|
|
@ -1,7 +1,33 @@
|
|||
module Gitlab
|
||||
module PerformanceBar
|
||||
def self.enabled?
|
||||
Rails.env.development? || Feature.enabled?('gitlab_performance_bar')
|
||||
include Gitlab::CurrentSettings
|
||||
|
||||
ALLOWED_USER_IDS_KEY = 'performance_bar_allowed_user_ids'.freeze
|
||||
|
||||
def self.enabled?(user = nil)
|
||||
return false unless user && allowed_group_id
|
||||
|
||||
allowed_user_ids.include?(user.id)
|
||||
end
|
||||
|
||||
def self.allowed_group_id
|
||||
current_application_settings.performance_bar_allowed_group_id
|
||||
end
|
||||
|
||||
def self.allowed_user_ids
|
||||
Rails.cache.fetch(ALLOWED_USER_IDS_KEY) do
|
||||
group = Group.find_by_id(allowed_group_id)
|
||||
|
||||
if group
|
||||
GroupMembersFinder.new(group).execute.pluck(:user_id)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def self.expire_allowed_user_ids_cache
|
||||
Rails.cache.delete(ALLOWED_USER_IDS_KEY)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,10 +2,30 @@ module Gitlab
|
|||
module Routing
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
mattr_accessor :_includers
|
||||
self._includers = []
|
||||
|
||||
included do
|
||||
Gitlab::Routing._includers << self
|
||||
include Gitlab::Routing.url_helpers
|
||||
end
|
||||
|
||||
def self.add_helpers(mod)
|
||||
url_helpers.include mod
|
||||
url_helpers.extend mod
|
||||
|
||||
app_url_helpers = Gitlab::Application.routes.named_routes.url_helpers_module
|
||||
app_url_helpers.include mod
|
||||
app_url_helpers.extend mod
|
||||
|
||||
GitlabRoutingHelper.include mod
|
||||
GitlabRoutingHelper.extend mod
|
||||
|
||||
_includers.each do |klass|
|
||||
klass.include mod
|
||||
end
|
||||
end
|
||||
|
||||
# Returns the URL helpers Module.
|
||||
#
|
||||
# This method caches the output as Rails' "url_helpers" method creates an
|
||||
|
|
|
@ -2,7 +2,7 @@ module Gitlab
|
|||
module SlashCommands
|
||||
module Presenters
|
||||
class Base
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
|
||||
def initialize(resource = nil)
|
||||
@resource = resource
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
module Gitlab
|
||||
class UrlBuilder
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
include GitlabRoutingHelper
|
||||
include ActionView::RecordIdentifier
|
||||
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
require_relative '../model_helpers'
|
||||
|
||||
module RuboCop
|
||||
module Cop
|
||||
# Cop that prevents the use of `in_batches`
|
||||
class InBatches < RuboCop::Cop::Cop
|
||||
MSG = 'Do not use `in_batches`, use `each_batch` from the EachBatch module instead'.freeze
|
||||
|
||||
def on_send(node)
|
||||
return unless node.children[1] == :in_batches
|
||||
|
||||
add_offense(node, :selector)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,7 @@ require_relative 'cop/redirect_with_status'
|
|||
require_relative 'cop/polymorphic_associations'
|
||||
require_relative 'cop/project_path_helper'
|
||||
require_relative 'cop/active_record_dependent'
|
||||
require_relative 'cop/in_batches'
|
||||
require_relative 'cop/migration/add_column'
|
||||
require_relative 'cop/migration/add_column_with_default_to_large_table'
|
||||
require_relative 'cop/migration/add_concurrent_foreign_key'
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe 'Issuables Close/Reopen/Report toggle', :feature do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
shared_examples 'an issuable close/reopen/report toggle' do
|
||||
let(:container) { find('.issuable-close-dropdown') }
|
||||
let(:human_model_name) { issuable.model_name.human.downcase }
|
||||
|
||||
it 'shows toggle' do
|
||||
expect(page).to have_link("Close #{human_model_name}")
|
||||
expect(page).to have_selector('.issuable-close-dropdown')
|
||||
end
|
||||
|
||||
it 'opens a dropdown when toggle is clicked' do
|
||||
container.find('.dropdown-toggle').click
|
||||
|
||||
expect(container).to have_selector('.dropdown-menu')
|
||||
expect(container).to have_content("Close #{human_model_name}")
|
||||
expect(container).to have_content('Report abuse')
|
||||
expect(container).to have_content("Report #{human_model_name.pluralize} that are abusive, inappropriate or spam.")
|
||||
expect(container).to have_selector('.close-item.droplab-item-selected')
|
||||
expect(container).to have_selector('.report-item')
|
||||
expect(container).not_to have_selector('.report-item.droplab-item-selected')
|
||||
expect(container).not_to have_selector('.reopen-item')
|
||||
end
|
||||
|
||||
it 'changes the button when an item is selected' do
|
||||
button = container.find('.issuable-close-button')
|
||||
|
||||
container.find('.dropdown-toggle').click
|
||||
container.find('.report-item').click
|
||||
|
||||
expect(container).not_to have_selector('.dropdown-menu')
|
||||
expect(button).to have_content('Report abuse')
|
||||
|
||||
container.find('.dropdown-toggle').click
|
||||
container.find('.close-item').click
|
||||
|
||||
expect(button).to have_content("Close #{human_model_name}")
|
||||
end
|
||||
end
|
||||
|
||||
context 'on an issue' do
|
||||
let(:project) { create(:empty_project) }
|
||||
let(:issuable) { create(:issue, project: project) }
|
||||
|
||||
before do
|
||||
project.add_master(user)
|
||||
login_as user
|
||||
end
|
||||
|
||||
context 'when user has permission to update', :js do
|
||||
before do
|
||||
visit project_issue_path(project, issuable)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issuable close/reopen/report toggle'
|
||||
end
|
||||
|
||||
context 'when user doesnt have permission to update' do
|
||||
let(:cant_project) { create(:empty_project) }
|
||||
let(:cant_issuable) { create(:issue, project: cant_project) }
|
||||
|
||||
before do
|
||||
cant_project.add_guest(user)
|
||||
|
||||
visit project_issue_path(cant_project, cant_issuable)
|
||||
end
|
||||
|
||||
it 'only shows the `Report abuse` and `New issue` buttons' do
|
||||
expect(page).to have_link('Report abuse')
|
||||
expect(page).to have_link('New issue')
|
||||
expect(page).not_to have_link('Close issue')
|
||||
expect(page).not_to have_link('Reopen issue')
|
||||
expect(page).not_to have_link('Edit')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'on a merge request' do
|
||||
let(:project) { create(:project) }
|
||||
let(:issuable) { create(:merge_request, source_project: project) }
|
||||
|
||||
before do
|
||||
project.add_master(user)
|
||||
login_as user
|
||||
end
|
||||
|
||||
context 'when user has permission to update', :js do
|
||||
before do
|
||||
visit project_merge_request_path(project, issuable)
|
||||
end
|
||||
|
||||
it_behaves_like 'an issuable close/reopen/report toggle'
|
||||
end
|
||||
|
||||
context 'when user doesnt have permission to update' do
|
||||
let(:cant_project) { create(:project) }
|
||||
let(:cant_issuable) { create(:merge_request, source_project: cant_project) }
|
||||
|
||||
before do
|
||||
cant_project.add_reporter(user)
|
||||
|
||||
visit project_merge_request_path(cant_project, cant_issuable)
|
||||
end
|
||||
|
||||
it 'only shows a `Report abuse` button' do
|
||||
expect(page).to have_link('Report abuse')
|
||||
expect(page).not_to have_link('Close merge request')
|
||||
expect(page).not_to have_link('Reopen merge request')
|
||||
expect(page).not_to have_link('Edit')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -133,7 +133,7 @@ describe 'Visual tokens', js: true, feature: true do
|
|||
describe 'editing milestone token' do
|
||||
before do
|
||||
input_filtered_search('milestone:%10.0 author:none', submit: false)
|
||||
first('.tokens-container .filtered-search-token').double_click
|
||||
first('.tokens-container .filtered-search-token').click
|
||||
first('#js-dropdown-milestone .filter-dropdown .filter-dropdown-item')
|
||||
end
|
||||
|
||||
|
|
|
@ -14,6 +14,18 @@ feature 'GFM autocomplete', feature: true, js: true do
|
|||
wait_for_requests
|
||||
end
|
||||
|
||||
it 'updates issue descripton with GFM reference' do
|
||||
find('.issuable-edit').click
|
||||
|
||||
find('#issue-description').native.send_keys("@#{user.name[0...3]}")
|
||||
|
||||
find('.atwho-view .cur').trigger('click')
|
||||
|
||||
click_button 'Save changes'
|
||||
|
||||
expect(find('.description')).to have_content(user.to_reference)
|
||||
end
|
||||
|
||||
it 'opens autocomplete menu when field starts with text' do
|
||||
page.within '.timeline-content-form' do
|
||||
find('#note_note').native.send_keys('')
|
||||
|
|
|
@ -13,7 +13,7 @@ feature 'OAuth Login', js: true do
|
|||
end
|
||||
|
||||
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
|
||||
:facebook, :cas3, :auth0]
|
||||
:facebook, :cas3, :auth0, :authentiq]
|
||||
|
||||
before(:all) do
|
||||
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
|
||||
|
|
|
@ -33,22 +33,24 @@ describe 'User can display performance bar', :js do
|
|||
end
|
||||
end
|
||||
|
||||
let(:group) { create(:group) }
|
||||
|
||||
context 'when user is logged-out' do
|
||||
before do
|
||||
visit root_path
|
||||
end
|
||||
|
||||
context 'when the gitlab_performance_bar feature is disabled' do
|
||||
context 'when the performance_bar feature is disabled' do
|
||||
before do
|
||||
Feature.disable('gitlab_performance_bar')
|
||||
stub_application_setting(performance_bar_allowed_group_id: nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'performance bar is disabled'
|
||||
end
|
||||
|
||||
context 'when the gitlab_performance_bar feature is enabled' do
|
||||
context 'when the performance_bar feature is enabled' do
|
||||
before do
|
||||
Feature.enable('gitlab_performance_bar')
|
||||
stub_application_setting(performance_bar_allowed_group_id: group.id)
|
||||
end
|
||||
|
||||
it_behaves_like 'performance bar is disabled'
|
||||
|
@ -57,22 +59,25 @@ describe 'User can display performance bar', :js do
|
|||
|
||||
context 'when user is logged-in' do
|
||||
before do
|
||||
sign_in(create(:user))
|
||||
user = create(:user)
|
||||
|
||||
sign_in(user)
|
||||
group.add_guest(user)
|
||||
|
||||
visit root_path
|
||||
end
|
||||
|
||||
context 'when the gitlab_performance_bar feature is disabled' do
|
||||
context 'when the performance_bar feature is disabled' do
|
||||
before do
|
||||
Feature.disable('gitlab_performance_bar')
|
||||
stub_application_setting(performance_bar_allowed_group_id: nil)
|
||||
end
|
||||
|
||||
it_behaves_like 'performance bar is disabled'
|
||||
end
|
||||
|
||||
context 'when the gitlab_performance_bar feature is enabled' do
|
||||
context 'when the performance_bar feature is enabled' do
|
||||
before do
|
||||
Feature.enable('gitlab_performance_bar')
|
||||
stub_application_setting(performance_bar_allowed_group_id: group.id)
|
||||
end
|
||||
|
||||
it_behaves_like 'performance bar is enabled'
|
||||
|
|
|
@ -0,0 +1,270 @@
|
|||
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
|
||||
import DropLab from '~/droplab/drop_lab';
|
||||
|
||||
describe('CloseReopenReportToggle', () => {
|
||||
describe('class constructor', () => {
|
||||
const dropdownTrigger = {};
|
||||
const dropdownList = {};
|
||||
const button = {};
|
||||
let commentTypeToggle;
|
||||
|
||||
beforeEach(function () {
|
||||
commentTypeToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
});
|
||||
|
||||
it('sets .dropdownTrigger', function () {
|
||||
expect(commentTypeToggle.dropdownTrigger).toBe(dropdownTrigger);
|
||||
});
|
||||
|
||||
it('sets .dropdownList', function () {
|
||||
expect(commentTypeToggle.dropdownList).toBe(dropdownList);
|
||||
});
|
||||
|
||||
it('sets .button', function () {
|
||||
expect(commentTypeToggle.button).toBe(button);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initDroplab', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = jasmine.createSpyObj('dropdownList', ['querySelector']);
|
||||
const dropdownTrigger = {};
|
||||
const button = {};
|
||||
const reopenItem = {};
|
||||
const closeItem = {};
|
||||
const config = {};
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(DropLab.prototype, 'init');
|
||||
dropdownList.querySelector.and.returnValues(reopenItem, closeItem);
|
||||
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
spyOn(closeReopenReportToggle, 'setConfig').and.returnValue(config);
|
||||
|
||||
closeReopenReportToggle.initDroplab();
|
||||
});
|
||||
|
||||
it('sets .reopenItem and .closeItem', () => {
|
||||
expect(dropdownList.querySelector).toHaveBeenCalledWith('.reopen-item');
|
||||
expect(dropdownList.querySelector).toHaveBeenCalledWith('.close-item');
|
||||
expect(closeReopenReportToggle.reopenItem).toBe(reopenItem);
|
||||
expect(closeReopenReportToggle.closeItem).toBe(closeItem);
|
||||
});
|
||||
|
||||
it('sets .droplab', () => {
|
||||
expect(closeReopenReportToggle.droplab).toEqual(jasmine.any(Object));
|
||||
});
|
||||
|
||||
it('calls .setConfig', () => {
|
||||
expect(closeReopenReportToggle.setConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls droplab.init', () => {
|
||||
expect(DropLab.prototype.init).toHaveBeenCalledWith(
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
jasmine.any(Array),
|
||||
config,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateButton', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = {};
|
||||
const dropdownTrigger = {};
|
||||
const button = jasmine.createSpyObj('button', ['blur']);
|
||||
const isClosed = true;
|
||||
|
||||
beforeEach(() => {
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
spyOn(closeReopenReportToggle, 'toggleButtonType');
|
||||
|
||||
closeReopenReportToggle.updateButton(isClosed);
|
||||
});
|
||||
|
||||
it('calls .toggleButtonType', () => {
|
||||
expect(closeReopenReportToggle.toggleButtonType).toHaveBeenCalledWith(isClosed);
|
||||
});
|
||||
|
||||
it('calls .button.blur', () => {
|
||||
expect(closeReopenReportToggle.button.blur).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleButtonType', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = {};
|
||||
const dropdownTrigger = {};
|
||||
const button = {};
|
||||
const isClosed = true;
|
||||
const showItem = jasmine.createSpyObj('showItem', ['click']);
|
||||
const hideItem = {};
|
||||
showItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
|
||||
hideItem.classList = jasmine.createSpyObj('classList', ['add', 'remove']);
|
||||
|
||||
beforeEach(() => {
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
spyOn(closeReopenReportToggle, 'getButtonTypes').and.returnValue([showItem, hideItem]);
|
||||
|
||||
closeReopenReportToggle.toggleButtonType(isClosed);
|
||||
});
|
||||
|
||||
it('calls .getButtonTypes', () => {
|
||||
expect(closeReopenReportToggle.getButtonTypes).toHaveBeenCalledWith(isClosed);
|
||||
});
|
||||
|
||||
it('removes hide class and add selected class to showItem, opposite for hideItem', () => {
|
||||
expect(showItem.classList.remove).toHaveBeenCalledWith('hidden');
|
||||
expect(showItem.classList.add).toHaveBeenCalledWith('droplab-item-selected');
|
||||
expect(hideItem.classList.add).toHaveBeenCalledWith('hidden');
|
||||
expect(hideItem.classList.remove).toHaveBeenCalledWith('droplab-item-selected');
|
||||
});
|
||||
|
||||
it('clicks the showItem', () => {
|
||||
expect(showItem.click).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getButtonTypes', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = {};
|
||||
const dropdownTrigger = {};
|
||||
const button = {};
|
||||
const reopenItem = {};
|
||||
const closeItem = {};
|
||||
|
||||
beforeEach(() => {
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
closeReopenReportToggle.reopenItem = reopenItem;
|
||||
closeReopenReportToggle.closeItem = closeItem;
|
||||
});
|
||||
|
||||
it('returns reopenItem, closeItem if isClosed is true', () => {
|
||||
const buttonTypes = closeReopenReportToggle.getButtonTypes(true);
|
||||
|
||||
expect(buttonTypes).toEqual([reopenItem, closeItem]);
|
||||
});
|
||||
|
||||
it('returns closeItem, reopenItem if isClosed is false', () => {
|
||||
const buttonTypes = closeReopenReportToggle.getButtonTypes(false);
|
||||
|
||||
expect(buttonTypes).toEqual([closeItem, reopenItem]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('setDisable', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = {};
|
||||
const dropdownTrigger = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
|
||||
const button = jasmine.createSpyObj('button', ['setAttribute', 'removeAttribute']);
|
||||
|
||||
beforeEach(() => {
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
});
|
||||
|
||||
it('disable .button and .dropdownTrigger if shouldDisable is true', () => {
|
||||
closeReopenReportToggle.setDisable(true);
|
||||
|
||||
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
|
||||
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
|
||||
});
|
||||
|
||||
it('disable .button and .dropdownTrigger if shouldDisable is undefined', () => {
|
||||
closeReopenReportToggle.setDisable();
|
||||
|
||||
expect(button.setAttribute).toHaveBeenCalledWith('disabled', 'true');
|
||||
expect(dropdownTrigger.setAttribute).toHaveBeenCalledWith('disabled', 'true');
|
||||
});
|
||||
|
||||
it('enable .button and .dropdownTrigger if shouldDisable is false', () => {
|
||||
closeReopenReportToggle.setDisable(false);
|
||||
|
||||
expect(button.removeAttribute).toHaveBeenCalledWith('disabled');
|
||||
expect(dropdownTrigger.removeAttribute).toHaveBeenCalledWith('disabled');
|
||||
});
|
||||
});
|
||||
|
||||
describe('setConfig', () => {
|
||||
let closeReopenReportToggle;
|
||||
const dropdownList = {};
|
||||
const dropdownTrigger = {};
|
||||
const button = {};
|
||||
let config;
|
||||
|
||||
beforeEach(() => {
|
||||
closeReopenReportToggle = new CloseReopenReportToggle({
|
||||
dropdownTrigger,
|
||||
dropdownList,
|
||||
button,
|
||||
});
|
||||
|
||||
config = closeReopenReportToggle.setConfig();
|
||||
});
|
||||
|
||||
it('returns a config object', () => {
|
||||
expect(config).toEqual({
|
||||
InputSetter: [
|
||||
{
|
||||
input: button,
|
||||
valueAttribute: 'data-text',
|
||||
inputAttribute: 'data-value',
|
||||
},
|
||||
{
|
||||
input: button,
|
||||
valueAttribute: 'data-text',
|
||||
inputAttribute: 'title',
|
||||
},
|
||||
{
|
||||
input: button,
|
||||
valueAttribute: 'data-button-class',
|
||||
inputAttribute: 'class',
|
||||
},
|
||||
{
|
||||
input: dropdownTrigger,
|
||||
valueAttribute: 'data-toggle-class',
|
||||
inputAttribute: 'class',
|
||||
},
|
||||
{
|
||||
input: button,
|
||||
valueAttribute: 'data-url',
|
||||
inputAttribute: 'href',
|
||||
},
|
||||
{
|
||||
input: button,
|
||||
valueAttribute: 'data-method',
|
||||
inputAttribute: 'data-method',
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,10 +1,10 @@
|
|||
/* eslint-disable space-before-function-paren, one-var, one-var-declaration-per-line, no-use-before-define, comma-dangle, max-len */
|
||||
import Issue from '~/issue';
|
||||
|
||||
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
|
||||
import '~/lib/utils/text_utility';
|
||||
|
||||
describe('Issue', function() {
|
||||
let $boxClosed, $boxOpen, $btnClose, $btnReopen;
|
||||
let $boxClosed, $boxOpen, $btn;
|
||||
|
||||
preloadFixtures('issues/closed-issue.html.raw');
|
||||
preloadFixtures('issues/issue-with-task-list.html.raw');
|
||||
|
@ -20,9 +20,7 @@ describe('Issue', function() {
|
|||
function expectIssueState(isIssueOpen) {
|
||||
expectVisibility($boxClosed, !isIssueOpen);
|
||||
expectVisibility($boxOpen, isIssueOpen);
|
||||
|
||||
expectVisibility($btnClose, isIssueOpen);
|
||||
expectVisibility($btnReopen, !isIssueOpen);
|
||||
expect($btn).toHaveText(isIssueOpen ? 'Close issue' : 'Reopen issue');
|
||||
}
|
||||
|
||||
function expectNewBranchButtonState(isPending, canCreate) {
|
||||
|
@ -57,7 +55,7 @@ describe('Issue', function() {
|
|||
}
|
||||
}
|
||||
|
||||
function findElements() {
|
||||
function findElements(isIssueInitiallyOpen) {
|
||||
$boxClosed = $('div.status-box-closed');
|
||||
expect($boxClosed).toExist();
|
||||
expect($boxClosed).toHaveText('Closed');
|
||||
|
@ -66,13 +64,9 @@ describe('Issue', function() {
|
|||
expect($boxOpen).toExist();
|
||||
expect($boxOpen).toHaveText('Open');
|
||||
|
||||
$btnClose = $('.btn-close.btn-grouped');
|
||||
expect($btnClose).toExist();
|
||||
expect($btnClose).toHaveText('Close issue');
|
||||
|
||||
$btnReopen = $('.btn-reopen.btn-grouped');
|
||||
expect($btnReopen).toExist();
|
||||
expect($btnReopen).toHaveText('Reopen issue');
|
||||
$btn = $('.js-issuable-close-button');
|
||||
expect($btn).toExist();
|
||||
expect($btn).toHaveText(isIssueInitiallyOpen ? 'Close issue' : 'Reopen issue');
|
||||
}
|
||||
|
||||
describe('task lists', function() {
|
||||
|
@ -99,7 +93,6 @@ describe('Issue', function() {
|
|||
function ajaxSpy(req) {
|
||||
if (req.url === this.$triggeredButton.attr('href')) {
|
||||
expect(req.type).toBe('PUT');
|
||||
expect(this.$triggeredButton).toHaveProp('disabled', true);
|
||||
expectNewBranchButtonState(true, false);
|
||||
return this.issueStateDeferred;
|
||||
} else if (req.url === Issue.createMrDropdownWrap.dataset.canCreatePath) {
|
||||
|
@ -119,10 +112,11 @@ describe('Issue', function() {
|
|||
loadFixtures('issues/closed-issue.html.raw');
|
||||
}
|
||||
|
||||
findElements();
|
||||
findElements(isIssueInitiallyOpen);
|
||||
this.issue = new Issue();
|
||||
expectIssueState(isIssueInitiallyOpen);
|
||||
this.$triggeredButton = isIssueInitiallyOpen ? $btnClose : $btnReopen;
|
||||
|
||||
this.$triggeredButton = $btn;
|
||||
|
||||
this.$projectIssuesCounter = $('.issue_counter');
|
||||
this.$projectIssuesCounter.text('1,001');
|
||||
|
@ -143,7 +137,7 @@ describe('Issue', function() {
|
|||
});
|
||||
|
||||
expectIssueState(!isIssueInitiallyOpen);
|
||||
expect(this.$triggeredButton).toHaveProp('disabled', false);
|
||||
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
|
||||
expect(this.$projectIssuesCounter.text()).toBe(isIssueInitiallyOpen ? '1,000' : '1,002');
|
||||
expectNewBranchButtonState(false, !isIssueInitiallyOpen);
|
||||
});
|
||||
|
@ -158,7 +152,7 @@ describe('Issue', function() {
|
|||
});
|
||||
|
||||
expectIssueState(isIssueInitiallyOpen);
|
||||
expect(this.$triggeredButton).toHaveProp('disabled', false);
|
||||
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
|
||||
expectErrorMessage();
|
||||
expect(this.$projectIssuesCounter.text()).toBe('1,001');
|
||||
expectNewBranchButtonState(false, isIssueInitiallyOpen);
|
||||
|
@ -172,7 +166,7 @@ describe('Issue', function() {
|
|||
});
|
||||
|
||||
expectIssueState(isIssueInitiallyOpen);
|
||||
expect(this.$triggeredButton).toHaveProp('disabled', true);
|
||||
expect(this.$triggeredButton.get(0).getAttribute('disabled')).toBeNull();
|
||||
expectErrorMessage();
|
||||
expect(this.$projectIssuesCounter.text()).toBe('1,001');
|
||||
expectNewBranchButtonState(false, isIssueInitiallyOpen);
|
||||
|
@ -195,4 +189,37 @@ describe('Issue', function() {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('units', () => {
|
||||
describe('class constructor', () => {
|
||||
it('calls .initCloseReopenReport', () => {
|
||||
spyOn(Issue.prototype, 'initCloseReopenReport');
|
||||
|
||||
new Issue(); // eslint-disable-line no-new
|
||||
|
||||
expect(Issue.prototype.initCloseReopenReport).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('initCloseReopenReport', () => {
|
||||
it('calls .initDroplab', () => {
|
||||
const container = jasmine.createSpyObj('container', ['querySelector']);
|
||||
const dropdownTrigger = {};
|
||||
const dropdownList = {};
|
||||
const button = {};
|
||||
|
||||
spyOn(document, 'querySelector').and.returnValue(container);
|
||||
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
|
||||
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
|
||||
|
||||
Issue.prototype.initCloseReopenReport();
|
||||
|
||||
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
|
||||
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
/* global MergeRequest */
|
||||
|
||||
import '~/merge_request';
|
||||
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
|
||||
import IssuablesHelper from '~/helpers/issuables_helper';
|
||||
|
||||
(function() {
|
||||
describe('MergeRequest', function() {
|
||||
return describe('task lists', function() {
|
||||
describe('task lists', function() {
|
||||
preloadFixtures('merge_requests/merge_request_with_task_list.html.raw');
|
||||
beforeEach(function() {
|
||||
loadFixtures('merge_requests/merge_request_with_task_list.html.raw');
|
||||
|
@ -27,5 +29,34 @@ import '~/merge_request';
|
|||
return $('.js-task-list-field').trigger('tasklist:changed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('class constructor', () => {
|
||||
it('calls .initCloseReopenReport', () => {
|
||||
spyOn(IssuablesHelper, 'initCloseReopenReport');
|
||||
|
||||
new MergeRequest(); // eslint-disable-line no-new
|
||||
|
||||
expect(IssuablesHelper.initCloseReopenReport).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls .initDroplab', () => {
|
||||
const container = jasmine.createSpyObj('container', ['querySelector']);
|
||||
const dropdownTrigger = {};
|
||||
const dropdownList = {};
|
||||
const button = {};
|
||||
|
||||
spyOn(CloseReopenReportToggle.prototype, 'initDroplab');
|
||||
spyOn(document, 'querySelector').and.returnValue(container);
|
||||
container.querySelector.and.returnValues(dropdownTrigger, dropdownList, button);
|
||||
|
||||
new MergeRequest(); // eslint-disable-line no-new
|
||||
|
||||
expect(document.querySelector).toHaveBeenCalledWith('.js-issuable-close-dropdown');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-toggle');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-menu');
|
||||
expect(container.querySelector).toHaveBeenCalledWith('.js-issuable-close-button');
|
||||
expect(CloseReopenReportToggle.prototype.initDroplab).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
}).call(window);
|
||||
|
|
|
@ -3,7 +3,7 @@ require 'spec_helper'
|
|||
describe ExtractsPath, lib: true do
|
||||
include ExtractsPath
|
||||
include RepoHelpers
|
||||
include Gitlab::Routing.url_helpers
|
||||
include Gitlab::Routing
|
||||
|
||||
let(:project) { double('project') }
|
||||
let(:request) { double('request') }
|
||||
|
|
|
@ -111,7 +111,7 @@ describe Gitlab::Git::Blob, seed_helper: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.raw' do
|
||||
shared_examples 'finding blobs by ID' do
|
||||
let(:raw_blob) { Gitlab::Git::Blob.raw(repository, SeedRepo::RubyBlob::ID) }
|
||||
it { expect(raw_blob.id).to eq(SeedRepo::RubyBlob::ID) }
|
||||
it { expect(raw_blob.data[0..10]).to eq("require \'fi") }
|
||||
|
@ -136,6 +136,16 @@ describe Gitlab::Git::Blob, seed_helper: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.raw' do
|
||||
context 'when the blob_raw Gitaly feature is enabled' do
|
||||
it_behaves_like 'finding blobs by ID'
|
||||
end
|
||||
|
||||
context 'when the blob_raw Gitaly feature is disabled', skip_gitaly_mock: true do
|
||||
it_behaves_like 'finding blobs by ID'
|
||||
end
|
||||
end
|
||||
|
||||
describe 'encoding' do
|
||||
context 'file with russian text' do
|
||||
let(:blob) { Gitlab::Git::Blob.find(repository, SeedRepo::Commit::ID, "encoding/russian.rb") }
|
||||
|
|
|
@ -0,0 +1,92 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Gitlab::PerformanceBar do
|
||||
shared_examples 'allowed user IDs are cached' do
|
||||
before do
|
||||
# Warm the Redis cache
|
||||
described_class.enabled?(user)
|
||||
end
|
||||
|
||||
it 'caches the allowed user IDs in cache', :caching do
|
||||
expect do
|
||||
expect(described_class.enabled?(user)).to be_truthy
|
||||
end.not_to exceed_query_limit(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe '.enabled?' do
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
stub_application_setting(performance_bar_allowed_group_id: -1)
|
||||
end
|
||||
|
||||
it 'returns false when given user is nil' do
|
||||
expect(described_class.enabled?(nil)).to be_falsy
|
||||
end
|
||||
|
||||
it 'returns false when allowed_group_id is nil' do
|
||||
expect(described_class).to receive(:allowed_group_id).and_return(nil)
|
||||
|
||||
expect(described_class.enabled?(user)).to be_falsy
|
||||
end
|
||||
|
||||
context 'when allowed group ID does not exist' do
|
||||
it 'returns false' do
|
||||
expect(described_class.enabled?(user)).to be_falsy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when allowed group exists' do
|
||||
let!(:my_group) { create(:group, path: 'my-group') }
|
||||
|
||||
before do
|
||||
stub_application_setting(performance_bar_allowed_group_id: my_group.id)
|
||||
end
|
||||
|
||||
context 'when user is not a member of the allowed group' do
|
||||
it 'returns false' do
|
||||
expect(described_class.enabled?(user)).to be_falsy
|
||||
end
|
||||
|
||||
it_behaves_like 'allowed user IDs are cached'
|
||||
end
|
||||
|
||||
context 'when user is a member of the allowed group' do
|
||||
before do
|
||||
my_group.add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(described_class.enabled?(user)).to be_truthy
|
||||
end
|
||||
|
||||
it_behaves_like 'allowed user IDs are cached'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when allowed group is nested', :nested_groups do
|
||||
let!(:nested_my_group) { create(:group, parent: create(:group, path: 'my-org'), path: 'my-group') }
|
||||
|
||||
before do
|
||||
create(:group, path: 'my-group')
|
||||
nested_my_group.add_developer(user)
|
||||
stub_application_setting(performance_bar_allowed_group_id: nested_my_group.id)
|
||||
end
|
||||
|
||||
it 'returns the nested group' do
|
||||
expect(described_class.enabled?(user)).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when a nested group has the same path', :nested_groups do
|
||||
before do
|
||||
create(:group, :nested, path: 'my-group').add_developer(user)
|
||||
end
|
||||
|
||||
it 'returns false' do
|
||||
expect(described_class.enabled?(user)).to be_falsy
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -214,6 +214,160 @@ describe ApplicationSetting, models: true do
|
|||
end
|
||||
end
|
||||
|
||||
describe 'performance bar settings' do
|
||||
describe 'performance_bar_allowed_group_id=' do
|
||||
context 'with a blank path' do
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = create(:group).full_path
|
||||
end
|
||||
|
||||
it 'persists nil for a "" path and clears allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_allowed_group_id = ''
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an invalid path' do
|
||||
it 'does not persist an invalid group path' do
|
||||
setting.performance_bar_allowed_group_id = 'foo'
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a path to an existing group' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
it 'persists a valid group path and clears allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
|
||||
end
|
||||
|
||||
context 'when the given path is the same' do
|
||||
context 'with a blank path' do
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = nil
|
||||
end
|
||||
|
||||
it 'clears the cached allowed user IDs' do
|
||||
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_allowed_group_id = ''
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a valid path' do
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
end
|
||||
|
||||
it 'clears the cached allowed user IDs' do
|
||||
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'performance_bar_allowed_group' do
|
||||
context 'with no performance_bar_allowed_group_id saved' do
|
||||
it 'returns nil' do
|
||||
expect(setting.performance_bar_allowed_group).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with a performance_bar_allowed_group_id saved' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
end
|
||||
|
||||
it 'returns the group' do
|
||||
expect(setting.performance_bar_allowed_group).to eq(group)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'performance_bar_enabled' do
|
||||
context 'with the Performance Bar is enabled' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
end
|
||||
|
||||
it 'returns true' do
|
||||
expect(setting.performance_bar_enabled).to be_truthy
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'performance_bar_enabled=' do
|
||||
context 'when the performance bar is enabled' do
|
||||
let(:group) { create(:group) }
|
||||
|
||||
before do
|
||||
setting.performance_bar_allowed_group_id = group.full_path
|
||||
end
|
||||
|
||||
context 'when passing true' do
|
||||
it 'does not clear allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_enabled = true
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to eq(group.id)
|
||||
expect(setting.performance_bar_enabled).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing false' do
|
||||
it 'disables the performance bar and clears allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_enabled = false
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to be_nil
|
||||
expect(setting.performance_bar_enabled).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the performance bar is disabled' do
|
||||
context 'when passing true' do
|
||||
it 'does nothing and does not clear allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_enabled = true
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to be_nil
|
||||
expect(setting.performance_bar_enabled).to be_falsey
|
||||
end
|
||||
end
|
||||
|
||||
context 'when passing false' do
|
||||
it 'does nothing and does not clear allowed user IDs cache' do
|
||||
expect(Gitlab::PerformanceBar).not_to receive(:expire_allowed_user_ids_cache)
|
||||
|
||||
setting.performance_bar_enabled = false
|
||||
|
||||
expect(setting.performance_bar_allowed_group_id).to be_nil
|
||||
expect(setting.performance_bar_enabled).to be_falsey
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'usage ping settings' do
|
||||
context 'when the usage ping is disabled in gitlab.yml' do
|
||||
before do
|
||||
|
|
|
@ -0,0 +1,53 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe EachBatch do
|
||||
describe '.each_batch' do
|
||||
let(:model) do
|
||||
Class.new(ActiveRecord::Base) do
|
||||
include EachBatch
|
||||
|
||||
self.table_name = 'users'
|
||||
end
|
||||
end
|
||||
|
||||
before do
|
||||
5.times { create(:user, updated_at: 1.day.ago) }
|
||||
end
|
||||
|
||||
it 'yields an ActiveRecord::Relation when a block is given' do
|
||||
model.each_batch do |relation|
|
||||
expect(relation).to be_a_kind_of(ActiveRecord::Relation)
|
||||
end
|
||||
end
|
||||
|
||||
it 'yields a batch index as the second argument' do
|
||||
model.each_batch do |_, index|
|
||||
expect(index).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
it 'accepts a custom batch size' do
|
||||
amount = 0
|
||||
|
||||
model.each_batch(of: 1) { amount += 1 }
|
||||
|
||||
expect(amount).to eq(5)
|
||||
end
|
||||
|
||||
it 'does not include ORDER BYs in the yielded relations' do
|
||||
model.each_batch do |relation|
|
||||
expect(relation.to_sql).not_to include('ORDER BY')
|
||||
end
|
||||
end
|
||||
|
||||
it 'allows updating of the yielded relations' do
|
||||
time = Time.now
|
||||
|
||||
model.each_batch do |relation|
|
||||
relation.update_all(updated_at: time)
|
||||
end
|
||||
|
||||
expect(model.where(updated_at: time).count).to eq(5)
|
||||
end
|
||||
end
|
||||
end
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue