summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc3
-rw-r--r--app/assets/javascripts/dispatcher.js.es63
-rw-r--r--app/assets/javascripts/droplab/droplab.js67
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax.js14
-rw-r--r--app/assets/javascripts/droplab/droplab_ajax_filter.js3
-rw-r--r--app/assets/javascripts/droplab/droplab_filter.js18
-rw-r--r--app/assets/javascripts/environments/environments_bundle.js.es61
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js.es62
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js.es619
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es62
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js.es614
-rw-r--r--app/assets/javascripts/notes.js21
-rw-r--r--app/assets/javascripts/version_check_image.js.es610
-rw-r--r--app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es647
-rw-r--r--app/assets/javascripts/vue_pipelines_index/stage.js.es67
-rw-r--r--app/assets/javascripts/vue_pipelines_index/store.js.es618
-rw-r--r--app/assets/stylesheets/framework/blocks.scss12
-rw-r--r--app/assets/stylesheets/framework/filters.scss24
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/helpers/version_check_helper.rb3
-rw-r--r--app/models/merge_request.rb8
-rw-r--r--app/models/user.rb4
-rw-r--r--app/views/discussions/_discussion.html.haml3
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml8
-rw-r--r--app/views/projects/commits/show.html.haml7
-rw-r--r--changelogs/unreleased/26445-accessible-piplelines-buttons.yml4
-rw-r--r--changelogs/unreleased/26447-fix-tab-list-order.yml4
-rw-r--r--changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml4
-rw-r--r--changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml4
-rw-r--r--changelogs/unreleased/fix_broken_diff_discussions.yml4
-rw-r--r--changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml4
-rw-r--r--changelogs/unreleased/newline-eslint-rule.yml4
-rw-r--r--config/initializers/metrics.rb219
-rw-r--r--doc/administration/raketasks/maintenance.md1
-rw-r--r--lib/tasks/gitlab/info.rake5
-rw-r--r--spec/features/issues/filtered_search/dropdown_label_spec.rb10
-rw-r--r--spec/features/issues/filtered_search/search_bar_spec.rb16
-rw-r--r--spec/initializers/metrics_spec.rb16
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es61
-rw-r--r--spec/javascripts/environments/environment_rollback_spec.js.es61
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js.es617
-rw-r--r--spec/models/user_spec.rb47
43 files changed, 477 insertions, 232 deletions
diff --git a/.eslintrc b/.eslintrc
index e13f76b213c..9ab0145820d 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -15,6 +15,7 @@
"filenames"
],
"rules": {
- "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"]
+ "filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
+ "no-multiple-empty-lines": ["error", { "max": 1 }]
}
}
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index dcf67a8fd68..529d476ca4e 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -261,6 +261,9 @@
case 'projects:artifacts:browse':
new BuildArtifacts();
break;
+ case 'help:index':
+ gl.VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
+ break;
case 'search:show':
new Search();
break;
diff --git a/app/assets/javascripts/droplab/droplab.js b/app/assets/javascripts/droplab/droplab.js
index c79f0230951..8b14191395b 100644
--- a/app/assets/javascripts/droplab/droplab.js
+++ b/app/assets/javascripts/droplab/droplab.js
@@ -58,6 +58,7 @@ var CustomEvent = require('./custom_event_polyfill');
var utils = require('./utils');
var DropDown = function(list) {
+ this.currentIndex = 0;
this.hidden = true;
this.list = list;
this.items = [];
@@ -164,15 +165,21 @@ Object.assign(DropDown.prototype, {
},
show: function() {
- // debugger
- this.list.style.display = 'block';
- this.hidden = false;
+ if (this.hidden) {
+ // debugger
+ this.list.style.display = 'block';
+ this.currentIndex = 0;
+ this.hidden = false;
+ }
},
hide: function() {
- // debugger
- this.list.style.display = 'none';
- this.hidden = true;
+ if (!this.hidden) {
+ // debugger
+ this.list.style.display = 'none';
+ this.currentIndex = 0;
+ this.hidden = true;
+ }
},
destroy: function() {
@@ -478,6 +485,8 @@ Object.assign(HookInput.prototype, {
this.input = function input(e) {
if(self.hasRemovedEvents) return;
+ self.list.show();
+
var inputEvent = new CustomEvent('input.dl', {
detail: {
hook: self,
@@ -487,7 +496,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(inputEvent);
- self.list.show();
}
this.keyup = function keyup(e) {
@@ -503,6 +511,8 @@ Object.assign(HookInput.prototype, {
}
function keyEvent(e, keyEventName){
+ self.list.show();
+
var keyEvent = new CustomEvent(keyEventName, {
detail: {
hook: self,
@@ -514,7 +524,6 @@ Object.assign(HookInput.prototype, {
cancelable: true
});
e.target.dispatchEvent(keyEvent);
- self.list.show();
}
this.events = this.events || {};
@@ -572,24 +581,43 @@ require('./window')(function(w){
module.exports = function(){
var currentKey;
var currentFocus;
- var currentIndex = 0;
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
- var listItems = list.list.querySelectorAll('li');
+ var listItems = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var listItemsTmp = [];
for(var i = 0; i < listItems.length; i++) {
- listItems[i].classList.remove('dropdown-active');
+ var listItem = listItems[i];
+ listItem.classList.remove('dropdown-active');
+
+ if (listItem.style.display !== 'none') {
+ listItemsTmp.push(listItem);
+ }
}
- return listItems;
+ return listItemsTmp;
};
var setMenuForArrows = function setMenuForArrows(list) {
var listItems = removeHighlight(list);
- if(currentIndex>0){
- if(!listItems[currentIndex-1]){
- currentIndex = currentIndex-1;
+ if(list.currentIndex>0){
+ if(!listItems[list.currentIndex-1]){
+ list.currentIndex = list.currentIndex-1;
+ }
+
+ if (listItems[list.currentIndex-1]) {
+ var el = listItems[list.currentIndex-1];
+ var filterDropdownEl = el.closest('.filter-dropdown');
+ el.classList.add('dropdown-active');
+
+ if (filterDropdownEl) {
+ var filterDropdownBottom = filterDropdownEl.offsetHeight;
+ var elOffsetTop = el.offsetTop - 30;
+
+ if (elOffsetTop > filterDropdownBottom) {
+ filterDropdownEl.scrollTop = elOffsetTop - filterDropdownBottom;
+ }
+ }
}
- listItems[currentIndex-1].classList.add('dropdown-active');
}
};
@@ -597,13 +625,13 @@ require('./window')(function(w){
var list = e.detail.hook.list;
removeHighlight(list);
list.show();
- currentIndex = 0;
+ list.currentIndex = 0;
isUpArrow = false;
isDownArrow = false;
};
var selectItem = function selectItem(list) {
var listItems = removeHighlight(list);
- var currentItem = listItems[currentIndex-1];
+ var currentItem = listItems[list.currentIndex-1];
var listEvent = new CustomEvent('click.dl', {
detail: {
list: list,
@@ -617,6 +645,8 @@ require('./window')(function(w){
var keydown = function keydown(e){
var typedOn = e.target;
+ var list = e.detail.hook.list;
+ var currentIndex = list.currentIndex;
isUpArrow = false;
isDownArrow = false;
@@ -648,6 +678,7 @@ require('./window')(function(w){
if(isUpArrow){ currentIndex--; }
if(isDownArrow){ currentIndex++; }
if(currentIndex < 0){ currentIndex = 0; }
+ list.currentIndex = currentIndex;
setMenuForArrows(e.detail.hook.list);
};
diff --git a/app/assets/javascripts/droplab/droplab_ajax.js b/app/assets/javascripts/droplab/droplab_ajax.js
index f20610b3811..f7fed0987a2 100644
--- a/app/assets/javascripts/droplab/droplab_ajax.js
+++ b/app/assets/javascripts/droplab/droplab_ajax.js
@@ -29,6 +29,7 @@ require('../window')(function(w){
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
+ this.hook = hook;
if (!config || !config.endpoint || !config.method) {
return;
@@ -52,19 +53,26 @@ require('../window')(function(w){
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
- var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
+ var dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
- hook.list[config.method].call(hook.list, d);
+
+ if (!self.hook.list.hidden) {
+ self.hook.list[config.method].call(self.hook.list, d);
+ }
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
+ if (this.listTemplate) {
+ var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
+ dynamicList.outerHTML = this.listTemplate;
+ }
}
};
});
@@ -76,4 +84,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_ajax_filter.js b/app/assets/javascripts/droplab/droplab_ajax_filter.js
index af163f76851..86a08d0d01d 100644
--- a/app/assets/javascripts/droplab/droplab_ajax_filter.js
+++ b/app/assets/javascripts/droplab/droplab_ajax_filter.js
@@ -93,6 +93,7 @@ require('../window')(function(w){
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
+ self.hook.list.currentIndex = 0;
});
},
@@ -142,4 +143,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/droplab/droplab_filter.js b/app/assets/javascripts/droplab/droplab_filter.js
index 41a220831f9..9b40a3f20a4 100644
--- a/app/assets/javascripts/droplab/droplab_filter.js
+++ b/app/assets/javascripts/droplab/droplab_filter.js
@@ -6,6 +6,8 @@ require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
+ var hiddenCount = 0;
+ var dataHiddenCount = 0;
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
@@ -27,10 +29,22 @@ require('../window')(function(w){
};
}
+ dataHiddenCount = data.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
matches = data.map(function(o) {
return filterFunction(o, value);
});
- list.render(matches);
+
+ hiddenCount = matches.filter(function(o) {
+ return !o.droplab_hidden;
+ }).length;
+
+ if (dataHiddenCount !== hiddenCount) {
+ list.render(matches);
+ list.currentIndex = 0;
+ }
},
init: function init(hookInput) {
@@ -57,4 +71,4 @@ module.exports = function(callback) {
};
},{}]},{},[1])(1)
-}); \ No newline at end of file
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js.es6 b/app/assets/javascripts/environments/environments_bundle.js.es6
index 9f24a6a4f88..3b003f6f661 100644
--- a/app/assets/javascripts/environments/environments_bundle.js.es6
+++ b/app/assets/javascripts/environments/environments_bundle.js.es6
@@ -3,7 +3,6 @@
//= require ./components/environment
//= require ./vue_resource_interceptor
-
$(() => {
window.gl = window.gl || {};
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6 b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
index 443ac222f70..eeab10fba17 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js.es6
@@ -84,7 +84,7 @@
let inputValue = input.value;
// Replace all spaces inside quote marks with underscores
// This helps with matching the beginning & end of a token:key
- inputValue = inputValue.replace(/"(.*?)"/g, str => str.replace(/\s/g, '_'));
+ inputValue = inputValue.replace(/("(.*?)"|:\s+)/g, str => str.replace(/\s/g, '_'));
// Get the right position for the word selected
// Regex matches first space
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6 b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
index ae19bb68360..8d62324b79f 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js.es6
@@ -64,13 +64,26 @@
}
checkForEnter(e) {
+ if (e.keyCode === 38 || e.keyCode === 40) {
+ const selectionStart = this.filteredSearchInput.selectionStart;
+
+ e.preventDefault();
+ this.filteredSearchInput.setSelectionRange(selectionStart, selectionStart);
+ }
+
if (e.keyCode === 13) {
+ const dropdown = this.dropdownManager.mapping[this.dropdownManager.currentDropdown];
+ const dropdownEl = dropdown.element;
+ const activeElements = dropdownEl.querySelectorAll('.dropdown-active');
+
e.preventDefault();
- // Prevent droplab from opening dropdown
- this.dropdownManager.destroyDroplab();
+ if (!activeElements.length) {
+ // Prevent droplab from opening dropdown
+ this.dropdownManager.destroyDroplab();
- this.search();
+ this.search();
+ }
}
}
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index c260ad03d47..e0ebd36a65c 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -61,7 +61,6 @@
return labels;
}
-
/**
* Will return only labels that were marked previously and the user has unmarked
* @return {Array} Label IDs
@@ -80,7 +79,6 @@
return result;
}
-
/**
* Simple form serialization, it will return just what we need
* Returns key/value pairs from form data
diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6
index 6d57d31f380..7452879d9a3 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js.es6
+++ b/app/assets/javascripts/lib/utils/common_utils.js.es6
@@ -159,5 +159,19 @@
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
+
+ /**
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
+ */
+ w.gl.utils.normalizeHeaders = (headers) => {
+ const upperCaseHeaders = {};
+
+ Object.keys(headers).forEach((e) => {
+ upperCaseHeaders[e.toUpperCase()] = headers[e];
+ });
+
+ return upperCaseHeaders;
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 06a72efa21d..9db830a7ada 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -220,7 +220,6 @@
})(this));
};
-
/*
Increase @pollingInterval up to 120 seconds on every function call,
if `shouldReset` has a truthy value, 'null' or 'undefined' the variable
@@ -244,7 +243,6 @@
return this.initRefresh();
};
-
Notes.prototype.handleCreateChanges = function(note) {
if (typeof note === 'undefined') {
return;
@@ -294,7 +292,6 @@
}
};
-
/*
Check if note does not exists on page
*/
@@ -307,7 +304,6 @@
return this.view === 'parallel';
};
-
/*
Render note in discussion area.
@@ -358,7 +354,6 @@
return this.updateNotesCount(1);
};
-
/*
Called in response the main target form has been successfully submitted.
@@ -390,7 +385,6 @@
return form.find(".js-note-text").trigger("input");
};
-
/*
Shows the main form and does some setup on it.
@@ -415,7 +409,6 @@
return this.parentTimeline = form.parents('.timeline');
};
-
/*
General note form setup.
@@ -432,7 +425,6 @@
return new Autosave(textarea, ["Note", form.find("#note_noteable_type").val(), form.find("#note_noteable_id").val(), form.find("#note_commit_id").val(), form.find("#note_type").val(), form.find("#note_line_code").val(), form.find("#note_position").val()]);
};
-
/*
Called in response to the new note form being submitted
@@ -448,7 +440,6 @@
return new Flash('Your comment could not be submitted! Please check your network connection and try again.', 'alert', this.parentTimeline);
};
-
/*
Called in response to the new note form being submitted
@@ -473,7 +464,6 @@
this.removeDiscussionNoteForm($form);
};
-
/*
Called in response to the edit note form being submitted
@@ -498,7 +488,6 @@
}
};
-
Notes.prototype.checkContentToAllowEditing = function($el) {
var initialContent = $el.find('.original-note-content').text().trim();
var currentContent = $el.find('.note-textarea').val();
@@ -522,7 +511,6 @@
return isAllowed;
};
-
/*
Called in response to clicking the edit note link
@@ -551,7 +539,6 @@
this.putEditFormInPlace($target);
};
-
/*
Called in response to clicking the edit note link
@@ -596,7 +583,6 @@
return form.find('.js-note-text').val(form.find('form.edit-note').data('original-note'));
};
-
/*
Called in response to deleting a note of any kind.
@@ -636,7 +622,6 @@
return this.updateNotesCount(-1);
};
-
/*
Called in response to clicking the delete attachment link
@@ -653,7 +638,6 @@
return note.find(".current-note-edit-form").remove();
};
-
/*
Called when clicking on the "reply" button for a diff line.
@@ -673,7 +657,6 @@
return this.setupDiscussionNoteForm(replyLink, form);
};
-
/*
Shows the diff or discussion form and does some setup on it.
@@ -715,7 +698,6 @@
.addClass("discussion-form js-discussion-note-form");
};
-
/*
Called when clicking on the "add a comment" button on the side of a diff line.
@@ -772,7 +754,6 @@
}
};
-
/*
Called in response to "cancel" on a diff note form.
@@ -806,7 +787,6 @@
return this.removeDiscussionNoteForm(form);
};
-
/*
Called after an attachment file has been selected.
@@ -821,7 +801,6 @@
return form.find(".js-attachment-filename").text(filename);
};
-
/*
Called when the tab visibility changes
*/
diff --git a/app/assets/javascripts/version_check_image.js.es6 b/app/assets/javascripts/version_check_image.js.es6
new file mode 100644
index 00000000000..1fa2b5ac399
--- /dev/null
+++ b/app/assets/javascripts/version_check_image.js.es6
@@ -0,0 +1,10 @@
+(() => {
+ class VersionCheckImage {
+ static bindErrorEvent(imageElement) {
+ imageElement.off('error').on('error', () => imageElement.hide());
+ }
+ }
+
+ window.gl = window.gl || {};
+ gl.VersionCheckImage = VersionCheckImage;
+})();
diff --git a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6 b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
index ad5cb30cc42..b195b0ef3ba 100644
--- a/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/pipeline_actions.js.es6
@@ -22,47 +22,51 @@
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
- <a
+ <button
v-if='actions'
- class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
+ class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
- alt="Manual Build"
+ data-placement="top"
+ data-toggle="dropdown"
+ aria-label="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <i class="fa fa-caret-down"></i>
- </a>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
- title="Manual build"
>
- <span v-html='svgs.iconPlay'></span>
- <span title="Manual build">{{action.name}}</span>
+ <span v-html='svgs.iconPlay' aria-hidden="true"></span>
+ <span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
- <a
+ <button
v-if='artifacts'
- class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
+ class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
+ data-toggle="dropdown"
+ title="Artifacts"
+ data-placement="top"
data-toggle="dropdown"
- type="button"
+ aria-label="Artifacts"
>
- <i class="fa fa-download"></i>
- <i class="fa fa-caret-down"></i>
- </a>
+ <i class="fa fa-download" aria-hidden="true"></i>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
+ </button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
- <i class="fa fa-download"></i>
+ <i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
@@ -76,9 +80,12 @@
title="Retry"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.retry_path'
+ aria-label="Retry"
>
- <i class="fa fa-repeat"></i>
+ <i class="fa fa-repeat" aria-hidden="true"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
@@ -86,10 +93,12 @@
title="Cancel"
rel="nofollow"
data-method="post"
+ data-placement="top"
+ data-toggle="dropdown"
:href='pipeline.cancel_path'
- data-original-title="Cancel"
+ aria-label="Cancel"
>
- <i class="fa fa-remove"></i>
+ <i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</div>
diff --git a/app/assets/javascripts/vue_pipelines_index/stage.js.es6 b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
index 4e85f16ebc5..496df9aaced 100644
--- a/app/assets/javascripts/vue_pipelines_index/stage.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/stage.js.es6
@@ -82,12 +82,13 @@
data-placement="top"
data-toggle="dropdown"
type="button"
+ :aria-label='stage.title'
>
- <span v-html="svg"></span>
- <i class="fa fa-caret-down "></i>
+ <span v-html="svg" aria-hidden="true"></span>
+ <i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
- <div class="arrow-up"></div>
+ <div class="arrow-up" aria-hidden="true"></div>
<div
@click='keepGraph($event)'
:class="dropdownClass"
diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6
index 1982142853a..9e19b1564dc 100644
--- a/app/assets/javascripts/vue_pipelines_index/store.js.es6
+++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6
@@ -4,19 +4,15 @@
((gl) => {
const pageValues = (headers) => {
- const normalizedHeaders = {};
-
- Object.keys(headers).forEach((e) => {
- normalizedHeaders[e.toUpperCase()] = headers[e];
- });
+ const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
- perPage: +normalizedHeaders['X-PER-PAGE'],
- page: +normalizedHeaders['X-PAGE'],
- total: +normalizedHeaders['X-TOTAL'],
- totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
- nextPage: +normalizedHeaders['X-NEXT-PAGE'],
- previousPage: +normalizedHeaders['X-PREV-PAGE'],
+ perPage: +normalized['X-PER-PAGE'],
+ page: +normalized['X-PAGE'],
+ total: +normalized['X-TOTAL'],
+ totalPages: +normalized['X-TOTAL-PAGES'],
+ nextPage: +normalized['X-NEXT-PAGE'],
+ previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo;
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 407c800feb7..592ef0d647f 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -82,7 +82,12 @@
}
.block-controls {
- float: right;
+ display: -webkit-flex;
+ display: flex;
+ -webkit-justify-content: flex-end;
+ justify-content: flex-end;
+ -webkit-flex: 1;
+ flex: 1;
.control {
float: left;
@@ -282,3 +287,8 @@
}
}
}
+
+.flex-container-block {
+ display: -webkit-flex;
+ display: flex;
+}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index d957ec64654..4b05ec691a8 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -79,6 +79,16 @@
overflow: auto;
}
+%filter-dropdown-item-btn-hover {
+ background-color: $dropdown-hover-color;
+ color: $white-light;
+ text-decoration: none;
+
+ .avatar {
+ border-color: $white-light;
+ }
+}
+
.filter-dropdown-item {
.btn {
border: none;
@@ -103,13 +113,7 @@
&:hover,
&:focus {
- background-color: $dropdown-hover-color;
- color: $white-light;
- text-decoration: none;
-
- .avatar {
- border-color: $white-light;
- }
+ @extend %filter-dropdown-item-btn-hover;
}
}
@@ -131,6 +135,12 @@
}
}
+.filter-dropdown-item.dropdown-active {
+ .btn {
+ @extend %filter-dropdown-item-btn-hover;
+ }
+}
+
.hint-dropdown {
width: 250px;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 8dff22e32bd..5d4bd091a6b 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -288,6 +288,10 @@
}
}
}
+
+ .tooltip {
+ white-space: nowrap;
+ }
}
.build-link {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index cd0839e58ea..1b0bf4554e6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -929,8 +929,32 @@ pre.light-well {
.variables-table {
table-layout: fixed;
+ &.table-responsive {
+ border: none;
+ }
+
.variable-key {
- width: 30%;
+ width: 300px;
+ max-width: 300px;
+ overflow: hidden;
+ word-wrap: break-word;
+
+ // override bootstrap
+ white-space: normal!important;
+
+ @media (max-width: $screen-sm-max) {
+ width: 150px;
+ max-width: 150px;
+ }
+ }
+
+ .variable-value {
+ @media(max-width: $screen-xs-max) {
+ width: 150px;
+ max-width: 150px;
+ overflow: hidden;
+ word-wrap: break-word;
+ }
}
}
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index a674564c4ec..456598b4c28 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,7 +1,8 @@
module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
- image_tag VersionCheck.new.url
+ image_url = VersionCheck.new.url
+ image_tag image_url, class: 'js-version-status-badge'
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index cd5b345bae5..6753504acff 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -865,9 +865,11 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
+ transaction do
+ active_diff_notes.each do |note|
+ service.execute(note)
+ Gitlab::Timeless.timeless(note, &:save)
+ end
end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 06dd98a3188..2caa66dd9f7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -179,8 +179,8 @@ class User < ActiveRecord::Base
scope :not_in_project, ->(project) { project.users.present? ? where("id not in (:ids)", ids: project.users.map(&:id) ) : all }
scope :without_projects, -> { where('id NOT IN (SELECT DISTINCT(user_id) FROM members WHERE user_id IS NOT NULL AND requested_at IS NULL)') }
scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
- scope :order_recent_sign_in, -> { reorder(last_sign_in_at: :desc) }
- scope :order_oldest_sign_in, -> { reorder(last_sign_in_at: :asc) }
+ scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'DESC')) }
+ scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('last_sign_in_at', 'ASC')) }
def self.with_two_factor
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id").
diff --git a/app/views/discussions/_discussion.html.haml b/app/views/discussions/_discussion.html.haml
index 2bce2780484..6f5d4bf2a2f 100644
--- a/app/views/discussions/_discussion.html.haml
+++ b/app/views/discussions/_discussion.html.haml
@@ -1,6 +1,9 @@
- expanded = discussion.expanded?
%li.note.note-discussion.timeline-entry
.timeline-entry-inner
+ .timeline-icon
+ = link_to user_path(discussion.author) do
+ = image_tag avatar_icon(discussion.author), class: "avatar s40"
.timeline-content
.discussion.js-toggle-container{ class: discussion.id, data: { discussion_id: discussion.id } }
.discussion-header
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 990bfbcf951..dfdaeb04869 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -78,7 +78,7 @@
.btn-group.inline
- if actions.any?
.btn-group
- %button.dropdown-toggle.btn.btn-default.js-pipeline-dropdown-manual-actions{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.has-tooltip.js-pipeline-dropdown-manual-actions{ type: 'button', title: 'Manual build', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Manual build' }
= custom_icon('icon_play')
= icon('caret-down', 'aria-hidden' => 'true')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -89,7 +89,7 @@
%span= build.name
- if artifacts.present?
.btn-group
- %button.dropdown-toggle.btn.btn-default.build-artifacts.js-pipeline-dropdown-download{ type: 'button', 'data-toggle' => 'dropdown' }
+ %button.dropdown-toggle.btn.btn-default.build-artifacts.has-tooltip.js-pipeline-dropdown-download{ type: 'button', title: 'Artifacts', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Artifacts' }
= icon("download")
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
@@ -102,8 +102,8 @@
- if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: 'Retry', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Retry' , method: :post do
= icon("repeat")
- if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: 'Cancel', data: { toggle: 'dropdown', placement: 'top' }, 'aria-label': 'Cancel' , method: :post do
= icon("remove")
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index e77f23c7fd8..d94f23f5a38 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -9,10 +9,13 @@
= render "head"
%div{ class: container_class }
- .row-content-block.second-block.content-component-block
+ .row-content-block.second-block.content-component-block.flex-container-block
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
.block-controls.hidden-xs.hidden-sm
- if @merge_request.present?
.control
@@ -30,8 +33,6 @@
.control
= link_to namespace_project_commits_path(@project.namespace, @project, @ref, { format: :atom, private_token: current_user.private_token }), title: "Commits Feed", class: 'btn' do
= icon("rss")
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
%div{ id: dom_id(@project) }
%ol#commits-list.list-unstyled.content_list
diff --git a/changelogs/unreleased/26445-accessible-piplelines-buttons.yml b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
new file mode 100644
index 00000000000..fb5274e5253
--- /dev/null
+++ b/changelogs/unreleased/26445-accessible-piplelines-buttons.yml
@@ -0,0 +1,4 @@
+---
+title: Improve button accessibility on pipelines page
+merge_request: 8561
+author:
diff --git a/changelogs/unreleased/26447-fix-tab-list-order.yml b/changelogs/unreleased/26447-fix-tab-list-order.yml
new file mode 100644
index 00000000000..351c53bd076
--- /dev/null
+++ b/changelogs/unreleased/26447-fix-tab-list-order.yml
@@ -0,0 +1,4 @@
+---
+title: Fix tab index order on branch commits list page
+merge_request:
+author: Ryan Harris
diff --git a/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
new file mode 100644
index 00000000000..87ae8233c4a
--- /dev/null
+++ b/changelogs/unreleased/26468-fix-users-sort-in-admin-area.yml
@@ -0,0 +1,4 @@
+---
+title: Fix Sort by Recent Sign-in in Admin Area
+merge_request: 8637
+author: Poornima M
diff --git a/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
new file mode 100644
index 00000000000..77750b55e7e
--- /dev/null
+++ b/changelogs/unreleased/broken_iamge_when_doing_offline_update_check-help_page-.yml
@@ -0,0 +1,4 @@
+---
+title: Hide version check image if there is no internet connection
+merge_request: 8355
+author: Ken Ding
diff --git a/changelogs/unreleased/fix_broken_diff_discussions.yml b/changelogs/unreleased/fix_broken_diff_discussions.yml
new file mode 100644
index 00000000000..4551212759f
--- /dev/null
+++ b/changelogs/unreleased/fix_broken_diff_discussions.yml
@@ -0,0 +1,4 @@
+---
+title: Make MR-review-discussions more reliable
+merge_request:
+author:
diff --git a/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
new file mode 100644
index 00000000000..f32b3aea3c8
--- /dev/null
+++ b/changelogs/unreleased/misalinged-discussion-with-no-avatar-26854.yml
@@ -0,0 +1,4 @@
+---
+title: adds avatar for discussion note
+merge_request: 8734
+author:
diff --git a/changelogs/unreleased/newline-eslint-rule.yml b/changelogs/unreleased/newline-eslint-rule.yml
new file mode 100644
index 00000000000..5ce080b6912
--- /dev/null
+++ b/changelogs/unreleased/newline-eslint-rule.yml
@@ -0,0 +1,4 @@
+---
+title: Flag multiple empty lines in eslint, fix offenses.
+merge_request: 8137
+author:
diff --git a/config/initializers/metrics.rb b/config/initializers/metrics.rb
index 3b8771543e4..e0702e06cc9 100644
--- a/config/initializers/metrics.rb
+++ b/config/initializers/metrics.rb
@@ -1,3 +1,117 @@
+# Autoload all classes that we want to instrument, and instrument the methods we
+# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
+# that we can stub it for testing, as it is only called when metrics are
+# enabled.
+#
+# rubocop:disable Metrics/AbcSize
+def instrument_classes(instrumentation)
+ instrumentation.instrument_instance_methods(Gitlab::Shell)
+
+ instrumentation.instrument_methods(Gitlab::Git)
+
+ Gitlab::Git.constants.each do |name|
+ const = Gitlab::Git.const_get(name)
+
+ next unless const.is_a?(Module)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Path to search => prefix to strip from constant
+ paths_to_instrument = {
+ ['app', 'finders'] => ['app', 'finders'],
+ ['app', 'mailers', 'emails'] => ['app', 'mailers'],
+ ['app', 'services', '**'] => ['app', 'services'],
+ ['lib', 'gitlab', 'conflicts'] => ['lib'],
+ ['lib', 'gitlab', 'diff'] => ['lib'],
+ ['lib', 'gitlab', 'email', 'message'] => ['lib'],
+ ['lib', 'gitlab', 'checks'] => ['lib']
+ }
+
+ paths_to_instrument.each do |(path, prefix)|
+ prefix = Rails.root.join(*prefix)
+
+ Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
+ path = Pathname.new(file_path).relative_path_from(prefix)
+ const = path.to_s.sub('.rb', '').camelize.constantize
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
+ instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
+
+ [
+ :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
+ :Tag, :TagCollection, :Tree
+ ].each do |name|
+ const = Rugged.const_get(name)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+
+ # Instruments all Banzai filters and reference parsers
+ {
+ Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
+ ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
+ }.each do |const_name, path|
+ Dir[path].each do |file|
+ klass = File.basename(file, File.extname(file)).camelize
+ const = Banzai.const_get(const_name).const_get(klass)
+
+ instrumentation.instrument_methods(const)
+ instrumentation.instrument_instance_methods(const)
+ end
+ end
+
+ instrumentation.instrument_methods(Banzai::Renderer)
+ instrumentation.instrument_methods(Banzai::Querying)
+
+ instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
+ instrumentation.instrument_instance_methods(Banzai::Redactor)
+ instrumentation.instrument_methods(Banzai::NoteRenderer)
+
+ [Issuable, Mentionable, Participable].each do |klass|
+ instrumentation.instrument_instance_methods(klass)
+ instrumentation.instrument_instance_methods(klass::ClassMethods)
+ end
+
+ instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
+ instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
+
+ # Instrument the classes used for checking if somebody has push access.
+ instrumentation.instrument_instance_methods(Gitlab::GitAccess)
+ instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
+
+ instrumentation.instrument_instance_methods(API::Helpers)
+
+ instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
+
+ instrumentation.instrument_instance_methods(Rouge::Plugins::Redcarpet)
+ instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
+
+ [:XML, :HTML].each do |namespace|
+ namespace_mod = Nokogiri.const_get(namespace)
+
+ instrumentation.instrument_methods(namespace_mod)
+ instrumentation.instrument_methods(namespace_mod::Document)
+ end
+
+ instrumentation.instrument_methods(Rinku)
+ instrumentation.instrument_instance_methods(Repository)
+
+ instrumentation.instrument_methods(Gitlab::Highlight)
+ instrumentation.instrument_instance_methods(Gitlab::Highlight)
+
+ # This is a Rails scope so we have to instrument it manually.
+ instrumentation.instrument_method(Project, :visible_to_user)
+end
+# rubocop:enable Metrics/AbcSize
+
if Gitlab::Metrics.enabled?
require 'pathname'
require 'influxdb'
@@ -49,110 +163,7 @@ if Gitlab::Metrics.enabled?
end
Gitlab::Metrics::Instrumentation.configure do |config|
- config.instrument_instance_methods(Gitlab::Shell)
-
- config.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name)
-
- next unless const.is_a?(Module)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- ['app', 'finders'] => ['app', 'finders'],
- ['app', 'mailers', 'emails'] => ['app', 'mailers'],
- ['app', 'services', '**'] => ['app', 'services'],
- ['lib', 'gitlab', 'conflicts'] => ['lib'],
- ['lib', 'gitlab', 'diff'] => ['lib'],
- ['lib', 'gitlab', 'email', 'message'] => ['lib'],
- ['lib', 'gitlab', 'checks'] => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Premailer::Adapter::Nokogiri)
- config.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- [
- :Blame, :Branch, :BranchCollection, :Blob, :Commit, :Diff, :Repository,
- :Tag, :TagCollection, :Tree
- ].each do |name|
- const = Rugged.const_get(name)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
-
- # Instruments all Banzai filters and reference parsers
- {
- Filter: Rails.root.join('lib', 'banzai', 'filter', '*.rb'),
- ReferenceParser: Rails.root.join('lib', 'banzai', 'reference_parser', '*.rb')
- }.each do |const_name, path|
- Dir[path].each do |file|
- klass = File.basename(file, File.extname(file)).camelize
- const = Banzai.const_get(const_name).const_get(klass)
-
- config.instrument_methods(const)
- config.instrument_instance_methods(const)
- end
- end
-
- config.instrument_methods(Banzai::Renderer)
- config.instrument_methods(Banzai::Querying)
-
- config.instrument_instance_methods(Banzai::ObjectRenderer)
- config.instrument_instance_methods(Banzai::Redactor)
- config.instrument_methods(Banzai::NoteRenderer)
-
- [Issuable, Mentionable, Participable].each do |klass|
- config.instrument_instance_methods(klass)
- config.instrument_instance_methods(klass::ClassMethods)
- end
-
- config.instrument_methods(Gitlab::ReferenceExtractor)
- config.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- config.instrument_instance_methods(Gitlab::GitAccess)
- config.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- config.instrument_instance_methods(API::Helpers)
-
- config.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- config.instrument_instance_methods(Rouge::Plugins::Redcarpet)
- config.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
-
- [:XML, :HTML].each do |namespace|
- namespace_mod = Nokogiri.const_get(namespace)
-
- config.instrument_methods(namespace_mod)
- config.instrument_methods(namespace_mod::Document)
- end
-
- config.instrument_methods(Rinku)
- config.instrument_instance_methods(Repository)
-
- config.instrument_methods(Gitlab::Highlight)
- config.instrument_instance_methods(Gitlab::Highlight)
-
- # This is a Rails scope so we have to instrument it manually.
- config.instrument_method(Project, :visible_to_user)
+ instrument_classes(config)
end
GC::Profiler.enable
diff --git a/doc/administration/raketasks/maintenance.md b/doc/administration/raketasks/maintenance.md
index f3c2e72341f..33b9b28433a 100644
--- a/doc/administration/raketasks/maintenance.md
+++ b/doc/administration/raketasks/maintenance.md
@@ -27,6 +27,7 @@ Ruby Version: 2.1.5p273
Gem Version: 2.4.3
Bundler Version: 1.7.6
Rake Version: 10.3.2
+Redis Version: 3.2.5
Sidekiq Version: 2.17.8
GitLab information
diff --git a/lib/tasks/gitlab/info.rake b/lib/tasks/gitlab/info.rake
index dffea8ed155..f7c831892ee 100644
--- a/lib/tasks/gitlab/info.rake
+++ b/lib/tasks/gitlab/info.rake
@@ -11,8 +11,10 @@ namespace :gitlab do
gem_version = run_command(%W(gem --version))
# check Bundler version
bunder_version = run_and_match(%W(bundle --version), /[\d\.]+/).try(:to_s)
- # check Bundler version
+ # check Rake version
rake_version = run_and_match(%W(rake --version), /[\d\.]+/).try(:to_s)
+ # check redis version
+ redis_version = run_and_match(%W(redis-cli --version), /redis-cli (\d+\.\d+\.\d+)/).to_a
puts ""
puts "System information".color(:yellow)
@@ -24,6 +26,7 @@ namespace :gitlab do
puts "Gem Version:\t#{gem_version || "unknown".color(:red)}"
puts "Bundler Version:#{bunder_version || "unknown".color(:red)}"
puts "Rake Version:\t#{rake_version || "unknown".color(:red)}"
+ puts "Redis Version:\t#{redis_version[1] || "unknown".color(:red)}"
puts "Sidekiq Version:#{Sidekiq::VERSION}"
diff --git a/spec/features/issues/filtered_search/dropdown_label_spec.rb b/spec/features/issues/filtered_search/dropdown_label_spec.rb
index bea00160f96..71e0608a664 100644
--- a/spec/features/issues/filtered_search/dropdown_label_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_label_spec.rb
@@ -40,6 +40,16 @@ describe 'Dropdown label', js: true, feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
+ describe 'keyboard navigation' do
+ it 'selects label' do
+ send_keys_to_filtered_search('label:')
+
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq("label:~#{special_label.name}")
+ end
+ end
+
describe 'behavior' do
it 'opens when the search bar has label:' do
filtered_search.set('label:')
diff --git a/spec/features/issues/filtered_search/search_bar_spec.rb b/spec/features/issues/filtered_search/search_bar_spec.rb
index 56b1d354eb0..90eb60eb337 100644
--- a/spec/features/issues/filtered_search/search_bar_spec.rb
+++ b/spec/features/issues/filtered_search/search_bar_spec.rb
@@ -20,6 +20,22 @@ describe 'Search bar', js: true, feature: true do
left_style.to_s.gsub('left: ', '').to_f
end
+ describe 'keyboard navigation' do
+ it 'makes item active' do
+ filtered_search.native.send_keys(:down)
+
+ page.within '#js-dropdown-hint' do
+ expect(page).to have_selector('.dropdown-active')
+ end
+ end
+
+ it 'selects item' do
+ filtered_search.native.send_keys(:down, :down, :enter)
+
+ expect(filtered_search.value).to eq('author:')
+ end
+ end
+
describe 'clear search button' do
it 'clears text' do
search_text = 'search_text'
diff --git a/spec/initializers/metrics_spec.rb b/spec/initializers/metrics_spec.rb
new file mode 100644
index 00000000000..bb595162370
--- /dev/null
+++ b/spec/initializers/metrics_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+require_relative '../../config/initializers/metrics'
+
+describe 'instrument_classes', lib: true do
+ let(:config) { double(:config) }
+
+ before do
+ allow(config).to receive(:instrument_method)
+ allow(config).to receive(:instrument_methods)
+ allow(config).to receive(:instrument_instance_methods)
+ end
+
+ it 'can autoload and instrument all files' do
+ expect { instrument_classes(config) }.not_to raise_error
+ end
+end
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index cf19aa05031..a2d57824585 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -21,7 +21,6 @@
messages = $('.abuse-reports .message');
});
-
it('should truncate long messages', () => {
const $longMessage = findMessage('LONG MESSAGE');
expect($longMessage.data('original-message')).toEqual(jasmine.anything());
diff --git a/spec/javascripts/environments/environment_rollback_spec.js.es6 b/spec/javascripts/environments/environment_rollback_spec.js.es6
index 21241116e29..95796f23894 100644
--- a/spec/javascripts/environments/environment_rollback_spec.js.es6
+++ b/spec/javascripts/environments/environment_rollback_spec.js.es6
@@ -33,7 +33,6 @@ describe('Rollback Component', () => {
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
});
-
it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({
el: document.querySelector('.test-dom-element'),
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6
index 031f9ca03c9..1ce8f28e568 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js.es6
+++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6
@@ -52,5 +52,22 @@
expect(value).toBe(null);
});
});
+
+ describe('gl.utils.normalizedHeaders', () => {
+ it('should upperCase all the header keys to keep them consistent', () => {
+ const apiHeaders = {
+ 'X-Something-Workhorse': { workhorse: 'ok' },
+ 'x-something-nginx': { nginx: 'ok' },
+ };
+
+ const normalized = gl.utils.normalizeHeaders(apiHeaders);
+
+ const WORKHORSE = 'X-SOMETHING-WORKHORSE';
+ const NGINX = 'X-SOMETHING-NGINX';
+
+ expect(normalized[WORKHORSE].workhorse).toBe('ok');
+ expect(normalized[NGINX].nginx).toBe('ok');
+ });
+ });
});
})();
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index ca3d4ff0aa9..c45d0b9317b 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -797,14 +797,14 @@ describe User, models: true do
describe '#avatar_type' do
let(:user) { create(:user) }
- it "is true if avatar is image" do
+ it 'is true if avatar is image' do
user.update_attribute(:avatar, 'uploads/avatar.png')
expect(user.avatar_type).to be_truthy
end
- it "is false if avatar is html page" do
+ it 'is false if avatar is html page' do
user.update_attribute(:avatar, 'uploads/avatar.html')
- expect(user.avatar_type).to eq(["only images allowed"])
+ expect(user.avatar_type).to eq(['only images allowed'])
end
end
@@ -926,8 +926,8 @@ describe User, models: true do
end
end
- describe "#starred?" do
- it "determines if user starred a project" do
+ describe '#starred?' do
+ it 'determines if user starred a project' do
user = create :user
project1 = create(:empty_project, :public)
project2 = create(:empty_project, :public)
@@ -953,8 +953,8 @@ describe User, models: true do
end
end
- describe "#toggle_star" do
- it "toggles stars" do
+ describe '#toggle_star' do
+ it 'toggles stars' do
user = create :user
project = create(:empty_project, :public)
@@ -966,31 +966,44 @@ describe User, models: true do
end
end
- describe "#sort" do
+ describe '#sort' do
before do
User.delete_all
@user = create :user, created_at: Date.today, last_sign_in_at: Date.today, name: 'Alpha'
@user1 = create :user, created_at: Date.today - 1, last_sign_in_at: Date.today - 1, name: 'Omega'
+ @user2 = create :user, created_at: Date.today - 2, last_sign_in_at: nil, name: 'Beta'
end
- it "sorts users by the recent sign-in time" do
- expect(User.sort('recent_sign_in').first).to eq(@user)
+ context 'when sort by recent_sign_in' do
+ it 'sorts users by the recent sign-in time' do
+ expect(User.sort('recent_sign_in').first).to eq(@user)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('recent_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users by the oldest sign-in time" do
- expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ context 'when sort by oldest_sign_in' do
+ it 'sorts users by the oldest sign-in time' do
+ expect(User.sort('oldest_sign_in').first).to eq(@user1)
+ end
+
+ it 'pushes users who never signed in to the end' do
+ expect(User.sort('oldest_sign_in').third).to eq(@user2)
+ end
end
- it "sorts users in descending order by their creation time" do
+ it 'sorts users in descending order by their creation time' do
expect(User.sort('created_desc').first).to eq(@user)
end
- it "sorts users in ascending order by their creation time" do
- expect(User.sort('created_asc').first).to eq(@user1)
+ it 'sorts users in ascending order by their creation time' do
+ expect(User.sort('created_asc').first).to eq(@user2)
end
- it "sorts users by id in descending order when nil is passed" do
- expect(User.sort(nil).first).to eq(@user1)
+ it 'sorts users by id in descending order when nil is passed' do
+ expect(User.sort(nil).first).to eq(@user2)
end
end