Commit 40a96946 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 1589-deploy-boards

* master: (212 commits)
  Truncate the DB in after(:all) test for Gitlab::ImportExport::ProjectTreeRestorer
  Fix  depenendencies loading Vue in EE
  Resolve conflicts
  writes test suite
  Support v4 API for GitLab Geo endpoints
  update yarn resolution for jquery-ui
  Remove support for locking in pipeline retry service
  Update gitlab_flow.md
  changed minor inconsistency in help text
  Update lock file
  use http to feth jquery-ui dependency instead of ssh
  Change development tanuki favicon colors to match logo color order
  Allow slashes in slash command arguments
  Geo: Don't create the project when it's being imported
  A few more specs
  Replicate repository creation in Geo secondary node
  Add API endpoint to get all milestone merge requests
  Added back weight icon on issue rows
  Backport API to v3
  remove trailing comma
  ...
parents 37ca5258 8800c0da
### Background:
(Include problem, use cases, benefits, and/or goals)
**What questions are you trying to answer?**
**Are you looking to verify an existing hypothesis or uncover new issues you should be exploring?**
**What is the backstory of this project and how does it impact the approach?**
**What do you already know about the areas you are exploring?**
**What does success look like at the end of the project?**
### Links / references:
/label ~"UX research"
...@@ -93,18 +93,20 @@ Please see the [UX Guide for GitLab]. ...@@ -93,18 +93,20 @@ Please see the [UX Guide for GitLab].
### Retrospective ### Retrospective
After each release (usually on the 22nd of each month), we have a retrospective After each release, we have a retrospective call where we discuss what went well,
call where we discuss what went well, what went wrong, and what we can improve what went wrong, and what we can improve for the next release. The
for the next release. The [retrospective notes] are public and you are invited [retrospective notes] are public and you are invited to comment on them.
to comment them. If you're interested, you can even join the
If you're interested, you can even join the [retrospective call][retro-kickoff-call]. [retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### Kickoff ### Kickoff
Before working on the next release (usually on the 8th of each month), we have a Before working on the next release, we have a
kickoff call to explain what we expect to ship in the next release. The kickoff call to explain what we expect to ship in the next release. The
[kickoff notes] are public and you are invited to comment them. [kickoff notes] are public and you are invited to comment on them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call]. If you're interested, you can even join the [kickoff call][retro-kickoff-call],
on the first working day after the 7th at 6pm CET / 9am PST..
[retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing [retrospective notes]: https://docs.google.com/document/d/1nEkM_7Dj4bT21GJy0Ut3By76FZqCfLBmFQNVThmW2TY/edit?usp=sharing
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing [kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
......
...@@ -103,11 +103,6 @@ require('es6-promise').polyfill(); ...@@ -103,11 +103,6 @@ require('es6-promise').polyfill();
} }
}); });
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
cursorborder: '1px solid #FFF'
});
$('.js-select-on-focus').on('focusin', function () { $('.js-select-on-focus').on('focusin', function () {
return $(this).select().one('mouseup', function (e) { return $(this).select().one('mouseup', function (e) {
return e.preventDefault(); return e.preventDefault();
...@@ -250,8 +245,6 @@ require('es6-promise').polyfill(); ...@@ -250,8 +245,6 @@ require('es6-promise').polyfill();
}); });
gl.awardsHandler = new AwardsHandler(); gl.awardsHandler = new AwardsHandler();
new Aside(); new Aside();
// bind sidebar events
new gl.Sidebar();
gl.utils.initTimeagoTimeout(); gl.utils.initTimeagoTimeout();
}); });
......
...@@ -97,7 +97,7 @@ $(() => { ...@@ -97,7 +97,7 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
return Store.shouldAddBlankState(); return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
}, },
}, },
template: ` template: `
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* *
* Used to store the Pipelines rendered in the commit view in the pipelines table. * Used to store the Pipelines rendered in the commit view in the pipelines table.
*/ */
require('../../vue_realtime_listener');
class PipelinesStore { class PipelinesStore {
constructor() { constructor() {
...@@ -24,7 +25,7 @@ class PipelinesStore { ...@@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed. * update the time to show how long as passed.
* *
*/ */
startTimeAgoLoops() { static startTimeAgoLoops() {
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => { this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => { this.$children[0].$children.reduce((acc, component) => {
...@@ -44,7 +45,4 @@ class PipelinesStore { ...@@ -44,7 +45,4 @@ class PipelinesStore {
} }
} }
window.gl = window.gl || {}; module.exports = PipelinesStore;
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource')); ...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils'); require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor'); require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table'); require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service'); require('./pipelines_service');
require('./pipelines_store'); const PipelineStore = require('./pipelines_store');
/** /**
* *
...@@ -41,7 +40,7 @@ require('./pipelines_store'); ...@@ -41,7 +40,7 @@ require('./pipelines_store');
data() { data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset; const svgsData = document.querySelector('.pipeline-svgs').dataset;
const store = new gl.commits.pipelines.PipelinesStore(); const store = new PipelineStore();
// Transform svgs DOMStringMap to a plain Object. // Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData); const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
...@@ -71,7 +70,6 @@ require('./pipelines_store'); ...@@ -71,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.storePipelines(json); this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
...@@ -80,9 +78,15 @@ require('./pipelines_store'); ...@@ -80,9 +78,15 @@ require('./pipelines_store');
}); });
}, },
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
},
template: ` template: `
<div> <div class="pipelines">
<div class="pipelines realtime-loading" v-if="isLoading"> <div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
</div> </div>
......
...@@ -121,6 +121,7 @@ const ShortcutsBlob = require('./shortcuts_blob'); ...@@ -121,6 +121,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.IssuableTemplateSelectors(); new gl.IssuableTemplateSelectors();
break; break;
case 'projects:merge_requests:new': case 'projects:merge_requests:new':
case 'projects:merge_requests:new_diffs':
case 'projects:merge_requests:edit': case 'projects:merge_requests:edit':
new gl.Diff(); new gl.Diff();
shortcut_handler = new ShortcutsNavigation(); shortcut_handler = new ShortcutsNavigation();
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $dueDateInput.get(0), field: $dueDateInput.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect: (dateText) => { onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd'); const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
...@@ -63,6 +63,7 @@ ...@@ -63,6 +63,7 @@
} }
}); });
calendar.setDate(new Date($dueDateInput.val()));
this.$datePicker.append(calendar.el); this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar); this.$datePicker.data('pikaday', calendar);
} }
...@@ -169,11 +170,12 @@ ...@@ -169,11 +170,12 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $datePicker.get(0), field: $datePicker.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect(dateText) { onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}); });
calendar.setDate(new Date($datePicker.val()));
$datePicker.data('pikaday', calendar); $datePicker.data('pikaday', calendar);
}); });
......
...@@ -8,6 +8,7 @@ const EnvironmentTable = require('./environments_table'); ...@@ -8,6 +8,7 @@ const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store'); const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination'); require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils'); require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', { module.exports = Vue.component('environment-component', {
......
...@@ -259,6 +259,11 @@ module.exports = Vue.component('environment-item', { ...@@ -259,6 +259,11 @@ module.exports = Vue.component('environment-item', {
return undefined; return undefined;
}, },
terminalIconSvg: {
type: String,
required: false,
},
/** /**
* If provided, returns the commit tag. * If provided, returns the commit tag.
* *
...@@ -436,7 +441,6 @@ module.exports = Vue.component('environment-item', { ...@@ -436,7 +441,6 @@ module.exports = Vue.component('environment-item', {
return true; return true;
// return this.model.rollout_status; // return this.model.rollout_status;
}, },
}, },
/** /**
...@@ -457,6 +461,7 @@ module.exports = Vue.component('environment-item', { ...@@ -457,6 +461,7 @@ module.exports = Vue.component('environment-item', {
template: ` template: `
<tr> <tr>
<td> <td>
<span class="deploy-board-icon" <span class="deploy-board-icon"
v-if="!model.isFolder" v-if="!model.isFolder"
v-on:click="toggleDeployBoard(model)"> v-on:click="toggleDeployBoard(model)">
......
...@@ -4,7 +4,6 @@ ...@@ -4,7 +4,6 @@
const Vue = require('vue'); const Vue = require('vue');
const EnvironmentItem = require('./environment_item'); const EnvironmentItem = require('./environment_item');
const DeployBoard = require('./deploy_board_component'); const DeployBoard = require('./deploy_board_component');
module.exports = Vue.component('environment-table-component', { module.exports = Vue.component('environment-table-component', {
components: { components: {
......
const EnvironmentsFolderComponent = require('./environments_folder_view'); const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-param-reassign, no-new */
/* global Flash */ /* global Flash */
const Vue = require('vue'); const Vue = window.Vue = require('vue');
Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service'); const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table'); const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store'); const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination'); require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils'); require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', { module.exports = Vue.component('environment-folder-view', {
......
...@@ -103,6 +103,9 @@ ...@@ -103,6 +103,9 @@
this.input.each((i, input) => { this.input.each((i, input) => {
const $input = $(input); const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input)); $input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
}); });
}, },
setupAtWho: function($input) { setupAtWho: function($input) {
......
...@@ -42,11 +42,12 @@ ...@@ -42,11 +42,12 @@
calendar = new Pikaday({ calendar = new Pikaday({
field: $issuableDueDate.get(0), field: $issuableDueDate.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
onSelect: function(dateText) { onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
} }
}); });
calendar.setDate(new Date($issuableDueDate.val()));
} }
} }
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
require('./flash'); require('./flash');
require('vendor/jquery.waitforimages'); require('vendor/jquery.waitforimages');
require('vendor/task_list'); require('./task_list');
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -11,10 +11,16 @@ require('vendor/task_list'); ...@@ -11,10 +11,16 @@ require('vendor/task_list');
this.Issue = (function() { this.Issue = (function() {
function Issue() { function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this); this.submitNoteForm = bind(this.submitNoteForm, this);
// Prevent duplicate event bindings
this.disableTaskList();
if ($('a.btn-close').length) { if ($('a.btn-close').length) {
this.initTaskList(); this.taskList = new gl.TaskList({
dataType: 'issue',
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: (result) => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
this.initIssueBtnEventListeners(); this.initIssueBtnEventListeners();
} }
this.initMergeRequests(); this.initMergeRequests();
...@@ -22,11 +28,6 @@ require('vendor/task_list'); ...@@ -22,11 +28,6 @@ require('vendor/task_list');
this.initCanCreateBranch(); this.initCanCreateBranch();
} }
Issue.prototype.initTaskList = function() {
$('.detail-page-description .js-task-list-container').taskList('enable');
return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
};
Issue.prototype.initIssueBtnEventListeners = function() { Issue.prototype.initIssueBtnEventListeners = function() {
var _this, issueFailMessage; var _this, issueFailMessage;
_this = this; _this = this;
...@@ -54,16 +55,19 @@ require('vendor/task_list'); ...@@ -54,16 +55,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text());
if (isClose) { if (isClose) {
$('a.btn-close').addClass('hidden'); $('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden'); $('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden'); $('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1);
} else { } else {
$('a.btn-reopen').addClass('hidden'); $('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden'); $('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden'); $('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden'); $('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
} }
} else { } else {
new Flash(issueFailMessage, 'alert'); new Flash(issueFailMessage, 'alert');
...@@ -82,30 +86,6 @@ require('vendor/task_list'); ...@@ -82,30 +86,6 @@ require('vendor/task_list');
} }
}; };
Issue.prototype.disableTaskList = function() {
$('.detail-page-description .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
};
Issue.prototype.updateTaskList = function() {
var patchData;
patchData = {};
patchData['issue'] = {
'description': $('.js-task-list-field', this).val()
};
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
data: patchData,
success: function(issue) {
document.querySelector('#task_status').innerText = issue.task_status;
document.querySelector('#task_status_short').innerText = issue.task_status_short;
}
});
// TODO (rspeicher): Make the issue description inline-editable like a note so
// that we can re-use its form here
};
Issue.prototype.initMergeRequests = function() { Issue.prototype.initMergeRequests = function() {
var $container; var $container;
$container = $('#merge-requests'); $container = $('#merge-requests');
......
...@@ -19,7 +19,7 @@ ...@@ -19,7 +19,7 @@
const calendar = new Pikaday({ const calendar = new Pikaday({
field: $input.get(0), field: $input.get(0),
theme: 'gitlab-theme', theme: 'gitlab-theme',
format: 'YYYY-MM-DD', format: 'yyyy-mm-dd',
minDate: new Date(), minDate: new Date(),
onSelect(dateText) { onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd')); $input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
...@@ -30,6 +30,7 @@ ...@@ -30,6 +30,7 @@
}, },
}); });
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar); $input.data('pikaday', calendar);
}); });
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
/* global MergeRequestTabs */ /* global MergeRequestTabs */
require('vendor/jquery.waitforimages'); require('vendor/jquery.waitforimages');
require('vendor/task_list'); require('./task_list');
require('./merge_request_tabs'); require('./merge_request_tabs');
(function() { (function() {
...@@ -24,12 +24,18 @@ require('./merge_request_tabs'); ...@@ -24,12 +24,18 @@ require('./merge_request_tabs');
}; };
})(this)); })(this));
this.initTabs(); this.initTabs();
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners(); this.initMRBtnListeners();
this.initCommitMessageListeners(); this.initCommitMessageListeners();
if ($("a.btn-close").length) { if ($("a.btn-close").length) {
this.initTaskList(); this.taskList = new gl.TaskList({
dataType: 'merge_request',
fieldName: 'description',
selector: '.detail-page-description',
onSuccess: (result) => {
document.querySelector('#task_status').innerText = result.task_status;
document.querySelector('#task_status_short').innerText = result.task_status_short;
}
});
} }
} }
...@@ -50,11 +56,6 @@ require('./merge_request_tabs'); ...@@ -50,11 +56,6 @@ require('./merge_request_tabs');
return this.$('.all-commits').removeClass('hide'); return this.$('.all-commits').removeClass('hide');
}; };
MergeRequest.prototype.initTaskList = function() {
$('.detail-page-description .js-task-list-container').taskList('enable');
return $(document).on('tasklist:changed', '.detail-page-description .js-task-list-container', this.updateTaskList);
};
MergeRequest.prototype.initMRBtnListeners = function() { MergeRequest.prototype.initMRBtnListeners = function() {
var _this; var _this;
_this = this; _this = this;
...@@ -85,30 +86,6 @@ require('./merge_request_tabs'); ...@@ -85,30 +86,6 @@ require('./merge_request_tabs');
} }
}; };
MergeRequest.prototype.disableTaskList = function() {
$('.detail-page-description .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.detail-page-description .js-task-list-container');
};
MergeRequest.prototype.updateTaskList = function() {
var patchData;
patchData = {};
patchData['merge_request'] = {
'description': $('.js-task-list-field', this).val()
};
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
data: patchData,
success: function(mergeRequest) {
document.querySelector('#task_status').innerText = mergeRequest.task_status;
document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
}
});
// TODO (rspeicher): Make the merge request description inline-editable like a
// note so that we can re-use its form here
};
MergeRequest.prototype.initCommitMessageListeners = function() { MergeRequest.prototype.initCommitMessageListeners = function() {
$(document).on('click', 'a.js-with-description-link', function(e) { $(document).on('click', 'a.js-with-description-link', function(e) {
var textarea = $('textarea.js-commit-message'); var textarea = $('textarea.js-commit-message');
......
...@@ -103,9 +103,10 @@ require('./flash'); ...@@ -103,9 +103,10 @@ require('./flash');
} }
clickTab(e) { clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) { if (e.currentTarget && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href'); const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation(); e.stopImmediatePropagation();
e.preventDefault();
window.open(targetLink, '_blank'); window.open(targetLink, '_blank');
} }
} }
......
...@@ -11,7 +11,7 @@ require('./dropzone_input'); ...@@ -11,7 +11,7 @@ require('./dropzone_input');
require('./gfm_auto_complete'); require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho'); require('vendor/jquery.atwho');
require('vendor/task_list'); require('./task_list');
(function() { (function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; }; var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
...@@ -51,7 +51,11 @@ require('vendor/task_list'); ...@@ -51,7 +51,11 @@ require('vendor/task_list');
this.addBinding(); this.addBinding();
this.setPollingInterval(); this.setPollingInterval();
this.setupMainTargetNoteForm(); this.setupMainTargetNoteForm();
this.initTaskList(); this.taskList = new gl.TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes'
});
this.collapseLongCommitList(); this.collapseLongCommitList();
// We are in the Merge Requests page so we need another edit form for Changes tab // We are in the Merge Requests page so we need another edit form for Changes tab
...@@ -125,8 +129,6 @@ require('vendor/task_list'); ...@@ -125,8 +129,6 @@ require('vendor/task_list');
$(document).off("keydown", ".js-note-text"); $(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button'); $(document).off('click', '.js-comment-resolve-button');
$(document).off("click", '.system-note-commit-list-toggler'); $(document).off("click", '.system-note-commit-list-toggler');
$('.note .js-task-list-container').taskList('disable');
return $(document).off('tasklist:changed', '.note .js-task-list-container');
}; };
Notes.prototype.keydownNoteText = function(e) { Notes.prototype.keydownNoteText = function(e) {
...@@ -286,7 +288,7 @@ require('vendor/task_list'); ...@@ -286,7 +288,7 @@ require('vendor/task_list');
// Update datetime format on the recent note // Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false); gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList(); this.collapseLongCommitList();
this.initTaskList(); this.taskList.init();
this.refresh(); this.refresh();
return this.updateNotesCount(1); return this.updateNotesCount(1);
} }
...@@ -863,15 +865,6 @@ require('vendor/task_list'); ...@@ -863,15 +865,6 @@ require('vendor/task_list');
} }
}; };
Notes.prototype.initTaskList = function() {
this.enableTaskList();
return $(document).on('tasklist:changed', '.note .js-task-list-container', this.updateTaskList.bind(this));
};
Notes.prototype.enableTaskList = function() {
return $('.note .js-task-list-container').taskList('enable');
};
Notes.prototype.putEditFormInPlace = function($el) { Notes.prototype.putEditFormInPlace = function($el) {
var $editForm = $(this.getEditFormSelector($el)); var $editForm = $(this.getEditFormSelector($el));
var $note = $el.closest('.note'); var $note = $el.closest('.note');
...@@ -896,17 +889,6 @@ require('vendor/task_list'); ...@@ -896,17 +889,6 @@ require('vendor/task_list');
$editForm.find('.referenced-users').hide(); $editForm.find('.referenced-users').hide();
}; };
Notes.prototype.updateTaskList = function(e) {
var $target = $(e.target);
var $list = $target.closest('.js-task-list-container');
var $editForm = $(this.getEditFormSelector($target));
var $note = $list.closest('.note');
this.putEditFormInPlace($list);
$editForm.find('#note_note').val($note.find('.original-task-list').val());
$('form', $list).submit();
};
Notes.prototype.updateNotesCount = function(updateCount) { Notes.prototype.updateNotesCount = function(updateCount) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount); return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
}; };
...@@ -923,9 +905,10 @@ require('vendor/task_list'); ...@@ -923,9 +905,10 @@ require('vendor/task_list');
}; };
Notes.prototype.toggleCommitList = function(e) { Notes.prototype.toggleCommitList = function(e) {
const $element = $(e.target); const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list'); const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade'); $closestSystemCommitList.toggleClass('hide-shade');
}; };
......
...@@ -38,13 +38,15 @@ ...@@ -38,13 +38,15 @@
this.$buttons.attr('data-status', newStatus); this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction); this.$buttons.find('> span').text(newAction);
for (const button of this.$buttons) { this.$buttons.map((button) => {
const $button = $(button); const $button = $(button);
if ($button.attr('data-original-title')) { if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle'); $button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
} }
}
return button;
});
}); });
} }
} }
......
...@@ -21,11 +21,16 @@ ...@@ -21,11 +21,16 @@
}; };
Sidebar.prototype.addEventListeners = function() { Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked); this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden); $('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading); $('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded); $('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(document).on('click', '.js-sidebar-toggle', function(e, triggered) { $(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => throttledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon; var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault(); e.preventDefault();
$this = $(this); $this = $(this);
...@@ -191,6 +196,17 @@ ...@@ -191,6 +196,17 @@
} }
}; };
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
$rightSidebar.outerHeight('100%');
}
};
Sidebar.prototype.isOpen = function() { Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded'); return this.sidebar.is('.right-sidebar-expanded');
}; };
......
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
const pinnedToggleSelector = '.js-nav-pin';
const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
const pinnedPageClass = 'page-sidebar-pinned';
const expandedPageClass = 'page-sidebar-expanded';
const pinnedNavbarClass = 'header-sidebar-pinned';
const expandedNavbarClass = 'header-sidebar-expanded';
class Sidebar {
constructor() {
if (!Sidebar.singleton) {
Sidebar.singleton = this;
Sidebar.singleton.init();
}
return Sidebar.singleton;
}
init() {
this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
this.isExpanded = (
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
$(window).on('resize', () => this.setSidebarHeight());
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState())
.on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
this.setSidebarHeight();
}
handleClickEvent(e) {
if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
const $target = $(e.target);
const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
this.toggleSidebar();
}
}
}
updateTodoCount(count) {
$('.js-todos-count').text(gl.text.addDelimiter(count));
}
toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.renderState();
}
setSidebarHeight() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$('.js-right-sidebar').outerHeight($(window).height() - diff);
} else {
$('.js-right-sidebar').outerHeight('100%');
}
}
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
this.isExpanded = false;
}
Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
this.renderState();
}
renderState() {
$(pageSelector)
.toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
.toggleClass(expandedPageClass, this.isExpanded);
$(navbarSelector)
.toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
.toggleClass(expandedNavbarClass, this.isExpanded);
const $pinnedToggle = $(pinnedToggleSelector);
const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
const sidebarContent = $(sidebarContentSelector);
setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
window.gl = window.gl || {};
gl.Sidebar = Sidebar;
})();
require('vendor/task_list');
class TaskList {
constructor(options = {}) {
this.selector = options.selector;
this.dataType = options.dataType;
this.fieldName = options.fieldName;
this.onSuccess = options.onSuccess || (() => {});
this.init();
}
init() {
// Prevent duplicate event bindings
this.disable();
$(`${this.selector} .js-task-list-container`).taskList('enable');
$(document).on('tasklist:changed', `${this.selector} .js-task-list-container`, this.update.bind(this));
}
disable() {
$(`${this.selector} .js-task-list-container`).taskList('disable');
$(document).off('tasklist:changed', `${this.selector} .js-task-list-container`);
}
update(e) {
const $target = $(e.target);
const patchData = {};
patchData[this.dataType] = {
[this.fieldName]: $target.val(),
};
return $.ajax({
type: 'PATCH',
url: $target.data('update-url') || $('form.js-issuable-update').attr('action'),
data: patchData,
success: this.onSuccess,
});
}
}
window.gl = window.gl || {};
window.gl.TaskList = TaskList;
...@@ -147,24 +147,21 @@ ...@@ -147,24 +147,21 @@
goToTodoUrl(e) { goToTodoUrl(e) {
const todoLink = this.dataset.url; const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) { if (!todoLink) {
return; return;
} }
if (gl.utils.isMetaClick(e)) { if (gl.utils.isMetaClick(e)) {
const windowTarget = '_blank';
const selected = e.target;
e.preventDefault(); e.preventDefault();
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually. if (selected.tagName === 'IMG') {
if (targetLink !== todoLink) { const avatarUrl = selected.parentElement.getAttribute('href');
return window.open(targetLink, '_blank'); return window.open(avatarUrl, windowTarget);
} else { } else {
return window.open(todoLink, '_blank'); return window.open(todoLink, windowTarget);
} }
} else { } else {
return gl.utils.visitUrl(todoLink); return gl.utils.visitUrl(todoLink);
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download
:href='artifact.path' :href='artifact.path'
> >
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
......
...@@ -5,6 +5,7 @@ window.Vue = require('vue'); ...@@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination'); require('../vue_shared/components/table_pagination');
require('./store'); require('./store');
require('../vue_shared/components/pipelines_table'); require('../vue_shared/components/pipelines_table');
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => { ((gl) => {
gl.VuePipelines = Vue.extend({ gl.VuePipelines = Vue.extend({
...@@ -28,24 +29,34 @@ require('../vue_shared/components/pipelines_table'); ...@@ -28,24 +29,34 @@ require('../vue_shared/components/pipelines_table');
}, },
props: ['scope', 'store', 'svgs'], props: ['scope', 'store', 'svgs'],
created() { created() {
const pagenum = gl.utils.getParameterByName('p'); const pagenum = gl.utils.getParameterByName('page');
const scope = gl.utils.getParameterByName('scope'); const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum; if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope; if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
}, },
methods: {
beforeUpdate() {
if (this.pipelines.length && this.$children) {
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
}
},
methods: {
/** /**
* Changes the URL according to the pagination component. * Changes the URL according to the pagination component.
* *
* If no scope is provided, 'all' is assumed. * If no scope is provided, 'all' is assumed.
* *
* Pagination component sends "null" when no scope is provided.
*
* @param {Number} pagenum * @param {Number} pagenum
* @param {String} apiScope = 'all' * @param {String} apiScope = 'all'
*/ */
change(pagenum, apiScope = 'all') { change(pagenum, apiScope) {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); if (!apiScope) apiScope = 'all';
gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
}, },
}, },
template: ` template: `
......
/* global gl, Flash */ /* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */ /* eslint-disable no-param-reassign */
require('../vue_realtime_listener');
((gl) => { ((gl) => {
const pageValues = (headers) => { const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers); const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.normalizeHeaders(normalized); const paginationInfo = gl.utils.parseIntPagination(normalized);
return paginationInfo; return paginationInfo;
}; };
gl.PipelineStore = class { gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) { fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true; this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () => return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`) .then((response) => {
.then((response) => { const pageInfo = pageValues(response.headers);
const pageInfo = pageValues(response.headers); this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body); const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count); this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines); this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count); this.pageRequest = false;
this.pageRequest = false; }, () => {
}, () => { this.pageRequest = false;
this.pageRequest = false; return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
return new Flash('An error occurred while fetching the pipelines, please reload the page again.'); });
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
} }
}; };
})(window.gl || (window.gl = {})); })(window.gl || (window.gl = {}));
...@@ -19,7 +19,6 @@ ...@@ -19,7 +19,6 @@
@import "framework/flash.scss"; @import "framework/flash.scss";
@import "framework/forms.scss"; @import "framework/forms.scss";
@import "framework/gfm.scss"; @import "framework/gfm.scss";
@import "framework/gitlab-theme.scss";
@import "framework/header.scss"; @import "framework/header.scss";
@import "framework/highlight.scss"; @import "framework/highlight.scss";
@import "framework/issue_box.scss"; @import "framework/issue_box.scss";
......
...@@ -116,7 +116,7 @@ ...@@ -116,7 +116,7 @@
} }
.btn, .btn,
.side-nav-toggle { .global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow); @include transition(background-color, border-color, color, box-shadow);
} }
...@@ -140,7 +140,6 @@ a { ...@@ -140,7 +140,6 @@ a {
@include transition(background-color, box-shadow); @include transition(background-color, box-shadow);
} }
.nav-sidebar a,
.dropdown-menu a, .dropdown-menu a,
.dropdown-menu button, .dropdown-menu button,
.dropdown-menu-nav a { .dropdown-menu-nav a {
......
/**
* Styles the GitLab application with a specific color theme
*
* $color-light -
* $color -
* $color-darker -
* $color-dark -
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
&:hover {
color: $white-light;
}
}
.sidebar-wrapper {
background: $color-darker;
}
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
}
.nav-sidebar {
li {
a {
color: $color-light;
&:hover,
&:focus,
&:active {
background: $color-dark;
}
i {
color: $color-light;
}
path,
polygon {
fill: $color-light;
}
.count {
color: $color-light;
background: $color-dark;
}
svg {
position: relative;
top: 3px;
}
}
&.separate-item {
border-top: 1px solid $color;
}
&.active a {
color: $white-light;
background: $color-dark;
&.no-highlight {
border: none;
}
i {
color: $white-light;
}
path,
polygon {
fill: $white-light;
}
}
}
.about-gitlab {
color: $color-light;
}
}
}
}
$theme-charcoal-light: #b9bbbe;
$theme-charcoal: #485157;
$theme-charcoal-dark: #3d454d;
$theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9;
$theme-blue: #2980b9;
$theme-blue-dark: #1970a9;
$theme-blue-darker: #096099;
$theme-graphite-light: #ccc;
$theme-graphite: #777;
$theme-graphite-dark: #666;
$theme-graphite-darker: #555;
$theme-black-light: #979797;
$theme-black: #373737;
$theme-black-dark: #272727;
$theme-black-darker: #222;
$theme-green-light: #adc;
$theme-green: #019875;
$theme-green-dark: #018865;
$theme-green-darker: #017855;
$theme-violet-light: #98c;
$theme-violet: #548;
$theme-violet-dark: #436;
$theme-violet-darker: #325;
body {
&.ui_blue {
@include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
}
&.ui_charcoal {
@include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
}
&.ui_graphite {
@include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
}
&.ui_black {
@include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
@include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
}
&.ui_violet {
@include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
}
}
...@@ -100,23 +100,42 @@ header { ...@@ -100,23 +100,42 @@ header {
} }
} }
} }
}
.side-nav-toggle { .global-dropdown {
position: absolute; position: absolute;
left: -10px; left: -10px;
margin: 7px 0;
font-size: 18px; .badge {
padding: 6px 10px; font-size: 11px;
border: none; }
background-color: $gray-light;
li {
.active a {
font-weight: bold;
}
&:hover { &:hover {
background-color: $white-normal; .badge {
color: $gl-header-nav-hover-color; background-color: $white-light;
}
} }
} }
} }
.global-dropdown-toggle {
margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $gray-light;
&:hover {
background-color: $white-normal;
color: $gl-header-nav-hover-color;
}
}
.header-content { .header-content {
position: relative; position: relative;
height: $header-height; height: $header-height;
......
.page-with-sidebar {
padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
.sidebar-wrapper {
box-shadow: none;
}
}
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
height: 100%;
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
box-shadow: 2px 0 16px 0 $black-transparent;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
}
.content-wrapper { .content-wrapper {
width: 100%; width: 100%;
transition: padding $sidebar-transition-duration; transition: padding $sidebar-transition-duration;
...@@ -47,105 +14,6 @@ ...@@ -47,105 +14,6 @@
} }
} }
.nav-sidebar {
position: absolute;
top: 50px;
bottom: 0;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
&.navbar-collapse {
padding: 0 !important;
}
li {
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
i {
font-size: 16px;
}
i,
svg {
margin-right: 13px;
}
}
}
.count {
float: right;
padding: 0 8px;
border-radius: 6px;
}
.about-gitlab {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
position: absolute;
bottom: 10px;
}
}
.sidebar-action-buttons {
width: $sidebar_width;
position: absolute;
top: 0;
left: 0;
min-height: 50px;
padding: 5px 0;
font-size: 18px;
line-height: 30px;
.toggle-nav-collapse {
left: 0;
}
.pin-nav-btn {
right: 0;
display: none;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.fa {
transition: transform .15s;
.page-sidebar-pinned & {
transform: rotate(90deg);
}
}
}
}
.nav-header-btn { .nav-header-btn {
padding: 10px $gl-sidebar-padding; padding: 10px $gl-sidebar-padding;
color: inherit; color: inherit;
...@@ -161,59 +29,16 @@ ...@@ -161,59 +29,16 @@
} }
} }
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
}
}
.page-sidebar-pinned {
.content-wrapper,
.layout-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
.merge-request-tabs-holder.affix {
@media (min-width: $sidebar-breakpoint) {
left: $sidebar_width;
}
}
&.right-sidebar-expanded {
.line-resolve-all-container {
@media (min-width: $sidebar-breakpoint) {
display: none;
}
}
}
}
header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
.side-nav-toggle {
display: none;
}
.header-content {
padding-left: 0;
}
}
}
.right-sidebar-collapsed { .right-sidebar-collapsed {
padding-right: 0; padding-right: 0;
@media (min-width: $screen-sm-min) { @media (min-width: $screen-sm-min) {
.content-wrapper { .content-wrapper {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
.merge-request-tabs-holder.affix { .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width; right: $gutter_collapsed_width;
} }
} }
...@@ -231,7 +56,7 @@ header.header-sidebar-pinned { ...@@ -231,7 +56,7 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) { @media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.build-sidebar):not(.wiki-sidebar) { &:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
} }
...@@ -245,12 +70,12 @@ header.header-sidebar-pinned { ...@@ -245,12 +70,12 @@ header.header-sidebar-pinned {
} }
&.with-overlay .merge-request-tabs-holder.affix { &.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width; right: $gutter_collapsed_width;
} }
} }
&.with-overlay { &.with-overlay {
padding-right: $sidebar_collapsed_width; padding-right: $gutter_collapsed_width;
} }
} }
......
/* /*
* Layout * Layout
*/ */
$sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px; $gutter_collapsed_width: 62px;
$gutter_width: 290px; $gutter_width: 290px;
$gutter_inner_width: 250px; $gutter_inner_width: 250px;
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
} }
&.scroll-top { &.scroll-top {
top: 110px; top: 10px;
} }
&.scroll-bottom { &.scroll-bottom {
......
...@@ -253,11 +253,11 @@ ...@@ -253,11 +253,11 @@
display: block; display: block;
} }
width: $sidebar_collapsed_width; width: $gutter_collapsed_width;
padding-top: 0; padding-top: 0;
.block { .block {
width: $sidebar_collapsed_width - 2px; width: $gutter_collapsed_width - 2px;
margin-left: -19px; margin-left: -19px;
padding: 15px 0 0; padding: 15px 0 0;
border-bottom: none; border-bottom: none;
......
...@@ -72,6 +72,7 @@ ul.notes { ...@@ -72,6 +72,7 @@ ul.notes {
overflow: hidden; overflow: hidden;
.system-note-commit-list-toggler { .system-note-commit-list-toggler {
color: $gl-link-color;
display: none; display: none;
padding: 10px 0 0; padding: 10px 0 0;
cursor: pointer; cursor: pointer;
...@@ -107,16 +108,6 @@ ul.notes { ...@@ -107,16 +108,6 @@ ul.notes {
display: none; display: none;
} }
p:last-child {
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
}
}
&::after { &::after {
content: ''; content: '';
width: 100%; width: 100%;
......
.application-theme {
label {
margin-right: 20px;
text-align: center;
.preview {
border-radius: 4px;
height: 80px;
margin-bottom: 10px;
width: 160px;
&.ui_blue {
background: $theme-blue;
}
&.ui_charcoal {
background: $theme-charcoal;
}
&.ui_graphite {
background: $theme-graphite;
}
&.ui_black {
background: $theme-black;
}
&.ui_green {
background: $theme-green;
}
&.ui_violet {
background: $theme-violet;
}
}
}
}
.syntax-theme { .syntax-theme {
label { label {
margin-right: 20px; margin-right: 20px;
......
...@@ -102,6 +102,7 @@ ...@@ -102,6 +102,7 @@
font-size: 24px; font-size: 24px;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
word-wrap: break-word;
.fa { .fa {
margin-left: 2px; margin-left: 2px;
......
...@@ -149,7 +149,7 @@ ...@@ -149,7 +149,7 @@
} }
.commit-actions { .commit-actions {
width: 200px; width: 260px;
} }
} }
......
...@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse, ...@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info, .blob-commit-info,
.file-title, .file-title,
.file-holder, .file-holder,
.sidebar-wrapper,
.nav, .nav,
.btn, .btn,
ul.notes-form, ul.notes-form,
......
...@@ -166,7 +166,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -166,7 +166,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:elasticsearch_search, :elasticsearch_search,
:repository_size_limit, :repository_size_limit,
:shared_runners_minutes, :shared_runners_minutes,
:usage_ping_enabled :usage_ping_enabled,
:minimum_mirror_sync_time
] ]
end end
end end
...@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def update def update
if @runner.update_attributes(runner_params) if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format| respond_to do |format|
format.js format.js
format.html { redirect_to admin_runner_path(@runner) } format.html { redirect_to admin_runner_path(@runner) }
...@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def resume def resume
if @runner.update_attributes(active: true) if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.' redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else else
redirect_to admin_runners_path, alert: 'Runner was not updated.' redirect_to admin_runners_path, alert: 'Runner was not updated.'
...@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController ...@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end end
def pause def pause
if @runner.update_attributes(active: false) if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.' redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else else
redirect_to admin_runners_path, alert: 'Runner was not updated.' redirect_to admin_runners_path, alert: 'Runner was not updated.'
......
...@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider, :provider,
:remember_me, :remember_me,
:skype, :skype,
:theme_id,
:twitter, :twitter,
:username, :username,
:website_url :website_url
......
...@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController ...@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout, :layout,
:dashboard, :dashboard,
:project_view, :project_view,
:theme_id
) )
end end
end end
...@@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController ...@@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController
end end
def apply_diff_view_cookie! def apply_diff_view_cookie!
@show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present? cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end end
......
...@@ -248,6 +248,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -248,6 +248,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format| respond_to do |format|
format.html do format.html do
define_new_vars define_new_vars
@show_changes_tab = true
render "new" render "new"
end end
format.json do format.json do
...@@ -395,10 +396,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -395,10 +396,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def merge_widget_refresh def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' if merge_request.merge_when_build_succeeds
@status = :success
elsif merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds @status = :merge_when_build_succeeds
else
# Only MRs that can be merged end in this action
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
# in last case it does not have any special status. Possible error is handled inside widget js function
@status = :success
end end
render 'merge' render 'merge'
...@@ -674,6 +678,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -674,6 +678,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute @labels = LabelsFinder.new(current_user, project_id: @project.id).execute
@show_changes_tab = params[:show_changes].present?
define_pipelines_vars define_pipelines_vars
end end
......
...@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def update def update
if @runner.update_attributes(runner_params) if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
render 'edit' render 'edit'
...@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def resume def resume
if @runner.update_attributes(active: true) if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
redirect_to runner_path(@runner), alert: 'Runner was not updated.' redirect_to runner_path(@runner), alert: 'Runner was not updated.'
...@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController ...@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end end
def pause def pause
if @runner.update_attributes(active: false) if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.' redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else else
redirect_to runner_path(@runner), alert: 'Runner was not updated.' redirect_to runner_path(@runner), alert: 'Runner was not updated.'
......
...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
end end
def create def create
result = CreateTagService.new(@project, current_user). result = Tags::CreateService.new(@project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description]) execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success if result[:status] == :success
...@@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController ...@@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController
end end
def destroy def destroy
DeleteTagService.new(project, current_user).execute(params[:id]) Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format| respond_to do |format|
format.html do format.html do
......
...@@ -300,4 +300,13 @@ module ApplicationHelper ...@@ -300,4 +300,13 @@ module ApplicationHelper
def page_class def page_class
"issue-boards-page" if current_controller?(:boards) "issue-boards-page" if current_controller?(:boards)
end end
# Returns active css class when condition returns true
# otherwise returns nil.
#
# Example:
# %li{ class: active_when(params[:filter] == '1') }
def active_when(condition)
'active' if condition
end
end end
...@@ -4,4 +4,10 @@ module MirrorHelper ...@@ -4,4 +4,10 @@ module MirrorHelper
message << "<br>To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above." if can?(current_user, :push_code, @project) message << "<br>To discard the local changes and overwrite the branch with the upstream version, delete it here and choose 'Update Now' above." if can?(current_user, :push_code, @project)
message message
end end
def mirror_sync_time_options
Gitlab::Mirror::SYNC_TIME_OPTIONS.select do |key, value|
value >= current_application_settings.minimum_mirror_sync_time
end
end
end end
module NavHelper module NavHelper
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
end
end
def page_gutter_class def page_gutter_class
if current_path?('merge_requests#show') || if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') || current_path?('merge_requests#diffs') ||
...@@ -32,10 +26,6 @@ module NavHelper ...@@ -32,10 +26,6 @@ module NavHelper
class_name = '' class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name class_name
end end
...@@ -46,8 +36,4 @@ module NavHelper ...@@ -46,8 +36,4 @@ module NavHelper
def nav_control_class def nav_control_class
"nav-control" if current_user "nav-control" if current_user
end end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end end
...@@ -34,6 +34,10 @@ module PageLayoutHelper ...@@ -34,6 +34,10 @@ module PageLayoutHelper
end end
end end
def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
end
def page_image def page_image
default = image_url('gitlab_logo.png') default = image_url('gitlab_logo.png')
......
...@@ -41,10 +41,6 @@ module PreferencesHelper ...@@ -41,10 +41,6 @@ module PreferencesHelper
] ]
end end
def user_application_theme
Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class Gitlab::ColorSchemes.for_user(current_user).css_class
end end
......
...@@ -128,6 +128,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -128,6 +128,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :minimum_mirror_sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
validates_each :restricted_visibility_levels do |record, attr, value| validates_each :restricted_visibility_levels do |record, attr, value|
value&.each do |level| value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level) unless Gitlab::VisibilityLevel.options.has_value?(level)
...@@ -155,6 +159,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -155,6 +159,8 @@ class ApplicationSetting < ActiveRecord::Base
before_save :ensure_runners_registration_token before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token before_save :ensure_health_check_access_token
after_update :update_mirror_cron_jobs, if: :minimum_mirror_sync_time_changed?
after_commit do after_commit do
Rails.cache.write(CACHE_KEY, self) Rails.cache.write(CACHE_KEY, self)
end end
...@@ -224,7 +230,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -224,7 +230,8 @@ class ApplicationSetting < ActiveRecord::Base
{ {
elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost', elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost',
elasticsearch_port: ENV['ELASTIC_PORT'] || '9200', elasticsearch_port: ENV['ELASTIC_PORT'] || '9200',
usage_ping_enabled: true usage_ping_enabled: true,
minimum_mirror_sync_time: Gitlab::Mirror::FIFTEEN
} }
end end
...@@ -236,6 +243,15 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -236,6 +243,15 @@ class ApplicationSetting < ActiveRecord::Base
create(defaults) create(defaults)
end end
def update_mirror_cron_jobs
Project.mirror.where('sync_time < ?', minimum_mirror_sync_time).
update_all(sync_time: minimum_mirror_sync_time)
RemoteMirror.where('sync_time < ?', minimum_mirror_sync_time).
update_all(sync_time: minimum_mirror_sync_time)
Gitlab::Mirror.configure_cron_jobs!
end
def elasticsearch_host def elasticsearch_host
read_attribute(:elasticsearch_host).split(',').map(&:strip) read_attribute(:elasticsearch_host).split(',').map(&:strip)
end end
......
...@@ -63,33 +63,10 @@ module Ci ...@@ -63,33 +63,10 @@ module Ci
new_build.save new_build.save
end end
def retry(build, user = nil) def retry(build, current_user)
new_build = Ci::Build.create( Ci::RetryBuildService
ref: build.ref, .new(build.project, current_user)
tag: build.tag, .execute(build)
options: build.options,
commands: build.commands,
tag_list: build.tag_list,
project: build.project,
pipeline: build.pipeline,
name: build.name,
allow_failure: build.allow_failure,
stage: build.stage,
stage_idx: build.stage_idx,
trigger_request: build.trigger_request,
yaml_variables: build.yaml_variables,
when: build.when,
user: user,
environment: build.environment,
status_event: 'enqueue'
)
MergeRequests::AddTodoWhenBuildFailsService
.new(build.project, nil)
.close(new_build)
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end end
end end
...@@ -137,7 +114,7 @@ module Ci ...@@ -137,7 +114,7 @@ module Ci
project.builds_enabled? && commands.present? && manual? && skipped? project.builds_enabled? && commands.present? && manual? && skipped?
end end
def play(current_user = nil) def play(current_user)
# Try to queue a current build # Try to queue a current build
if self.enqueue if self.enqueue
self.update(user: current_user) self.update(user: current_user)
......
...@@ -214,21 +214,17 @@ module Ci ...@@ -214,21 +214,17 @@ module Ci
def cancel_running def cancel_running
Gitlab::OptimisticLocking.retry_lock( Gitlab::OptimisticLocking.retry_lock(
statuses.cancelable) do |cancelable| statuses.cancelable) do |cancelable|
cancelable.each(&:cancel) cancelable.find_each(&:cancel)
end end
end end
def retry_failed(user) def retry_failed(current_user)
Gitlab::OptimisticLocking.retry_lock( Ci::RetryPipelineService.new(project, current_user)
builds.latest.failed_or_canceled) do |failed_or_canceled| .execute(self)
failed_or_canceled.select(&:retryable?).each do |build|
Ci::Build.retry(build, user)
end
end
end end
def mark_as_processable_after_stage(stage_idx) def mark_as_processable_after_stage(stage_idx)
builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process) builds.skipped.after_stage(stage_idx).find_each(&:process)
end end
def latest? def latest?
......
...@@ -22,8 +22,6 @@ module Ci ...@@ -22,8 +22,6 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) } scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) } scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
...@@ -40,6 +38,8 @@ module Ci ...@@ -40,6 +38,8 @@ module Ci
acts_as_taggable acts_as_taggable
after_destroy :cleanup_runner_queue
# Searches for runners matching the given query. # Searches for runners matching the given query.
# #
# This method uses ILIKE on PostgreSQL and LIKE on MySQL. # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
...@@ -147,14 +147,14 @@ module Ci ...@@ -147,14 +147,14 @@ module Ci
private private
def runner_queue_key def cleanup_runner_queue
"runner:build_queue:#{self.token}" Gitlab::Redis.with do |redis|
redis.del(runner_queue_key)
end
end end
def form_editable_changed? def runner_queue_key
FORM_EDITABLE.any? do |editable| "runner:build_queue:#{self.token}"
public_send("#{editable}_changed?")
end
end end
def tag_constraints def tag_constraints
......
...@@ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base ...@@ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id)) where(id: max_id.group(:name, :commit_id))
end end
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :failed_but_allowed, -> do scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled]) where(allow_failure: true, status: [:failed, :canceled])
end end
...@@ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base ...@@ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base
false, all_state_names - [:failed, :canceled]) false, all_state_names - [:failed, :canceled])
end end
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) } scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) } scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
state_machine :status do state_machine :status do
event :enqueue do event :enqueue do
......
...@@ -43,7 +43,7 @@ class Namespace < ActiveRecord::Base ...@@ -43,7 +43,7 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy # Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir after_destroy :rm_dir
scope :root, -> { where('type IS NULL') } scope :root, -> { where('type IS NULL') }
...@@ -216,6 +216,14 @@ class Namespace < ActiveRecord::Base ...@@ -216,6 +216,14 @@ class Namespace < ActiveRecord::Base
parent_id_changed? parent_id_changed?
end end
def prepare_for_destroy
old_repository_storage_paths
end
def old_repository_storage_paths
@old_repository_storage_paths ||= repository_storage_paths
end
private private
def repository_storage_paths def repository_storage_paths
...@@ -229,7 +237,7 @@ class Namespace < ActiveRecord::Base ...@@ -229,7 +237,7 @@ class Namespace < ActiveRecord::Base
def rm_dir def rm_dir
# Remove the namespace directory in all storages paths used by member projects # Remove the namespace directory in all storages paths used by member projects
@old_repository_storage_paths.each do |repository_storage_path| old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash. # Move namespace directory into trash.
# We will remove it later async # We will remove it later async
new_path = "#{path}+#{id}+deleted" new_path = "#{path}+#{id}+deleted"
......
...@@ -218,7 +218,7 @@ class Project < ActiveRecord::Base ...@@ -218,7 +218,7 @@ class Project < ActiveRecord::Base
validates :sync_time, validates :sync_time,
presence: true, presence: true,
inclusion: { in: Gitlab::Mirror.sync_time_options.values } inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
with_options if: :mirror? do |project| with_options if: :mirror? do |project|
project.validates :import_url, presence: true project.validates :import_url, presence: true
...@@ -234,6 +234,8 @@ class Project < ActiveRecord::Base ...@@ -234,6 +234,8 @@ class Project < ActiveRecord::Base
# Scopes # Scopes
default_scope { where(pending_delete: false) } default_scope { where(pending_delete: false) }
scope :with_deleted, -> { unscope(where: :pending_delete) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
......
...@@ -30,5 +30,9 @@ module ChatMessage ...@@ -30,5 +30,9 @@ module ChatMessage
def attachment_color def attachment_color
'#345' '#345'
end end
def link(text, url)
"[#{text}](#{url})"
end
end end
end end
...@@ -7,7 +7,11 @@ module ChatMessage ...@@ -7,7 +7,11 @@ module ChatMessage
attr_reader :project_name attr_reader :project_name
attr_reader :project_url attr_reader :project_url
attr_reader :user_name attr_reader :user_name
attr_reader :user_url
attr_reader :duration attr_reader :duration
attr_reader :stage
attr_reader :build_id
attr_reader :build_name
def initialize(params) def initialize(params)
@sha = params[:sha] @sha = params[:sha]
...@@ -17,7 +21,11 @@ module ChatMessage ...@@ -17,7 +21,11 @@ module ChatMessage
@project_url = params[:project_url] @project_url = params[:project_url]
@status = params[:commit][:status] @status = params[:commit][:status]
@user_name = params[:commit][:author_name] @user_name = params[:commit][:author_name]
@user_url = params[:commit][:author_url]
@duration = params[:commit][:duration] @duration = params[:commit][:duration]
@stage = params[:build_stage]
@build_name = params[:build_name]
@build_id = params[:build_id]
end end
def pretext def pretext
...@@ -35,7 +43,19 @@ module ChatMessage ...@@ -35,7 +43,19 @@ module ChatMessage
private private
def message def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}" "#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
end
def build_url
"#{project_url}/builds/#{build_id}"
end
def build_link
link(build_name, build_url)
end
def user_link
link(user_name, user_url)
end end
def format(string) def format(string)
...@@ -64,11 +84,11 @@ module ChatMessage ...@@ -64,11 +84,11 @@ module ChatMessage
end end
def branch_link def branch_link
"[#{ref}](#{branch_url})" link(ref, branch_url)
end end
def project_link def project_link
"[#{project_name}](#{project_url})" link(project_name, project_url)
end end
def commit_url def commit_url
...@@ -76,7 +96,7 @@ module ChatMessage ...@@ -76,7 +96,7 @@ module ChatMessage
end end
def commit_link def commit_link
"[#{Commit.truncate_sha(sha)}](#{commit_url})" link(Commit.truncate_sha(sha), commit_url)
end end
end end
end end
...@@ -55,11 +55,11 @@ module ChatMessage ...@@ -55,11 +55,11 @@ module ChatMessage
end end
def project_link def project_link
"[#{project_name}](#{project_url})" link(project_name, project_url)
end end
def issue_link def issue_link
"[#{issue_title}](#{issue_url})" link(issue_title, issue_url)
end end
def issue_title def issue_title
......
...@@ -43,7 +43,7 @@ module ChatMessage ...@@ -43,7 +43,7 @@ module ChatMessage
end end
def project_link def project_link
"[#{project_name}](#{project_url})" link(project_name, project_url)
end end
def merge_request_message def merge_request_message
...@@ -51,7 +51,7 @@ module ChatMessage ...@@ -51,7 +51,7 @@ module ChatMessage
end end
def merge_request_link def merge_request_link
"[merge request !#{merge_request_id}](#{merge_request_url})" link("merge request !#{merge_request_id}", merge_request_url)
end end
def merge_request_url def merge_request_url
......
...@@ -3,10 +3,9 @@ module ChatMessage ...@@ -3,10 +3,9 @@ module ChatMessage
attr_reader :message attr_reader :message
attr_reader :user_name attr_reader :user_name
attr_reader :project_name attr_reader :project_name
attr_reader :project_link attr_reader :project_url
attr_reader :note attr_reader :note
attr_reader :note_url attr_reader :note_url
attr_reader :title
def initialize(params) def initialize(params)
params = HashWithIndifferentAccess.new(params) params = HashWithIndifferentAccess.new(params)
...@@ -69,15 +68,15 @@ module ChatMessage ...@@ -69,15 +68,15 @@ module ChatMessage
end end
def description_message def description_message
[{ text: format(@note), color: attachment_color }] [{ text: format(note), color: attachment_color }]
end end
def project_link def project_link
"[#{@project_name}](#{@project_url})" link(project_name, project_url)
end end
def commented_on_message(target, title) def commented_on_message(target, title)
@message = "#{@user_name} [commented on #{target}](#{@note_url}) in #{project_link}: *#{title}*" @message = "#{user_name} #{link('commented on ' + target, note_url)} in #{project_link}: *#{title}*"
end end
end end
end end
# TODO(ayufan): The GitLabCiService is deprecated and the type should be removed when the database entries are removed
class GitlabCiService < CiService
# We override the active accessor to always make GitLabCiService disabled
# Otherwise the GitLabCiService can be picked, but should never be since it's deprecated
def active
false
end
end
...@@ -173,6 +173,10 @@ class ProjectWiki ...@@ -173,6 +173,10 @@ class ProjectWiki
} }
end end
def repository_storage_path
project.repository_storage_path
end
private private
def init_repo(path_with_namespace) def init_repo(path_with_namespace)
......
...@@ -14,7 +14,7 @@ class RemoteMirror < ActiveRecord::Base ...@@ -14,7 +14,7 @@ class RemoteMirror < ActiveRecord::Base
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true } validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validates :sync_time, validates :sync_time,
presence: true, presence: true,
inclusion: { in: Gitlab::Mirror.sync_time_options.values } inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? } validate :url_availability, if: -> (mirror) { mirror.url_changed? || mirror.enabled? }
......
...@@ -27,7 +27,7 @@ class Service < ActiveRecord::Base ...@@ -27,7 +27,7 @@ class Service < ActiveRecord::Base
validates :project_id, presence: true, unless: Proc.new { |service| service.template? } validates :project_id, presence: true, unless: Proc.new { |service| service.template? }
scope :visible, -> { where.not(type: ['GitlabIssueTrackerService', 'GitlabCiService']) } scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') }
scope :issue_trackers, -> { where(category: 'issue_tracker') } scope :issue_trackers, -> { where(category: 'issue_tracker') }
scope :external_wikis, -> { where(type: 'ExternalWikiService').active } scope :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
......
...@@ -23,7 +23,6 @@ class User < ActiveRecord::Base ...@@ -23,7 +23,6 @@ class User < ActiveRecord::Base
default_value_for :can_create_team, false default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret, attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base, key: Gitlab::Application.secrets.otp_key_base,
......
class BaseService class BaseService
include Gitlab::Allowable
include Gitlab::CurrentSettings include Gitlab::CurrentSettings
attr_accessor :project, :current_user, :params attr_accessor :project, :current_user, :params
...@@ -7,10 +8,6 @@ class BaseService ...@@ -7,10 +8,6 @@ class BaseService
@project, @current_user, @params = project, user, params.dup @project, @current_user, @params = project, user, params.dup
end end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
end
def notification_service def notification_service
NotificationService.new NotificationService.new
end end
......
module Ci
class RetryBuildService < ::BaseService
CLONE_ATTRIBUTES = %i[pipeline ref tag options commands tag_list name
allow_failure stage stage_idx trigger_request
yaml_variables when environment coverage_regex]
.freeze
REJECT_ATTRIBUTES = %i[id status user token coverage trace runner
artifacts_file artifacts_metadata artifacts_size
created_at updated_at started_at finished_at
queued_at erased_by erased_at].freeze
IGNORE_ATTRIBUTES = %i[trace type lock_version project target_url
deploy job_id description].freeze
def execute(build)
reprocess(build).tap do |new_build|
build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build.enqueue!
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close(new_build)
end
end
def reprocess(build)
unless can?(current_user, :update_build, build)
raise Gitlab::Access::AccessDeniedError
end
attributes = CLONE_ATTRIBUTES.map do |attribute|
[attribute, build.send(attribute)]
end
attributes.push([:user, current_user])
project.builds.create(Hash[attributes])
end
end
end
module Ci
class RetryPipelineService < ::BaseService
def execute(pipeline)
unless can?(current_user, :update_pipeline, pipeline)
raise Gitlab::Access::AccessDeniedError
end
pipeline.builds.failed_or_canceled.find_each do |build|
next unless build.retryable?
Ci::RetryBuildService.new(project, current_user)
.reprocess(build)
end
MergeRequests::AddTodoWhenBuildFailsService
.new(project, current_user)
.close_all(pipeline)
pipeline.process!
end
end
end
module Ci
class UpdateRunnerService
attr_reader :runner
def initialize(runner)
@runner = runner
end
def update(params)
runner.update(params).tap do |updated|
runner.tick_runner_queue if updated
end
end
end
end
class CreateTagService < BaseService
def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag
repository = project.repository
message&.strip!
new_tag = nil
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError
return error("Tag #{tag_name} already exists")
rescue GitHooksService::PreReceiveError => ex
return error(ex.message)
end
if new_tag
if release_description
CreateReleaseService.new(@project, @current_user).
execute(tag_name, release_description)
end
success.merge(tag: new_tag)
else
error("Target #{target} is invalid")
end
end
end
class DeleteTagService < BaseService
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
unless tag
return error('No such tag', 404)
end
if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
release&.destroy
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
success('Tag was removed')
else
error('Failed to remove tag')
end
end
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
end
def success(message)
super().merge(message: message)
end
def build_push_data(tag)
Gitlab::DataBuilder::Push.build(
project,
current_user,
tag.dereferenced_target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
[])
end
end
module Geo
class ScheduleRepoCreateService
attr_reader :id
def initialize(params)
@id = params['project_id']
end
def execute
GeoRepositoryCreateWorker.perform_async(id)
end
end
end
...@@ -8,7 +8,9 @@ module Groups ...@@ -8,7 +8,9 @@ module Groups
end end
def execute def execute
group.projects.each do |project| group.prepare_for_destroy
group.projects.with_deleted.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup. # Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all these repositories # that contain all these repositories
......
...@@ -18,5 +18,11 @@ module MergeRequests ...@@ -18,5 +18,11 @@ module MergeRequests
todo_service.merge_request_build_retried(merge_request) todo_service.merge_request_build_retried(merge_request)
end end
end end
def close_all(pipeline)
pipeline_merge_requests(pipeline) do |merge_request|
todo_service.merge_request_build_retried(merge_request)
end
end
end end
end end
module Tags
class CreateService < BaseService
def execute(tag_name, target, message, release_description = nil)
valid_tag = Gitlab::GitRefValidator.validate(tag_name)
return error('Tag name invalid') unless valid_tag
repository = project.repository
message&.strip!
new_tag = nil
begin
new_tag = repository.add_tag(current_user, tag_name, target, message)
rescue Rugged::TagError
return error("Tag #{tag_name} already exists")
rescue GitHooksService::PreReceiveError => ex
return error(ex.message)
end
if new_tag
if release_description
CreateReleaseService.new(@project, @current_user).
execute(tag_name, release_description)
end
success.merge(tag: new_tag)
else
error("Target #{target} is invalid")
end
end
end
end
module Tags
class DestroyService < BaseService
def execute(tag_name)
repository = project.repository
tag = repository.find_tag(tag_name)
unless tag
return error('No such tag', 404)
end
if repository.rm_tag(current_user, tag_name)
release = project.releases.find_by(tag: tag_name)
release&.destroy
push_data = build_push_data(tag)
EventCreateService.new.push(project, current_user, push_data)
project.execute_hooks(push_data.dup, :tag_push_hooks)
project.execute_services(push_data.dup, :tag_push_hooks)
success('Tag was removed')
else
error('Failed to remove tag')
end
end
def error(message, return_code = 400)
super(message).merge(return_code: return_code)
end
def success(message)
super().merge(message: message)
end
def build_push_data(tag)
Gitlab::DataBuilder::Push.build(
project,
current_user,
tag.dereferenced_target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
[])
end
end
end
...@@ -36,7 +36,7 @@ class FileUploader < GitlabUploader ...@@ -36,7 +36,7 @@ class FileUploader < GitlabUploader
escaped_filename = filename.gsub("]", "\\]") escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})" markdown = "[#{escaped_filename}](#{self.secure_url})"
markdown.prepend("!") if image_or_video? markdown.prepend("!") if image_or_video? || dangerous?
{ {
alt: filename, alt: filename,
......
# Extra methods for uploader # Extra methods for uploader
module UploaderHelper module UploaderHelper
IMAGE_EXT = %w[png jpg jpeg gif bmp tiff svg] IMAGE_EXT = %w[png jpg jpeg gif bmp tiff]
# We recommend using the .mp4 format over .mov. Videos in .mov format can # We recommend using the .mp4 format over .mov. Videos in .mov format can
# still be used but you really need to make sure they are served with the # still be used but you really need to make sure they are served with the
# proper MIME type video/mp4 and not video/quicktime or your videos won't play # proper MIME type video/mp4 and not video/quicktime or your videos won't play
# on IE >= 9. # on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html # http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
VIDEO_EXT = %w[mp4 m4v mov webm ogv] VIDEO_EXT = %w[mp4 m4v mov webm ogv]
# These extension types can contain dangerous code and should only be embedded inline with
# proper filtering. They should always be tagged as "Content-Disposition: attachment", not "inline".
DANGEROUS_EXT = %w[svg]
def image? def image?
extension_match?(IMAGE_EXT) extension_match?(IMAGE_EXT)
...@@ -20,6 +23,10 @@ module UploaderHelper ...@@ -20,6 +23,10 @@ module UploaderHelper
image? || video? image? || video?
end end
def dangerous?
extension_match?(DANGEROUS_EXT)
end
def extension_match?(extensions) def extension_match?(extensions)
return false unless file return false unless file
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
%th.wide Message %th.wide Message
%th Action %th Action
= render @abuse_reports = render @abuse_reports
= paginate @abuse_reports, theme: 'gitlab'
- else - else
.empty-state .empty-state
.text-center .text-center
......
...@@ -71,6 +71,10 @@ ...@@ -71,6 +71,10 @@
%span.help-block#repository_size_limit_help_block %span.help-block#repository_size_limit_help_block
Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited. Includes LFS objects. It can be overridden per group, or per project. 0 for unlimited.
= link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings") = link_to icon('question-circle'), help_page_path("user/admin_area/settings/account_and_limit_settings")
.form-group
= f.label :minimum_mirror_sync_time, class: 'control-label col-sm-2'
.col-sm-10
= f.select :minimum_mirror_sync_time, options_for_select(Gitlab::Mirror::SYNC_TIME_OPTIONS, @application_setting.minimum_mirror_sync_time), {}, class: 'form-control'
.form-group .form-group
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2' = f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
...@@ -205,7 +209,7 @@ ...@@ -205,7 +209,7 @@
= f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2' = f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
.col-sm-10 .col-sm-10
= f.number_field :max_pages_size, class: 'form-control' = f.number_field :max_pages_size, class: 'form-control'
.help-block Zero for unlimited .help-block 0 for unlimited
%fieldset %fieldset
%legend Continuous Integration %legend Continuous Integration
...@@ -581,7 +585,7 @@ ...@@ -581,7 +585,7 @@
= f.number_field :terminal_max_session_time, class: 'form-control' = f.number_field :terminal_max_session_time, class: 'form-control'
.help-block .help-block
Maximum time for web terminal websocket connection (in seconds). Maximum time for web terminal websocket connection (in seconds).
Set to 0 for unlimited time. 0 for unlimited.
.form-actions .form-actions
= f.submit 'Save', class: 'btn btn-save' = f.submit 'Save', class: 'btn btn-save'
...@@ -8,15 +8,14 @@ ...@@ -8,15 +8,14 @@
%div{ class: container_class } %div{ class: container_class }
%ul.nav-links.log-tabs %ul.nav-links.log-tabs
- loggers.each do |klass| - loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> %li{ class: active_when(klass == Gitlab::GitLogger) }>
= link_to klass::file_name, "##{klass::file_name_noext}", = link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab' 'data-toggle' => 'tab'
.row-content-block .row-content-block
To prevent performance issues admin logs output the last 2000 lines To prevent performance issues admin logs output the last 2000 lines
.tab-content .tab-content
- loggers.each do |klass| - loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
id: klass::file_name_noext }
.file-holder#README .file-holder#README
.js-file-title.file-title .js-file-title.file-title
%i.fa.fa-file %i.fa.fa-file
......
...@@ -48,13 +48,13 @@ ...@@ -48,13 +48,13 @@
= link_to admin_projects_path do = link_to admin_projects_path do
All All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public Public
......
...@@ -22,7 +22,7 @@ ...@@ -22,7 +22,7 @@
%td %td
#{runner.builds.count(:all)} #{runner.builds.count(:all)}
%td %td
- runner.tag_list.each do |tag| - runner.tag_list.sort.each do |tag|
%span.label.label-primary %span.label.label-primary
= tag = tag
%td %td
......
...@@ -39,31 +39,31 @@ ...@@ -39,31 +39,31 @@
.nav-block .nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.fade-left .fade-left
= nav_link(html_options: { class: ('active' unless params[:filter]) }) do = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do = link_to admin_users_path do
Active Active
%small.badge= number_with_delimiter(User.active.count) %small.badge= number_with_delimiter(User.active.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do = link_to admin_users_path(filter: "admins") do
Admins Admins
%small.badge= number_with_delimiter(User.admins.count) %small.badge= number_with_delimiter(User.admins.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do = link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled 2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count) %small.badge= number_with_delimiter(User.with_two_factor.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do = link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled 2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count) %small.badge= number_with_delimiter(User.without_two_factor.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do = link_to admin_users_path(filter: 'external') do
External External
%small.badge= number_with_delimiter(User.external.count) %small.badge= number_with_delimiter(User.external.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do = link_to admin_users_path(filter: "blocked") do
Blocked Blocked
%small.badge= number_with_delimiter(User.blocked.count) %small.badge= number_with_delimiter(User.blocked.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do = link_to admin_users_path(filter: "wop") do
Without projects Without projects
%small.badge= number_with_delimiter(User.without_projects.count) %small.badge= number_with_delimiter(User.without_projects.count)
......
.top-area .top-area
%ul.nav-links %ul.nav-links
%li{ class: ("active" unless params[:filter]) }> %li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects Your Projects
%li{ class: ("active" if params[:filter] == 'starred') }> %li{ class: active_when(params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects Starred Projects
...@@ -4,15 +4,13 @@ ...@@ -4,15 +4,13 @@
- if current_user.todos.any? - if current_user.todos.any?
.top-area .top-area
%ul.nav-links %ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
%li{ class: "todos-pending #{todo_pending_active}" }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
To do To do
%span.badge %span.badge
= number_with_delimiter(todos_pending_count) = number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done') %li.todos-done{ class: active_when(params[:state] == 'done') }>
%li{ class: "todos-done #{todo_done_active}" }>
= link_to todos_filter_path(state: 'done') do = link_to todos_filter_path(state: 'done') do
%span %span
Done Done
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= render 'devise/sessions/new_kerberos' = render 'devise/sessions/new_kerberos'
- @ldap_servers.each_with_index do |server, i| - @ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) } .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body .login-body
= render 'devise/sessions/new_ldap', server: server = render 'devise/sessions/new_ldap', server: server
- if signin_enabled? - if signin_enabled?
......
...@@ -6,8 +6,8 @@ ...@@ -6,8 +6,8 @@
%li{ class: (:active unless crowd_enabled? || ldap_enabled?) } %li{ class: (:active unless crowd_enabled? || ldap_enabled?) }
= link_to "Kerberos", "#kerberos", 'data-toggle' => 'tab' = link_to "Kerberos", "#kerberos", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i| - @ldap_servers.each_with_index do |server, i|
%li{ class: (:active if i.zero? && !crowd_enabled?) } %li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled? - if signin_enabled?
%li %li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab' = link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= link_to filter_projects_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
Any Any
- Gitlab::VisibilityLevel.values.each do |level| - Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } %li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' }
= link_to filter_projects_path(visibility_level: level) do = link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level) = visibility_level_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
Any Any
- @tags.each do |tag| - @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } %li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do = link_to filter_projects_path(tag: tag.name) do
= icon('tag') = icon('tag')
= tag.name = tag.name
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
-# total_pages: total number of pages -# total_pages: total number of pages
-# per_page: number of items to fetch per page -# per_page: number of items to fetch per page
-# remote: data-remote -# remote: data-remote
%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" } %li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
= link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%title= page_title(site_name) %title= page_title(site_name)
%meta{ name: "description", content: page_description } %meta{ name: "description", content: page_description }
= favicon_link_tag 'favicon.ico' = favicon_link_tag favicon
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
......
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" } .page-with-sidebar{ class: page_gutter_class }
.sidebar-wrapper.nicescroll
.sidebar-action-buttons
.nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
%span.sr-only Toggle navigation
= icon('bars')
%div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
%span.sr-only Toggle navigation pinning
= icon('fw thumb-tack')
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
= render 'layouts/nav/dashboard'
- else
= render 'layouts/nav/explore'
- if defined?(nav) && nav - if defined?(nav) && nav
.layout-nav .layout-nav
.container-fluid .container-fluid
......
!!! 5 !!! 5
%html{ lang: "en", class: "#{page_class}" } %html{ lang: "en", class: "#{page_class}" }
= render "layouts/head" = render "layouts/head"
%body{ class: "#{user_application_theme}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } } %body{ data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}" } }
= Gon::Base.render_data = Gon::Base.render_data
= render "layouts/header/default", title: header_title = render "layouts/header/default", title: header_title
......
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.
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