Commit b83e8a5a authored by Alejandro Rodríguez's avatar Alejandro Rodríguez

Merge branch 'master' into 8-14-stable

parents cb5f3368 6eeff67c
......@@ -213,11 +213,24 @@ rake downtime_check: *exec
rake ee_compat_check:
<<: *exec
only:
- branches
- branches@gitlab-org/gitlab-ce
- branches@gitlab/gitlabhq
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
allow_failure: yes
cache:
key: "ruby231-ee_compat_check_repo"
paths:
- ee_compat_check/repo/
- vendor/ruby
artifacts:
name: "${CI_BUILD_NAME}_${CI_BUILD_REF_NAME}_${CI_BUILD_REF}"
when: on_failure
expire_in: 10d
paths:
- ee_compat_check/patches/*.patch
rake db:migrate:reset:
stage: test
......
......@@ -51,6 +51,7 @@ entry.
- Fail gracefully when creating merge request with non-existing branch (alexsanford)
- Fix mobile layout issues in admin user overview page !7087
- Fix HipChat notifications rendering (airatshigapov, eisnerd)
- Removed unneeded "Builds" and "Environments" link from project titles
- Remove 'Edit' button from wiki edit view !7143 (Hiroyuki Sato)
- Cleaned up global namespace JS !19661 (Jose Ivan Vargas)
- Refactor Jira service to use jira-ruby gem
......
......@@ -137,6 +137,7 @@ gem 'acts-as-taggable-on', '~> 4.0'
gem 'sidekiq', '~> 4.2'
gem 'sidekiq-cron', '~> 0.4.0'
gem 'redis-namespace', '~> 1.5.2'
gem 'sidekiq-limit_fetch', '~> 3.4'
# HTTP requests
gem 'httparty', '~> 0.13.3'
......
......@@ -685,6 +685,8 @@ GEM
redis-namespace (>= 1.5.2)
rufus-scheduler (>= 2.0.24)
sidekiq (>= 4.0.0)
sidekiq-limit_fetch (3.4.0)
sidekiq (>= 4)
simplecov (0.12.0)
docile (~> 1.1.0)
json (>= 1.8, < 3)
......@@ -961,6 +963,7 @@ DEPENDENCIES
shoulda-matchers (~> 2.8.0)
sidekiq (~> 4.2)
sidekiq-cron (~> 0.4.0)
sidekiq-limit_fetch (~> 3.4)
simplecov (= 0.12.0)
slack-notifier (~> 1.2.0)
spinach-rails (~> 0.2.1)
......
......@@ -82,6 +82,10 @@ GitLab is a Ruby on Rails application that runs on the following software:
For more information please see the [architecture documentation](https://docs.gitlab.com/ce/development/architecture.html).
## UX design
Please adhere to the [UX Guide](doc/development/ux_guide/readme.md) when creating designs and implementing code.
## Third-party applications
There are a lot of [third-party applications integrating with GitLab](https://about.gitlab.com/applications/). These include GUI Git clients, mobile applications and API wrappers for various languages.
......
......@@ -58,11 +58,28 @@
document.addEventListener('page:fetch', gl.utils.cleanupBeforeFetch);
window.addEventListener('hashchange', gl.utils.shiftWindow);
// automatically adjust scroll position for hash urls taking the height of the navbar into account
// https://github.com/twitter/bootstrap/issues/1768
window.adjustScroll = function() {
var navbar = document.querySelector('.navbar-gitlab');
var subnav = document.querySelector('.layout-nav');
var fixedTabs = document.querySelector('.js-tabs-affix');
adjustment = 0;
if (navbar) adjustment -= navbar.offsetHeight;
if (subnav) adjustment -= subnav.offsetHeight;
if (fixedTabs) adjustment -= fixedTabs.offsetHeight;
return scrollBy(0, adjustment);
};
window.addEventListener("hashchange", adjustScroll);
window.onload = function () {
// Scroll the window to avoid the topnav bar
// https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
return setTimeout(gl.utils.shiftWindow, 100);
return setTimeout(adjustScroll, 100);
}
};
......
......@@ -22,6 +22,8 @@ $(() => {
gl.IssueBoardsApp.$destroy(true);
}
Store.create();
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
......@@ -37,16 +39,15 @@ $(() => {
issueLinkBase: $boardApp.dataset.issueLinkBase,
detailIssue: Store.detail
},
init: Store.create.bind(Store),
computed: {
detailIssueVisible () {
return Object.keys(this.detailIssue.issue).length;
}
},
},
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
ready () {
mounted () {
Store.disabled = this.disabled;
gl.boardService.all()
.then((resp) => {
......@@ -60,6 +61,8 @@ $(() => {
}
});
this.state.lists = _.sortBy(this.state.lists, 'position');
Store.addBlankState();
this.loading = false;
});
......@@ -70,6 +73,9 @@ $(() => {
el: '#js-boards-seach',
data: {
filters: Store.state.filters
},
mounted () {
gl.issueBoards.newListDropdownInit();
}
});
});
......@@ -10,6 +10,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.Board = Vue.extend({
template: '#js-board-template',
components: {
'board-list': gl.issueBoards.BoardList,
'board-delete': gl.issueBoards.BoardDelete,
......@@ -24,7 +25,6 @@
return {
detailIssue: Store.detail,
filters: Store.state.filters,
showIssueForm: false
};
},
watch: {
......@@ -58,10 +58,10 @@
},
methods: {
showNewIssueForm() {
this.showIssueForm = !this.showIssueForm;
this.$refs['board-list'].showIssueForm = !this.$refs['board-list'].showIssueForm;
}
},
ready () {
mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
group: 'boards',
......@@ -72,13 +72,9 @@
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
$board = this.$parent.$refs.board[e.oldIndex + 1],
list = $board.list;
$board.$destroy(true);
list = Store.findList('id', parseInt(e.item.dataset.id));
this.$nextTick(() => {
Store.state.lists.splice(e.newIndex, 0, list);
Store.moveList(list, order);
});
}
......@@ -87,8 +83,5 @@
this.sortable = Sortable.create(this.$el.parentNode, options);
},
beforeDestroy () {
Store.state.lists.$remove(this.list);
}
});
})();
......@@ -30,6 +30,8 @@
});
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
// Save the labels
gl.boardService.generateDefaultLists()
.then((resp) => {
......
......@@ -6,6 +6,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardCard = Vue.extend({
template: '#js-board-list-card',
props: {
list: Object,
issue: Object,
......@@ -53,11 +54,6 @@
mouseDown () {
this.showDetail = true;
},
mouseMove () {
if (this.showDetail) {
this.showDetail = false;
}
},
showIssue (e) {
const targetTagName = e.target.tagName.toLowerCase();
......
......@@ -9,6 +9,7 @@
window.gl.issueBoards = window.gl.issueBoards || {};
gl.issueBoards.BoardList = Vue.extend({
template: '#js-board-list-template',
components: {
'board-card': gl.issueBoards.BoardCard,
'board-new-issue': gl.issueBoards.BoardNewIssue
......@@ -19,20 +20,20 @@
issues: Array,
loading: Boolean,
issueLinkBase: String,
showIssueForm: Boolean
},
data () {
return {
scrollOffset: 250,
filters: Store.state.filters,
showCount: false
showCount: false,
showIssueForm: false
};
},
watch: {
filters: {
handler () {
this.list.loadingMore = false;
this.$els.list.scrollTop = 0;
this.$refs.list.scrollTop = 0;
},
deep: true
},
......@@ -51,15 +52,20 @@
});
}
},
computed: {
orderedIssues () {
return _.sortBy(this.issues, 'priority');
},
},
methods: {
listHeight () {
return this.$els.list.getBoundingClientRect().height;
return this.$refs.list.getBoundingClientRect().height;
},
scrollHeight () {
return this.$els.list.scrollHeight;
return this.$refs.list.scrollHeight;
},
scrollTop () {
return this.$els.list.scrollTop + this.listHeight();
return this.$refs.list.scrollTop + this.listHeight();
},
loadNextPage () {
const getIssues = this.list.nextPage();
......@@ -72,7 +78,7 @@
}
},
},
ready () {
mounted () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
group: 'issues',
sort: false,
......@@ -81,23 +87,27 @@
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
card.showDetail = false;
Store.moving.issue = card.issue;
Store.moving.list = card.list;
gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
// Add the element back to original list to allow Vue to handle DOM updates
e.from.appendChild(e.item);
this.$nextTick(() => {
// Update the issues once we know the element has been moved
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
});
},
onRemove: (e) => {
this.$refs.issue[e.oldIndex].$destroy(true);
}
});
this.sortable = Sortable.create(this.$els.list, options);
this.sortable = Sortable.create(this.$refs.list, options);
// Scroll event on list to load more
this.$els.list.onscroll = () => {
this.$refs.list.onscroll = () => {
if ((this.scrollTop() > this.scrollHeight() - this.scrollOffset) && !this.list.loadingMore) {
this.loadNextPage();
}
......
......@@ -7,7 +7,6 @@
gl.issueBoards.BoardNewIssue = Vue.extend({
props: {
list: Object,
showIssueForm: Boolean
},
data() {
return {
......@@ -15,11 +14,6 @@
error: false
};
},
watch: {
showIssueForm () {
this.$els.input.focus();
}
},
methods: {
submit(e) {
e.preventDefault();
......@@ -37,28 +31,30 @@
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
$(this.$refs.submitButton).enable();
Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
$(this.$refs.submitButton).enable();
// Remove the issue
this.list.removeIssue(issue);
// Show error message
this.error = true;
this.showIssueForm = true;
});
this.cancel();
},
cancel() {
this.showIssueForm = false;
this.title = '';
this.$parent.showIssueForm = false;
}
}
},
mounted() {
this.$refs.input.focus();
},
});
})();
......@@ -41,7 +41,7 @@
this.detail.issue = {};
}
},
ready () {
mounted () {
new IssuableContext(this.currentUser);
new MilestoneSelect();
new gl.DueDateSelectors();
......
/* eslint-disable */
$(() => {
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
const Store = gl.issueBoards.BoardsStore;
$(document).off('created.label').on('created.label', (e, label) => {
......@@ -15,54 +18,58 @@ $(() => {
});
});
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
gl.issueBoards.newListDropdownInit = () => {
$('.js-new-board-list').each(function () {
const $this = $(this);
new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-labels'))
.then((resp) => {
callback(resp);
});
},
renderRow (label) {
const active = Store.findList('title', label.title),
$li = $('<li />'),
$a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
href: '#'
}),
$labelColor = $('<span />', {
class: 'dropdown-label-box',
style: `background-color: ${label.color}`
});
$this.glDropdown({
data(term, callback) {
$.get($this.attr('data-labels'))
.then((resp) => {
callback(resp);
});
},
renderRow (label) {
const active = Store.findList('title', label.title),
$li = $('<li />'),
$a = $('<a />', {
class: (active ? `is-active js-board-list-${active.id}` : ''),
text: label.title,
href: '#'
}),
$labelColor = $('<span />', {
class: 'dropdown-label-box',
style: `background-color: ${label.color}`
});
return $li.append($a.prepend($labelColor));
},
search: {
fields: ['title']
},
filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
e.preventDefault();
return $li.append($a.prepend($labelColor));
},
search: {
fields: ['title']
},
filterable: true,
selectable: true,
multiSelect: true,
clicked (label, $el, e) {
e.preventDefault();
if (!Store.findList('title', label.title)) {
Store.new({
title: label.title,
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
if (!Store.findList('title', label.title)) {
Store.new({
title: label.title,
color: label.color
}
});
position: Store.state.lists.length - 2,
list_type: 'label',
label: {
id: label.id,
title: label.title,
color: label.color
}
});
Store.state.lists = _.sortBy(Store.state.lists, 'position');
}
}
}
});
});
});
});
};
})();
......@@ -23,7 +23,7 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.board-delete, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 50,
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
......
......@@ -42,7 +42,8 @@ class List {
}
destroy () {
gl.issueBoards.BoardsStore.state.lists.$remove(this);
const index = gl.issueBoards.BoardsStore.state.lists.indexOf(this);
gl.issueBoards.BoardsStore.state.lists.splice(index, 1);
gl.issueBoards.BoardsStore.updateNewListDropdown(this.id);
gl.boardService.destroyList(this.id);
......
......@@ -39,6 +39,8 @@
// Remove any new issues from the backlog
// as they will be visible in the new list
list.issues.forEach(backlogList.removeIssue.bind(backlogList));
this.state.lists = _.sortBy(this.state.lists, 'position');
});
this.removeBlankState();
},
......@@ -58,6 +60,8 @@
title: 'Welcome to your Issue Board!',
position: 0
});
this.state.lists = _.sortBy(this.state.lists, 'position');
},
removeBlankState () {
this.removeList('blank');
......
/* eslint-disable */
((w) => {
w.CommentAndResolveBtn = Vue.extend({
(() => {
const CommentAndResolveBtn = Vue.extend({
props: {
discussionId: String,
textareaIsEmpty: Boolean
},
data() {
return {
textareaIsEmpty: true
}
},
computed: {
discussion: function () {
......@@ -35,7 +39,7 @@
}
}
},
ready: function () {
mounted: function () {
const $textarea = $(`#new-discussion-note-form-${this.discussionId} .note-textarea`);
this.textareaIsEmpty = $textarea.val() === '';
......@@ -47,4 +51,6 @@
$(`#new-discussion-note-form-${this.discussionId} .note-textarea`).off('input.comment-and-resolve-btn');
}
});
Vue.component('comment-and-resolve-btn', CommentAndResolveBtn);
})(window);
/* eslint-disable */
((w) => {
w.ResolveBtn = Vue.extend({
(() => {
const ResolveBtn = Vue.extend({
props: {
noteId: Number,
discussionId: String,
......@@ -54,7 +54,7 @@
},
methods: {
updateTooltip: function () {
$(this.$els.button)
$(this.$refs.button)
.tooltip('hide')
.tooltip('fixTitle');
},
......@@ -89,8 +89,8 @@
});
}
},
compiled: function () {
$(this.$els.button).tooltip({
mounted: function () {
$(this.$refs.button).tooltip({
container: 'body'
});
},
......@@ -101,4 +101,6 @@
CommentsStore.create(this.discussionId, this.noteId, this.canResolve, this.resolved, this.resolvedBy);
}
});
})(window);
Vue.component('resolve-btn', ResolveBtn);
})();
......@@ -13,6 +13,9 @@
computed: {
allResolved: function () {
return this.resolvedDiscussionCount === this.discussionCount;
},
resolvedCountText() {
return this.discussionCount === 1 ? 'discussion' : 'discussions';
}
}
});
......
/* eslint-disable */
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
(() => {
const ResolveDiscussionBtn = Vue.extend({
props: {
discussionId: String,
mergeRequestId: Number,
......@@ -54,4 +54,6 @@
CommentsStore.createDiscussion(this.discussionId, this.canResolve);
}
});
})(window);
Vue.component('resolve-discussion-btn', ResolveDiscussionBtn);
})();
......@@ -8,24 +8,35 @@
//= require_directory ./components
$(() => {
window.DiffNotesApp = new Vue({
el: '#diff-notes-app',
components: {
'resolve-btn': ResolveBtn,
'resolve-discussion-btn': ResolveDiscussionBtn,
'comment-and-resolve-btn': CommentAndResolveBtn
},
methods: {
compileComponents: function () {
const $components = $('resolve-btn, resolve-discussion-btn, jump-to-discussion');
if ($components.length) {
$components.each(function () {
DiffNotesApp.$compile($(this).get(0));
});
const COMPONENT_SELECTOR = 'resolve-btn, resolve-discussion-btn, jump-to-discussion, comment-and-resolve-btn';
window.gl = window.gl || {};
window.gl.diffNoteApps = {};
gl.diffNotesCompileComponents = () => {
const $components = $(COMPONENT_SELECTOR).filter(function () {
return $(this).closest('resolve-count').length !== 1;
});
if ($components) {
$components.each(function () {
const $this = $(this);
const noteId = $this.attr(':note-id');
const tmp = Vue.extend({
template: $this.get(0).outerHTML
});
const tmpApp = new tmp().$mount();
if (noteId) {
gl.diffNoteApps[`note_${noteId}`] = tmpApp;
}
}
$this.replaceWith(tmpApp.$el);
});
}
});
};
gl.diffNotesCompileComponents();
new Vue({
el: '#resolve-count-app',
......
......@@ -36,7 +36,7 @@
this.loadEditor();
}
},
ready() {
mounted() {
if (this.file.loadEditor) {
this.loadEditor();
}
......
/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
global.mergeConflicts.parallelConflictLine = Vue.extend({
props: {
file: Object,
line: Object
},
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: '#parallel-conflict-line'
});
})(window.gl || (window.gl = {}));
......@@ -7,10 +7,22 @@
props: {
file: Object
},
mixins: [global.mergeConflicts.utils],
components: {
'parallel-conflict-line': gl.mergeConflicts.parallelConflictLine
}
mixins: [global.mergeConflicts.utils, global.mergeConflicts.actions],
template: `
<table>
<tr class="line_holder parallel" v-for="section in file.parallelLines">
<template v-for="line in section">
<td class="diff-line-num header" :class="lineCssClass(line)" v-if="line.isHeader"></td>
<td class="line_content header" :class="lineCssClass(line)" v-if="line.isHeader">
<strong>{{line.richText}}</strong>
<button class="btn" @click="handleSelected(file, line.id, line.section)">{{line.buttonTitle}}</button>
</td>
<td class="diff-line-num old_line" :class="lineCssClass(line)" v-if="!line.isHeader">{{line.lineNumber}}</td>
<td class="line_content parallel" :class="lineCssClass(line)" v-if="!line.isHeader" v-html="line.richText"></td>
</template>
</tr>
</table>
`,
});
})(window.gl || (window.gl = {}));
......@@ -6,7 +6,6 @@
//= require ./mixins/line_conflict_actions
//= require ./components/diff_file_editor
//= require ./components/inline_conflict_lines
//= require ./components/parallel_conflict_line
//= require ./components/parallel_conflict_lines
$(() => {
......@@ -49,7 +48,7 @@ $(() => {
mergeConflictsStore.setLoadingState(false);
this.$nextTick(() => {
$(conflictsEl.querySelectorAll('.js-syntax-highlight')).syntaxHighlight();
$('.js-syntax-highlight').syntaxHighlight();
});
});
},
......
......@@ -130,7 +130,7 @@
MergeRequestTabs.prototype.scrollToElement = function(container) {
var $el, navBarHeight;
if (window.location.hash) {
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
navBarHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight() + document.querySelector('.js-tabs-affix').offsetHeight;
$el = $(container + " " + window.location.hash + ":not(.match)");
if ($el.length) {
return $.scrollTo(container + " " + window.location.hash + ":not(.match)", {
......@@ -227,8 +227,8 @@
return function(data) {
$('#diffs').html(data.html);
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
......
......@@ -325,8 +325,8 @@
discussionContainer.append(note_html);
}
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
gl.utils.localTimeAgo($('.js-timeago', note_html), false);
......@@ -466,8 +466,8 @@
$note_li.replaceWith($html);
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
};
......@@ -559,11 +559,9 @@
note = $(el);
notes = note.closest(".notes");
if (typeof DiffNotesApp !== "undefined" && DiffNotesApp !== null) {
ref = DiffNotesApp.$refs[noteId];
if (ref) {
ref.$destroy(true);
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
if (gl.diffNoteApps[noteId]) {
gl.diffNoteApps[noteId].$destroy();
}
}
......@@ -643,11 +641,12 @@
form.find('.js-note-target-close').remove();
this.setupNoteForm(form);
if (typeof DiffNotesApp !== 'undefined') {
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
var $commentBtn = form.find('comment-and-resolve-btn');
$commentBtn
.attr(':discussion-id', "'" + dataHolder.data('discussionId') + "'");
DiffNotesApp.$compile($commentBtn.get(0));
gl.diffNotesCompileComponents();
}
form.find(".js-note-text").focus();
......
......@@ -45,15 +45,15 @@
this.content.hide();
this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
......@@ -76,8 +76,8 @@
}
_this.collapsedContent.after(_this.content);
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
if (typeof gl.diffNotesCompileComponents !== 'undefined') {
gl.diffNotesCompileComponents();
}
if (cb) cb();
......
......@@ -71,7 +71,7 @@
display: none;
}
.group-right-buttons {
.group-buttons {
display: none;
}
......
......@@ -166,8 +166,12 @@
}
}
.board-list {
.board-list-component {
height: calc(100% - 49px);
}
.board-list {
height: 100%;
margin-bottom: 0;
padding: 5px;
list-style: none;
......@@ -175,7 +179,7 @@
overflow-x: hidden;
&.is-smaller {
height: calc(100% - 185px);
height: calc(100% - 136px);
}
}
......
......@@ -62,6 +62,8 @@
.ci-status-link {
display: inline-block;
position: relative;
top: 1px;
}
.btn-clipboard,
......
......@@ -10,7 +10,6 @@
}
.group-row {
.stats {
float: right;
line-height: $list-text-height;
......@@ -23,36 +22,18 @@
}
.ldap-group-links {
.form-actions {
margin-bottom: $gl-padding;
}
}
.groups-cover-block {
.container-fluid {
position: relative;
}
.group-right-buttons {
position: absolute;
right: 16px;
.btn {
@include btn-gray;
padding: 3px 10px;
background-color: $background-color;
}
}
.group-avatar {
border: 0;
.group-buttons {
.notification-dropdown {
display: inline-block;
}
}
.groups-header {
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
......
......@@ -267,20 +267,6 @@
}
}
.issuable-header-btn {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
background: $gray-dark;
border: 1px solid $border-gray-dark;
}
&.btn-primary {
@extend .btn-primary;
}
}
a {
&:hover {
color: $md-link-color;
......
......@@ -47,6 +47,7 @@
&.right {
float: right;
padding-right: 0;
a {
color: $gl-gray;
......
......@@ -86,7 +86,8 @@
}
}
.project-home-panel {
.project-home-panel,
.group-home-panel {
padding-top: 24px;
padding-bottom: 24px;
......@@ -94,7 +95,8 @@
border-bottom: 1px solid $border-color;
}
.project-avatar {
.project-avatar,
.group-avatar {
float: none;
margin: 0 auto;
border: none;
......@@ -104,7 +106,8 @@
}
}
.project-title {
.project-title,
.group-title {
margin-top: 10px;
margin-bottom: 10px;
font-size: 24px;
......@@ -118,10 +121,11 @@
}
}
.project-home-desc {
.project-home-desc,
.group-home-desc {
margin-left: auto;
margin-right: auto;
margin-bottom: 15px;
margin-bottom: 0;
max-width: 700px;
> p {
......@@ -141,13 +145,18 @@
}
}
.project-repo-buttons {
font-size: 0;
.project-repo-buttons,
.group-buttons {
margin-top: 15px;
.btn {
@include btn-gray;
padding: 3px 10px;
&:last-child {
margin-left: 0;
}
.fa {
color: $layout-link-gray;
}
......@@ -168,7 +177,8 @@
}
}
.project-repo-btn-group,
.download-button,
.dropdown-toggle,
.notification-dropdown,
.project-dropdown {
margin-left: 10px;
......@@ -474,9 +484,7 @@ a.deploy-project-label {
margin-right: $gl-padding;
}
&.project-repo-buttons-right {
margin-top: 10px;
&.right {
@media (min-width: $screen-md-min) {
float: right;
margin-top: 0;
......
......@@ -117,6 +117,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:send_user_confirmation_email,
:container_registry_token_expire_delay,
:enabled_git_access_protocol,
:sidekiq_throttling_enabled,
:sidekiq_throttling_factor,
:housekeeping_enabled,
:housekeeping_bitmaps_enabled,
:housekeeping_incremental_repack_period,
......@@ -125,7 +127,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
repository_storages: [],
restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: []
disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: []
)
end
end
......@@ -3,7 +3,7 @@ module DiffForPath
def render_diff_for_path(diffs)
diff_file = diffs.diff_files.find do |diff|
diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
diff.file_identifier == params[:file_identifier]
end
return render_404 unless diff_file
......
......@@ -58,7 +58,7 @@ class Groups::MilestonesController < Groups::ApplicationController
def render_new_with_error(empty_project_ids)
@milestone = Milestone.new(milestone_params)
@milestone.errors.add(:project_id, "Please select at least one project.") if empty_project_ids
@milestone.errors.add(:base, "Please select at least one project.") if empty_project_ids
render :new
end
......
......@@ -507,6 +507,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.close
end
labels
define_pipelines_vars
end
......
......@@ -18,7 +18,9 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def create
@pipeline = Ci::CreatePipelineService.new(project, current_user, create_params).execute(ignore_skip_ci: true, save_on_errors: false)
@pipeline = Ci::CreatePipelineService
.new(project, current_user, create_params)
.execute(ignore_skip_ci: true, save_on_errors: false)
unless @pipeline.persisted?
render 'new'
return
......
......@@ -100,4 +100,8 @@ module ApplicationSettingsHelper
options_for_select(options, @application_setting.repository_storages)
end
def sidekiq_queue_options_for_select
options_for_select(Sidekiq::Queue.all.map(&:name), @application_setting.sidekiq_throttling_queues)
end
end
......@@ -49,7 +49,7 @@ module ProjectsHelper
end
end
def project_title(project, name = nil, url = nil)
def project_title(project)
namespace_link =
if project.group
link_to(simple_sanitize(project.group.name), group_path(project.group))
......@@ -66,10 +66,7 @@ module ProjectsHelper
end
end
full_title = "#{namespace_link} / #{project_link}".html_safe
full_title << ' &middot; '.html_safe << link_to(simple_sanitize(name), url) if name
full_title
"#{namespace_link} / #{project_link}".html_safe
end
def remove_project_message(project)
......
......@@ -19,6 +19,7 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
serialize :repository_storages
serialize :sidekiq_throttling_queues, Array
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
......@@ -85,6 +86,15 @@ class ApplicationSetting < ActiveRecord::Base
presence: { message: 'Domain blacklist cannot be empty if Blacklist is enabled.' },
if: :domain_blacklist_enabled?
validates :sidekiq_throttling_factor,
numericality: { greater_than: 0, less_than: 1 },
presence: { message: 'Throttling factor cannot be empty if Sidekiq Throttling is enabled.' },
if: :sidekiq_throttling_enabled?
validates :sidekiq_throttling_queues,
presence: { message: 'Queues to throttle cannot be empty if Sidekiq Throttling is enabled.' },
if: :sidekiq_throttling_enabled?
validates :housekeeping_incremental_repack_period,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
......@@ -180,6 +190,7 @@ class ApplicationSetting < ActiveRecord::Base
container_registry_token_expire_delay: 5,
repository_storages: ['default'],
user_default_external: false,
sidekiq_throttling_enabled: false,
housekeeping_enabled: true,
housekeeping_bitmaps_enabled: true,
housekeeping_incremental_repack_period: 10,
......
......@@ -1334,6 +1334,10 @@ class Project < ActiveRecord::Base
end
end
def only_allow_merge_if_all_discussions_are_resolved
super || false
end
private
def pushes_since_gc_redis_key
......
......@@ -84,15 +84,17 @@ class Repository
def commit(ref = 'HEAD')
return nil unless exists?
commit =
if ref.is_a?(Gitlab::Git::Commit)
ref
else
Gitlab::Git::Commit.find(raw_repository, ref)
end
commit = ::Commit.new(commit, @project) if commit
commit
rescue Rugged::OdbError
rescue Rugged::OdbError, Rugged::TreeError
nil
end
......@@ -232,6 +234,8 @@ class Repository
def ref_exists?(ref)
rugged.references.exist?(ref)
rescue Rugged::ReferenceError
false
end
def update_ref!(name, newrev, oldrev)
......@@ -239,7 +243,7 @@ class Repository
# offer 'compare and swap' ref updates. Without compare-and-swap we can
# (and have!) accidentally reset the ref to an earlier state, clobbering
# commits. See also https://github.com/libgit2/libgit2/issues/1534.
command = %w[git update-ref --stdin -z]
command = %W(#{Gitlab.config.git.bin_path} update-ref --stdin -z)
_, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
end
......@@ -270,11 +274,7 @@ class Repository
end
def kept_around?(sha)
begin
ref_exists?(keep_around_ref_name(sha))
rescue Rugged::ReferenceError
false
end
ref_exists?(keep_around_ref_name(sha))
end
def tag_names
......
......@@ -283,6 +283,31 @@
The amount of points to store in a single UDP packet. More points
results in fewer but larger UDP packets being sent.
%fieldset
%legend Background Jobs
%p
These settings require a restart to take effect.
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
= f.label :sidekiq_throttling_enabled do
= f.check_box :sidekiq_throttling_enabled
Enable Sidekiq Job Throttling
.help-block
Limit the amount of resources slow running jobs are assigned.
.form-group
= f.label :sidekiq_throttling_queues, 'Sidekiq queues to throttle', class: 'control-label col-sm-2'
.col-sm-10
= f.select :sidekiq_throttling_queues, sidekiq_queue_options_for_select, { include_hidden: false }, multiple: true, class: 'select2 select-wide', data: { field: 'sidekiq_throttling_queues' }
.help-block
Choose which queues you wish to throttle.
.form-group
= f.label :sidekiq_throttling_factor, 'Throttling Factor', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :sidekiq_throttling_factor, class: 'form-control', min: '0.01', max: '0.99', step: '0.01'
.help-block
The factor by which the queues should be throttled. A value between 0.0 and 1.0, exclusive.
%fieldset
%legend Spam and Anti-bot Protection
.form-group
......
......@@ -8,3 +8,6 @@
- if signin_enabled?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
- if signin_enabled? && signup_enabled?
%li
= link_to 'Register', '#register-pane', 'data-toggle' => 'tab'
......@@ -34,7 +34,7 @@
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
= f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
{ selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
{ selected: @group.projects.non_archived.pluck(:id) }, required: true, multiple: true, class: 'select2'
.col-md-6
.form-group
......
......@@ -4,25 +4,23 @@
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
.cover-block.groups-cover-block
.group-home-panel.text-center
%div{ class: container_class }
.avatar-container.s70.group-avatar
= image_tag group_icon(@group), class: "avatar s70 avatar-tile"
.group-info
.cover-title
%h1
@#{@group.path}
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
%h1.group-title
@#{@group.path}
%span.visibility-icon.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, fw: false)
.group-right-buttons.btn-group
- if current_user
.pull-left.append-right-10= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @group.description.present?
.group-home-desc
= markdown_field(@group, :description)
- if @group.description.present?
.cover-desc.description
= markdown_field(@group, :description)
- if current_user
.group-buttons
= render 'shared/members/access_request_buttons', source: @group
= render 'shared/notifications/button', notification_setting: @notification_setting
%div.groups-header{ class: container_class }
.top-area
......
......@@ -14,7 +14,7 @@
- if can_admin_group
= nav_link(path: 'groups#projects') do
= link_to 'Projects', projects_group_path(@group), title: 'Projects'
- if can_edit || can_leave
- if (can_edit || can_leave) && can_admin_group
%li.divider
- if can_edit
%li
......
- page_title 'Artifacts', "#{@build.name} (##{@build.id})", 'Builds'
- header_title project_title(@project, "Builds", project_builds_path(@project))
.top-block.row-content-block.clearfix
.pull-right
......
- @no_container = true
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
%script#js-board-template{ type: "text/x-template" }= render "projects/boards/components/board"
%script#js-board-list-template{ type: "text/x-template" }= render "projects/boards/components/board_list"
%script#js-board-list-card{ type: "text/x-template" }= render "projects/boards/components/card"
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
%board{ "v-cloak" => true,
"v-for" => "list in state.lists",
"ref" => "board",
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
":key" => "_uid" }
= render "projects/boards/components/sidebar"
%board-blank-state{ "inline-template" => true,
"v-if" => "list.id == 'blank'" }
"v-if" => 'list.id == "blank"' }
.board-blank-state
%p
Add the following default lists to your Issue Board with one click:
......
%board{ "inline-template" => true,
"v-cloak" => true,
"v-for" => "list in state.lists | orderBy 'position'",
"v-ref:board" => true,
":list" => "list",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
"track-by" => "_uid" }
.board{ ":class" => "{ 'is-draggable': !list.preset }",
":data-id" => "list.id" }
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
%span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
%span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => "list.type !== 'done'",
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
= icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
%board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'",
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ "inline-template" => true,
.board{ ":class" => '{ "is-draggable": !list.preset }',
":data-id" => "list.id" }
.board-inner
%header.board-header{ ":class" => '{ "has-border": list.label }', ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => '{ "user-can-drag": (!disabled && !list.preset) }' }
%span.has-tooltip{ ":title" => '(list.label ? list.label.description : "")',
data: { container: "body", placement: "bottom" } }
{{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => 'list.type !== "blank"' }
%span.board-issue-count.pull-left{ ":class" => '{ "has-btn": list.type !== "done" }' }
{{ list.issuesSize }}
- if can?(current_user, :admin_issue, @project)
%button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
"@click" => "showNewIssueForm",
"v-if" => 'list.type !== "done"',
"aria-label" => "Add an issue",
"title" => "Add an issue",
data: { placement: "top", container: "body" } }
= icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
":show-issue-form.sync" => "showIssueForm",
"v-show" => "list.type !== 'done' && showIssueForm" }
.card.board-new-issue-form
%form{ "@submit" => "submit($event)" }
.flash-container{ "v-if" => "error" }
.flash-alert
An error occured. Please try again.
%label.label-light{ ":for" => "list.id + '-title'" }
Title
%input.form-control{ type: "text",
"v-model" => "title",
"v-el:input" => true,
":id" => "list.id + '-title'" }
.clearfix.prepend-top-10
%button.btn.btn-success.pull-left{ type: "submit",
":disabled" => "title === ''",
"v-el:submit-button" => true }
Submit issue
%button.btn.btn-default.pull-right{ type: "button",
"@click" => "cancel" }
Cancel
%ul.board-list{ "v-el:list" => true,
"v-show" => "!loading",
":data-board" => "list.id",
":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
"v-if" => "!list.preset && list.id" }
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
%board-list{ "v-if" => 'list.type !== "blank"',
":list" => "list",
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
":issue-link-base" => "issueLinkBase",
"ref" => "board-list" }
- if can?(current_user, :admin_list, @project)
= render "projects/boards/components/blank_state"
.board-list-component
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
- if can? current_user, :create_issue, @project
%board-new-issue{ "inline-template" => true,
":list" => "list",
"v-if" => 'list.type !== "done" && showIssueForm' }
.card.board-new-issue-form
%form{ "@submit" => "submit($event)" }
.flash-container{ "v-if" => "error" }
.flash-alert
An error occured. Please try again.
%label.label-light{ ":for" => 'list.id + "-title"' }
Title
%input.form-control{ type: "text",
"v-model" => "title",
"ref" => "input",
":id" => 'list.id + "-title"' }
.clearfix.prepend-top-10
%button.btn.btn-success.pull-left{ type: "submit",
":disabled" => 'title === ""',
"ref" => "submit-button" }
Submit issue
%button.btn.btn-default.pull-right{ type: "button",
"@click" => "cancel" }
Cancel
%ul.board-list{ "ref" => "list",
"v-show" => "!loading",
":data-board" => "list.id",
":class" => '{ "is-smaller": showIssueForm }' }
%board-card{ "v-for" => "(issue, index) in orderedIssues",
"ref" => "issue",
":index" => "index",
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"key" => "id" }
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
%span{ "v-if" => "list.issues.length === list.issuesSize" }
Showing all issues
%span{ "v-else" => true }
Showing {{ list.issues.length }} of {{ list.issuesSize }} issues
%board-card{ "inline-template" => true,
"v-for" => "issue in issues | orderBy 'priority'",
"v-ref:issue" => true,
":index" => "$index",
":list" => "list",
":issue" => "issue",
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
":index" => "index",
"@mousedown" => "mouseDown",
"@mouseMove" => "mouseMove",
"@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
":title" => "issue.title" }
{{ issue.title }}
.card-footer
%span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee",
data: { container: 'body' } }
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
"@click" => "filterByLabel(label, $event)",
":style" => "{ backgroundColor: label.color, color: label.textColor }",
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
%li.card{ ":class" => '{ "user-can-drag": !disabled && issue.id, "is-disabled": disabled || !issue.id, "is-active": issueDetailVisible }',
":index" => "index",
"@mousedown" => "mouseDown",
"@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => 'issueLinkBase + "/" + issue.id',
":title" => "issue.title" }
{{ issue.title }}
.card-footer
%span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%a.has-tooltip{ ":href" => "\"#{root_path}\" + issue.assignee.username",
":title" => '"Assigned to " + issue.assignee.name',
"v-if" => "issue.assignee",
data: { container: 'body' } }
%img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
"@click" => "filterByLabel(label, $event)",
":style" => "{ backgroundColor: label.color, color: label.textColor }",
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- @no_container = true
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
= render "show"
- @no_container = true
- @content_class = "issue-boards-content"
- page_title "Boards"
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('boards/boards_bundle.js')
= page_specific_javascript_tag('boards/test_utils/simulate_drag.js') if Rails.env.test?
= render "projects/issues/head"
= render 'shared/issuable/filter', type: :boards
#board-app.boards-app{ "v-cloak" => true, data: board_data }
.boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
.boards-app-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
= render "projects/boards/components/board"
= render "projects/boards/components/sidebar"
= render "show"
- @no_container = true
- page_title "#{@build.name} (##{@build.id})", "Builds"
- header_title project_title(@project, "Builds", project_builds_path(@project))
- trace_with_state = @build.trace_with_state
= render "projects/pipelines/head", build_subnav: true
%div{ class: container_class }
......
- if current_user
.dropdown.inline.project-dropdown
.dropdown.inline
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
= icon("caret-down")
......
.pipeline-graph-container
.row-content-block.build-content.middle-block.pipeline-actions
.pull-right
.btn.btn-grouped.btn-white.toggle-pipeline-btn
%button.btn.btn-grouped.btn-white.toggle-pipeline-btn
%span.toggle-btn-text Hide
%span pipeline graph
%span.caret
......
......@@ -6,7 +6,7 @@
- note_count = notes.user.count
- cache_key = [project.path_with_namespace, commit.id, current_application_settings, note_count]
- cache_key.push(commit.status) if commit.status
- cache_key.push(commit.status(ref)) if commit.status(ref)
= cache(cache_key, expires_in: 1.day) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
......
......@@ -9,7 +9,7 @@
- if !project.repository.diffable?(blob)
.nothing-here-block This diff was suppressed by a .gitattributes entry.
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path, file_identifier: diff_file.file_identifier))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
This diff is collapsed.
%a.click-to-expand
......
- header_title project_title(@project, "Environments", project_environments_path(@project))
- page_title "Import in progress"
- page_title @project.forked? ? "Forking in progress" : "Import in progress"
.save-project-loader
.center
%h2
......
......@@ -74,14 +74,15 @@
%span.badge= @merge_request.diff_size
%li#resolve-count-app.line-resolve-all-container.pull-right.prepend-top-10.hidden-xs{ "v-cloak" => true }
%resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
.line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ discussionCount | pluralize 'discussion' }} resolved
= render "discussions/jump_to_next"
%div
.line-resolve-all{ "v-show" => "discussionCount > 0",
":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
%span.line-resolve-btn.is-disabled{ type: "button",
":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
= render "shared/icons/icon_status_success.svg"
%span.line-resolve-text
{{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
= render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
......
......@@ -30,11 +30,8 @@
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "!isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/inline_conflict_lines"
.diff-wrap-lines.code.file-content.js-syntax-highlight{"v-show" => "isParallel && file.resolveMode === 'interactive' && file.type === 'text'" }
= render partial: "projects/merge_requests/conflicts/components/parallel_conflict_lines"
%parallel-conflict-lines{ ":file" => "file" }
%div{"v-show" => "file.resolveMode === 'edit' || file.type === 'text-editor'"}
= render partial: "projects/merge_requests/conflicts/components/diff_file_editor"
= render partial: "projects/merge_requests/conflicts/submit_form"
-# Components
= render partial: 'projects/merge_requests/conflicts/components/parallel_conflict_line'
......@@ -5,11 +5,10 @@
%a {{line.new_line}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
%a {{line.old_line}}
%td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{{line.richText}}}
%td.line_content{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader", "v-html" => "line.richText"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{{line.richText}}}
%strong{"v-html" => "line.richText"}
%button.btn{ "@click" => "handleSelected(file, line.id, line.section)" }
{{line.buttonTitle}}
%script{"id" => 'parallel-conflict-line', "type" => "text/x-template"}
%td.diff-line-num.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%td.line_content.header{":class" => "lineCssClass(line)", "v-if" => "line.isHeader"}
%strong {{line.richText}}
%button.btn{"@click" => "handleSelected(file, line.id, line.section)"}
{{line.buttonTitle}}
%td.diff-line-num.old_line{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{line.lineNumber}}
%td.line_content.parallel{":class" => "lineCssClass(line)", "v-if" => "!line.isHeader"}
{{{line.richText}}}
%parallel-conflict-lines{"inline-template" => "true", ":file" => "file"}
%table
%tr.line_holder.parallel{"v-for" => "section in file.parallelLines"}
%td{"is"=>"parallel-conflict-line", "v-for" => "line in section", ":line" => "line", ":file" => "file"}
......@@ -32,7 +32,7 @@
"resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"v-ref:note_#{note.id}" => true }
"ref" => "note_#{note.id}" }
.note-action-button
= icon("spin spinner", "v-show" => "loading")
......@@ -43,7 +43,7 @@
"@click" => "resolve",
":title" => "buttonText",
"v-show" => "!loading",
"v-el:button" => true }
":ref" => "'button'" }
= render "shared/icons/icon_status_success.svg"
......
......@@ -66,8 +66,8 @@
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
%li.project-repo-buttons.right
.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
......@@ -76,7 +76,8 @@
= render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
.pull-right
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
.project-last-commit{ class: container_class }
= render 'projects/last_commit', commit: @repository.commit, ref: current_ref, project: @project
......
<svg width="20" height="20" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
<svg width="14" height="14" class="ci-status-icon-skipped" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><title>Group Copy 31</title><g fill="#5C5C5C" fill-rule="evenodd"><path d="M10 17.857c4.286 0 7.857-3.571 7.857-7.857S14.286 2.143 10 2.143 2.143 5.714 2.143 10 5.714 17.857 10 17.857M10 0c5.571 0 10 4.429 10 10s-4.429 10-10 10S0 15.571 0 10 4.429 0 10 0"/><path d="M10.986 11l-1.293 1.293a1 1 0 0 0 1.414 1.414l2.644-2.644a1.505 1.505 0 0 0 0-2.126l-2.644-2.644a1 1 0 0 0-1.414 1.414L10.986 9H6.4a1 1 0 0 0 0 2h4.586z"/></g></svg>
- left_align = local_assigns[:left_align]
- if notification_setting
.dropdown.notification-dropdown.pull-right
.dropdown.notification-dropdown
= form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
= hidden_setting_source_input(notification_setting)
= f.hidden_field :level, class: "notification_setting_level"
......
---
title: Fix project records with invalid visibility_level values
merge_request: 7391
author:
---
title: Fix no "Register" tab if ldap auth is enabled (#24038)
merge_request: 7274
author: Luc Didry
---
title: "[Fix] Extra divider issue in dropdown"
merge_request: 7398
author:
---
title: Removed gray button styling from todo buttons in sidebars
merge_request: 7387
author:
---
title: Remove additional padding on right-aligned items in MR widget.
merge_request: 7411
author: Didem Acet
---
title: Fix issue causing Labels not to appear in sidebar on MR page
merge_request: 7416
author: Alex Sanford
---
title: Fix expanding a collapsed diff when converting a symlink to a regular file
merge_request: 6953
author:
---
title: Add api endpoint `/groups/owned`
merge_request: 7103
author: Borja Aparicio
---
title: Fix cache for commit status in commits list to respect branches
merge_request: 7372
author:
---
title: Fix error when using invalid branch name when creating a new pipeline
merge_request: 7324
author:
---
title: Use 'Forking in progress' title when appropriate
merge_request: 7394
author: Philip Karpiak
---
title: Require projects before creating milestone.
merge_request: 7301
author: gfyoung
---
title: Added ability to throttle Sidekiq Jobs
merge_request: 7292
author:
......@@ -29,6 +29,8 @@ Sidekiq.configure_server do |config|
end
Sidekiq::Cron::Job.load_from_hash! cron_jobs
Gitlab::SidekiqThrottler.execute!
# Database pool should be at least `sidekiq_concurrency` + 2
# For more info, see: https://github.com/mperham/sidekiq/blob/master/4.0-Upgrade.md
config = ActiveRecord::Base.configurations[Rails.env] ||
......
......@@ -5,10 +5,7 @@ class OnlyAllowMergeIfAllDiscussionsAreResolved < ActiveRecord::Migration
disable_ddl_transaction!
def up
add_column_with_default(:projects,
:only_allow_merge_if_all_discussions_are_resolved,
:boolean,
default: false)
add_column :projects, :only_allow_merge_if_all_discussions_are_resolved, :boolean
end
def down
......
# See http://doc.gitlab.com/ce/development/migration_style_guide.html
# for more information on how to write migrations for GitLab.
class AddSidekiqThrottlingToApplicationSettings < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
# Set this constant to true if this migration requires downtime.
DOWNTIME = false
# When a migration requires downtime you **must** uncomment the following
# constant and define a short and easy to understand explanation as to why the
# migration requires downtime.
# DOWNTIME_REASON = ''
# When using the methods "add_concurrent_index" or "add_column_with_default"
# you must disable the use of transactions as these methods can not run in an
# existing transaction. When using "add_concurrent_index" make sure that this
# method is the _only_ method called in the migration, any other changes
# should go in a separate migration. This ensures that upon failure _only_ the
# index creation fails and can be retried or reverted easily.
#
# To disable transactions uncomment the following line and remove these
# comments:
# disable_ddl_transaction!
def change
add_column :application_settings, :sidekiq_throttling_enabled, :boolean, default: false
add_column :application_settings, :sidekiq_throttling_queues, :string
add_column :application_settings, :sidekiq_throttling_factor, :decimal
end
end
class FixProjectRecordsWithInvalidVisibility < ActiveRecord::Migration
include Gitlab::Database::MigrationHelpers
BATCH_SIZE = 1000
DOWNTIME = false
# This migration is idempotent and there's no sense in throwing away the
# partial result if it's interrupted
disable_ddl_transaction!
def up
projects = Arel::Table.new(:projects)
namespaces = Arel::Table.new(:namespaces)
finder =
projects.
join(namespaces, Arel::Nodes::InnerJoin).
on(projects[:namespace_id].eq(namespaces[:id])).
where(projects[:visibility_level].gt(namespaces[:visibility_level])).
project(projects[:id]).
take(BATCH_SIZE)
# MySQL requires a derived table to perform this query
nested_finder =
projects.
from(finder.as("AS projects_inner")).
project(projects[:id])
valuer =
namespaces.
where(namespaces[:id].eq(projects[:namespace_id])).
project(namespaces[:visibility_level])
# Update matching rows until none remain. The finder contains a limit.
loop do
updater = Arel::UpdateManager.new(ActiveRecord::Base).
table(projects).
set(projects[:visibility_level] => Arel::Nodes::SqlLiteral.new("(#{valuer.to_sql})")).
where(projects[:id].in(nested_finder))
num_updated = connection.exec_update(updater.to_sql, self.class.name, [])
break if num_updated == 0
end
end
def down
# no-op
end
end
......@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 20161106185620) do
ActiveRecord::Schema.define(version: 20161109150329) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
......@@ -98,6 +98,9 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.text "help_page_text_html"
t.text "shared_runners_text_html"
t.text "after_sign_up_text_html"
t.boolean "sidekiq_throttling_enabled", default: false
t.string "sidekiq_throttling_queues"
t.decimal "sidekiq_throttling_factor"
t.boolean "housekeeping_enabled", default: true, null: false
t.boolean "housekeeping_bitmaps_enabled", default: true, null: false
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
......@@ -912,7 +915,7 @@ ActiveRecord::Schema.define(version: 20161106185620) do
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
t.text "description_html"
t.boolean "only_allow_merge_if_all_discussions_are_resolved", default: false, null: false
t.boolean "only_allow_merge_if_all_discussions_are_resolved"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
......
# GitLab operations
- [Sidekiq MemoryKiller](operations/sidekiq_memory_killer.md)
- [Sidekiq Job throttling](operations/sidekiq_job_throttling.md)
- [Cleaning up Redis sessions](operations/cleaning_up_redis_sessions.md)
- [Understanding Unicorn and unicorn-worker-killer](operations/unicorn.md)
- [Moving repositories to a new location](operations/moving_repositories.md)
# Sidekiq Job throttling
> Note: Introduced with GitLab 8.14
When your GitLab installation needs to handle tens of thousands of background
jobs, it can be convenient to throttle queues that do not need to be executed
immediately, e.g. long running jobs like Pipelines, thus allowing jobs that do
need to be executed immediately to have access to more resources.
In order to accomplish this, you can limit the amount of workers that certain
slow running queues can have available. This is what we call Sidekiq Job
Throttling. Depending on your infrastructure, you might have different slow
running queues, which is why you can choose which queues you want to throttle
and by how much you want to throttle them.
These settings are available in the Application Settings of your GitLab
installation.
![Sidekiq Job Throttling](img/sidekiq_job_throttling.png)
The throttle factor determines the maximum number of workers a queue can run on.
This value gets multiplied by `:concurrency` value set in the Sidekiq settings
and rounded up to the closest full integer.
So, for example, you set the `:concurrency` to 25 and the `Throttling factor` to
0.1, the maximum workers assigned to the selected queues would be 3.
```ruby
queue_limit = (factor * Sidekiq.options[:concurrency]).ceil
```
After enabling the job throttling, you will need to restart your GitLab
instance, in order for the changes to take effect.
\ No newline at end of file
......@@ -26,6 +26,15 @@ GET /groups
You can search for groups by name or path, see below.
=======
## List owned groups
Get a list of groups which are owned by the authenticated user.
```
GET /groups/owned
```
## List a group's projects
Get a list of projects in this group.
......
......@@ -14,7 +14,7 @@
contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
- [UX guide](ux_guide/README.md) for building GitLab with existing CSS styles and elements
- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for working with SQL queries
- [Sidekiq guidelines](sidekiq_style_guide.md) for working with Sidekiq workers
......
# Basics
## Contents
* [Responsive](#responsive)
* [Typography](#typography)
* [Icons](#icons)
* [Color](#color)
* [Motion](#motion)
* [Voice and tone](#voice-and-tone)
---
## Responsive
GitLab is a responsive experience that works well across all screen sizes, from mobile devices to large monitors. In order to provide a great user experience, the core functionality (browsing files, creating issues, writing comments, etc.) must be available at all resolutions. However, due to size limitations, some secondary functionality may be hidden on smaller screens. Please keep this functionality limited to rare actions that aren't expected to be needed on small devices.
---
## Typography
### Primary typeface
GitLab's main typeface used throughout the UI is **Source Sans Pro**. We support both the bold and regular weight.
![Source Sans Pro sample](img/sourcesanspro-sample.png)
### Monospace typeface
This is the typeface used for code blocks. GitLab uses the OS default font.
- **Menlo** (Mac)
- **Consolas** (Windows)
- **Liberation Mono** (Linux)
![Monospace font sample](img/monospacefont-sample.png)
---
## Icons
GitLab uses Font Awesome icons throughout our interface.
![Trash icon](img/icon-trash.png)
The trash icon is used for destructive actions that deletes information.
![Edit icon](img/icon-edit.png)
The pencil icon is used for editing content such as comments.
![Notification icon](img/icon-notification.png)
The bell icon is for notifications, such as Todos.
![Subscribe icon](img/icon-subscribe.png)
The eye icon is for subscribing to updates. For example, you can subscribe to a label and get updated on issues with that label.
![RSS icon](img/icon-rss.png)
The standard RSS icon is used for linking to RSS/atom feeds.
![Close icon](img/icon-close.png)
An 'x' is used for closing UI elements such as dropdowns.
![Add icon](img/icon-add.png)
A plus is used when creating new objects, such as issues, projects, etc.
> TODO: update this section, add more general guidance to icon usage and personality, etc.
---
## Color
![Blue](img/color-blue.png)
Blue is used to highlight primary active elements (such as current tab), as well as other organization and managing commands.
![Green](img/color-green.png)
Green is for actions that create new objects.
![Orange](img/color-orange.png)
Orange is used for warnings
![Red](img/color-red.png)
Red is reserved for delete and other destructive commands
![Grey](img/color-grey.png)
Grey, and white (depending on context) is used for netral, secondary elements
> TODO: Establish a perspective for color in terms of our personality and rationalize with Marketing usage.
---
## Motion
Motion is a tool to help convey important relationships, changes or transitions between elements. It should be used sparingly and intentionally, highlighting the right elements at the right moment.
> TODO: Determine a more concrete perspective on motion, create consistent easing/timing curves to follow.
---
## Voice and tone
The copy for GitLab is clear and direct. We strike a clear balance between professional and friendly. We can empathesize with users (such as celebrating completing all Todos), and remain respectful of the importance of the work. We are that trusted, friendly coworker that is helpful and understanding.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment