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].
### Retrospective
After each release (usually on the 22nd of each month), we have a retrospective
call where we discuss what went well, what went wrong, and what we can improve
for the next release. The [retrospective notes] are public and you are invited
to comment them.
If you're interested, you can even join the [retrospective call][retro-kickoff-call].
After each release, we have a retrospective call where we discuss what went well,
what went wrong, and what we can improve for the next release. The
[retrospective notes] are public and you are invited to comment on them.
If you're interested, you can even join the
[retrospective call][retro-kickoff-call], on the first working day after the
22nd at 6pm CET / 9am PST.
### 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 notes] are public and you are invited to comment them.
If you're interested, you can even join the [kickoff call][retro-kickoff-call].
[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],
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
[kickoff notes]: https://docs.google.com/document/d/1ElPkZ90A8ey_iOkTvUs_ByMlwKK6NAB2VOK5835wYK0/edit?usp=sharing
......
......@@ -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 () {
return $(this).select().one('mouseup', function (e) {
return e.preventDefault();
......@@ -250,8 +245,6 @@ require('es6-promise').polyfill();
});
gl.awardsHandler = new AwardsHandler();
new Aside();
// bind sidebar events
new gl.Sidebar();
gl.utils.initTimeagoTimeout();
});
......
......@@ -97,7 +97,7 @@ $(() => {
},
computed: {
disabled() {
return Store.shouldAddBlankState();
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
},
},
template: `
......
......@@ -4,6 +4,7 @@
*
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
require('../../vue_realtime_listener');
class PipelinesStore {
constructor() {
......@@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
static startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
......@@ -44,7 +45,4 @@ class PipelinesStore {
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
module.exports = PipelinesStore;
......@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service');
require('./pipelines_store');
const PipelineStore = require('./pipelines_store');
/**
*
......@@ -41,7 +40,7 @@ require('./pipelines_store');
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').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.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
......@@ -71,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json())
.then((json) => {
this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false;
})
.catch(() => {
......@@ -80,9 +78,15 @@ require('./pipelines_store');
});
},
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
},
template: `
<div>
<div class="pipelines realtime-loading" v-if="isLoading">
<div class="pipelines">
<div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
......
......@@ -121,6 +121,7 @@ const ShortcutsBlob = require('./shortcuts_blob');
new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:new_diffs':
case 'projects:merge_requests:edit':
new gl.Diff();
shortcut_handler = new ShortcutsNavigation();
......
......@@ -48,7 +48,7 @@
const calendar = new Pikaday({
field: $dueDateInput.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
......@@ -63,6 +63,7 @@
}
});
calendar.setDate(new Date($dueDateInput.val()));
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
}
......@@ -169,11 +170,12 @@
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
calendar.setDate(new Date($datePicker.val()));
$datePicker.data('pikaday', calendar);
});
......
......@@ -8,6 +8,7 @@ const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', {
......
......@@ -259,6 +259,11 @@ module.exports = Vue.component('environment-item', {
return undefined;
},
terminalIconSvg: {
type: String,
required: false,
},
/**
* If provided, returns the commit tag.
*
......@@ -436,7 +441,6 @@ module.exports = Vue.component('environment-item', {
return true;
// return this.model.rollout_status;
},
},
/**
......@@ -457,6 +461,7 @@ module.exports = Vue.component('environment-item', {
template: `
<tr>
<td>
<span class="deploy-board-icon"
v-if="!model.isFolder"
v-on:click="toggleDeployBoard(model)">
......
......@@ -4,7 +4,6 @@
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
const DeployBoard = require('./deploy_board_component');
module.exports = Vue.component('environment-table-component', {
components: {
......
const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
......
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
const Vue = require('vue');
Vue.use(require('vue-resource'));
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', {
......
......@@ -103,6 +103,9 @@
this.input.each((i, input) => {
const $input = $(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) {
......
......@@ -42,11 +42,12 @@
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
calendar.setDate(new Date($issuableDueDate.val()));
}
}
......
......@@ -3,7 +3,7 @@
require('./flash');
require('vendor/jquery.waitforimages');
require('vendor/task_list');
require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
......@@ -11,10 +11,16 @@ require('vendor/task_list');
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
// Prevent duplicate event bindings
this.disableTaskList();
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.initMergeRequests();
......@@ -22,11 +28,6 @@ require('vendor/task_list');
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() {
var _this, issueFailMessage;
_this = this;
......@@ -54,16 +55,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text());
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1);
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
}
} else {
new Flash(issueFailMessage, 'alert');
......@@ -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() {
var $container;
$container = $('#merge-requests');
......
......@@ -19,7 +19,7 @@
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
minDate: new Date(),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
......@@ -30,6 +30,7 @@
},
});
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
});
......
......@@ -2,7 +2,7 @@
/* global MergeRequestTabs */
require('vendor/jquery.waitforimages');
require('vendor/task_list');
require('./task_list');
require('./merge_request_tabs');
(function() {
......@@ -24,12 +24,18 @@ require('./merge_request_tabs');
};
})(this));
this.initTabs();
// Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
this.initCommitMessageListeners();
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');
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() {
var _this;
_this = this;
......@@ -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() {
$(document).on('click', 'a.js-with-description-link', function(e) {
var textarea = $('textarea.js-commit-message');
......
......@@ -103,9 +103,10 @@ require('./flash');
}
clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href');
if (e.currentTarget && gl.utils.isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation();
e.preventDefault();
window.open(targetLink, '_blank');
}
}
......
......@@ -11,7 +11,7 @@ require('./dropzone_input');
require('./gfm_auto_complete');
require('vendor/jquery.caret'); // required by jquery.atwho
require('vendor/jquery.atwho');
require('vendor/task_list');
require('./task_list');
(function() {
var bind = function(fn, me) { return function() { return fn.apply(me, arguments); }; };
......@@ -51,7 +51,11 @@ require('vendor/task_list');
this.addBinding();
this.setPollingInterval();
this.setupMainTargetNoteForm();
this.initTaskList();
this.taskList = new gl.TaskList({
dataType: 'note',
fieldName: 'note',
selector: '.notes'
});
this.collapseLongCommitList();
// We are in the Merge Requests page so we need another edit form for Changes tab
......@@ -125,8 +129,6 @@ require('vendor/task_list');
$(document).off("keydown", ".js-note-text");
$(document).off('click', '.js-comment-resolve-button');
$(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) {
......@@ -286,7 +288,7 @@ require('vendor/task_list');
// Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.collapseLongCommitList();
this.initTaskList();
this.taskList.init();
this.refresh();
return this.updateNotesCount(1);
}
......@@ -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) {
var $editForm = $(this.getEditFormSelector($el));
var $note = $el.closest('.note');
......@@ -896,17 +889,6 @@ require('vendor/task_list');
$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) {
return this.notesCountBadge.text(parseInt(this.notesCountBadge.text(), 10) + updateCount);
};
......@@ -923,9 +905,10 @@ require('vendor/task_list');
};
Notes.prototype.toggleCommitList = function(e) {
const $element = $(e.target);
const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
};
......
......@@ -38,13 +38,15 @@
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
for (const button of this.$buttons) {
this.$buttons.map((button) => {
const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
}
}
return button;
});
});
}
}
......
......@@ -21,11 +21,16 @@
};
Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.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;
e.preventDefault();
$this = $(this);
......@@ -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() {
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 @@
goToTodoUrl(e) {
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) {
return;
}
if (gl.utils.isMetaClick(e)) {
const windowTarget = '_blank';
const selected = e.target;
e.preventDefault();
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually.
if (targetLink !== todoLink) {
return window.open(targetLink, '_blank');
if (selected.tagName === 'IMG') {
const avatarUrl = selected.parentElement.getAttribute('href');
return window.open(avatarUrl, windowTarget);
} else {
return window.open(todoLink, '_blank');
return window.open(todoLink, windowTarget);
}
} else {
return gl.utils.visitUrl(todoLink);
......
......@@ -62,6 +62,7 @@
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
download
:href='artifact.path'
>
<i class="fa fa-download" aria-hidden="true"></i>
......
......@@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination');
require('./store');
require('../vue_shared/components/pipelines_table');
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => {
gl.VuePipelines = Vue.extend({
......@@ -28,24 +29,34 @@ require('../vue_shared/components/pipelines_table');
},
props: ['scope', 'store', 'svgs'],
created() {
const pagenum = gl.utils.getParameterByName('p');
const pagenum = gl.utils.getParameterByName('page');
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope;
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.
*
* If no scope is provided, 'all' is assumed.
*
* Pagination component sends "null" when no scope is provided.
*
* @param {Number} pagenum
* @param {String} apiScope = 'all'
*/
change(pagenum, apiScope = 'all') {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
change(pagenum, apiScope) {
if (!apiScope) apiScope = 'all';
gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
},
},
template: `
......
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
require('../vue_realtime_listener');
/* eslint-disable no-param-reassign */
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.normalizeHeaders(normalized);
const paginationInfo = gl.utils.parseIntPagination(normalized);
return paginationInfo;
};
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
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 = () =>
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
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);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
}
};
})(window.gl || (window.gl = {}));
......@@ -19,7 +19,6 @@
@import "framework/flash.scss";
@import "framework/forms.scss";
@import "framework/gfm.scss";
@import "framework/gitlab-theme.scss";
@import "framework/header.scss";
@import "framework/highlight.scss";
@import "framework/issue_box.scss";
......
......@@ -116,7 +116,7 @@
}
.btn,
.side-nav-toggle {
.global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow);
}
......@@ -140,7 +140,6 @@ a {
@include transition(background-color, box-shadow);
}
.nav-sidebar a,
.dropdown-menu a,
.dropdown-menu button,
.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 {
}
}
}
}
.side-nav-toggle {
position: absolute;
left: -10px;
margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $gray-light;
.global-dropdown {
position: absolute;
left: -10px;
.badge {
font-size: 11px;
}
li {
.active a {
font-weight: bold;
}
&:hover {
background-color: $white-normal;
color: $gl-header-nav-hover-color;
.badge {
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 {
position: relative;
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 {
width: 100%;
transition: padding $sidebar-transition-duration;
......@@ -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 {
padding: 10px $gl-sidebar-padding;
color: inherit;
......@@ -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 {
padding-right: 0;
@media (min-width: $screen-sm-min) {
.content-wrapper {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
.merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
right: $gutter_collapsed_width;
}
}
......@@ -231,7 +56,7 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
}
......@@ -245,12 +70,12 @@ header.header-sidebar-pinned {
}
&.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
right: $gutter_collapsed_width;
}
}
&.with-overlay {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
}
......
/*
* Layout
*/
$sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
......
......@@ -91,7 +91,7 @@
}
&.scroll-top {
top: 110px;
top: 10px;
}
&.scroll-bottom {
......
......@@ -253,11 +253,11 @@
display: block;
}
width: $sidebar_collapsed_width;
width: $gutter_collapsed_width;
padding-top: 0;
.block {
width: $sidebar_collapsed_width - 2px;
width: $gutter_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;
......
......@@ -72,6 +72,7 @@ ul.notes {
overflow: hidden;
.system-note-commit-list-toggler {
color: $gl-link-color;
display: none;
padding: 10px 0 0;
cursor: pointer;
......@@ -107,16 +108,6 @@ ul.notes {
display: none;
}
p:last-child {
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
}
}
&::after {
content: '';
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 {
label {
margin-right: 20px;
......
......@@ -102,6 +102,7 @@
font-size: 24px;
font-weight: 400;
line-height: 1;
word-wrap: break-word;
.fa {
margin-left: 2px;
......
......@@ -149,7 +149,7 @@
}
.commit-actions {
width: 200px;
width: 260px;
}
}
......
......@@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info,
.file-title,
.file-holder,
.sidebar-wrapper,
.nav,
.btn,
ul.notes-form,
......
......@@ -166,7 +166,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:elasticsearch_search,
:repository_size_limit,
:shared_runners_minutes,
:usage_ping_enabled
:usage_ping_enabled,
:minimum_mirror_sync_time
]
end
end
......@@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
if @runner.update_attributes(runner_params)
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
......@@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
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.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
......@@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
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.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
......
......@@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url
......
......@@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout,
:dashboard,
:project_view,
:theme_id
)
end
end
......@@ -83,7 +83,6 @@ class Projects::ApplicationController < ApplicationController
end
def apply_diff_view_cookie!
@show_changes_tab = params[:view].present?
cookies.permanent[:diff_view] = params.delete(:view) if params[:view].present?
end
......
......@@ -248,6 +248,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
respond_to do |format|
format.html do
define_new_vars
@show_changes_tab = true
render "new"
end
format.json do
......@@ -395,10 +396,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
@status = :success
elsif merge_request.merge_when_build_succeeds
if merge_request.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
render 'merge'
......@@ -674,6 +678,8 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@labels = LabelsFinder.new(current_user, project_id: @project.id).execute
@show_changes_tab = params[:show_changes].present?
define_pipelines_vars
end
......
......@@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
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.'
else
render 'edit'
......@@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
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.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
......@@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
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.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
......
......@@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
end
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])
if result[:status] == :success
......@@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format|
format.html do
......
......@@ -300,4 +300,13 @@ module ApplicationHelper
def page_class
"issue-boards-page" if current_controller?(:boards)
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
......@@ -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
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
module NavHelper
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
end
end
def page_gutter_class
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
......@@ -32,10 +26,6 @@ module NavHelper
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name
end
......@@ -46,8 +36,4 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end
......@@ -34,6 +34,10 @@ module PageLayoutHelper
end
end
def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
end
def page_image
default = image_url('gitlab_logo.png')
......
......@@ -41,10 +41,6 @@ module PreferencesHelper
]
end
def user_application_theme
Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
......
......@@ -128,6 +128,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
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|
value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
......@@ -155,6 +159,8 @@ class ApplicationSetting < ActiveRecord::Base
before_save :ensure_runners_registration_token
before_save :ensure_health_check_access_token
after_update :update_mirror_cron_jobs, if: :minimum_mirror_sync_time_changed?
after_commit do
Rails.cache.write(CACHE_KEY, self)
end
......@@ -224,7 +230,8 @@ class ApplicationSetting < ActiveRecord::Base
{
elasticsearch_host: ENV['ELASTIC_HOST'] || 'localhost',
elasticsearch_port: ENV['ELASTIC_PORT'] || '9200',
usage_ping_enabled: true
usage_ping_enabled: true,
minimum_mirror_sync_time: Gitlab::Mirror::FIFTEEN
}
end
......@@ -236,6 +243,15 @@ class ApplicationSetting < ActiveRecord::Base
create(defaults)
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
read_attribute(:elasticsearch_host).split(',').map(&:strip)
end
......
......@@ -63,33 +63,10 @@ module Ci
new_build.save
end
def retry(build, user = nil)
new_build = Ci::Build.create(
ref: build.ref,
tag: build.tag,
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
def retry(build, current_user)
Ci::RetryBuildService
.new(build.project, current_user)
.execute(build)
end
end
......@@ -137,7 +114,7 @@ module Ci
project.builds_enabled? && commands.present? && manual? && skipped?
end
def play(current_user = nil)
def play(current_user)
# Try to queue a current build
if self.enqueue
self.update(user: current_user)
......
......@@ -214,21 +214,17 @@ module Ci
def cancel_running
Gitlab::OptimisticLocking.retry_lock(
statuses.cancelable) do |cancelable|
cancelable.each(&:cancel)
cancelable.find_each(&:cancel)
end
end
def retry_failed(user)
Gitlab::OptimisticLocking.retry_lock(
builds.latest.failed_or_canceled) do |failed_or_canceled|
failed_or_canceled.select(&:retryable?).each do |build|
Ci::Build.retry(build, user)
end
end
def retry_failed(current_user)
Ci::RetryPipelineService.new(project, current_user)
.execute(self)
end
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
def latest?
......
......@@ -22,8 +22,6 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do
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)
......@@ -40,6 +38,8 @@ module Ci
acts_as_taggable
after_destroy :cleanup_runner_queue
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
......@@ -147,14 +147,14 @@ module Ci
private
def runner_queue_key
"runner:build_queue:#{self.token}"
def cleanup_runner_queue
Gitlab::Redis.with do |redis|
redis.del(runner_queue_key)
end
end
def form_editable_changed?
FORM_EDITABLE.any? do |editable|
public_send("#{editable}_changed?")
end
def runner_queue_key
"runner:build_queue:#{self.token}"
end
def tag_constraints
......
......@@ -23,9 +23,6 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :failed_but_allowed, -> do
where(allow_failure: true, status: [:failed, :canceled])
end
......@@ -36,8 +33,11 @@ class CommitStatus < ActiveRecord::Base
false, all_state_names - [:failed, :canceled])
end
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :latest_ordered, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ordered, -> { retried.ordered.includes(project: :namespace) }
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
state_machine :status do
event :enqueue do
......
......@@ -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') }
# 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
scope :root, -> { where('type IS NULL') }
......@@ -216,6 +216,14 @@ class Namespace < ActiveRecord::Base
parent_id_changed?
end
def prepare_for_destroy
old_repository_storage_paths
end
def old_repository_storage_paths
@old_repository_storage_paths ||= repository_storage_paths
end
private
def repository_storage_paths
......@@ -229,7 +237,7 @@ class Namespace < ActiveRecord::Base
def rm_dir
# 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.
# We will remove it later async
new_path = "#{path}+#{id}+deleted"
......
......@@ -218,7 +218,7 @@ class Project < ActiveRecord::Base
validates :sync_time,
presence: true,
inclusion: { in: Gitlab::Mirror.sync_time_options.values }
inclusion: { in: Gitlab::Mirror::SYNC_TIME_OPTIONS.values }
with_options if: :mirror? do |project|
project.validates :import_url, presence: true
......@@ -234,6 +234,8 @@ class Project < ActiveRecord::Base
# Scopes
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_stars, -> { reorder('projects.star_count DESC') }
......
......@@ -30,5 +30,9 @@ module ChatMessage
def attachment_color
'#345'
end
def link(text, url)
"[#{text}](#{url})"
end
end
end
......@@ -7,7 +7,11 @@ module ChatMessage
attr_reader :project_name
attr_reader :project_url
attr_reader :user_name
attr_reader :user_url
attr_reader :duration
attr_reader :stage
attr_reader :build_id
attr_reader :build_name
def initialize(params)
@sha = params[:sha]
......@@ -17,7 +21,11 @@ module ChatMessage
@project_url = params[:project_url]
@status = params[:commit][:status]
@user_name = params[:commit][:author_name]
@user_url = params[:commit][:author_url]
@duration = params[:commit][:duration]
@stage = params[:build_stage]
@build_name = params[:build_name]
@build_id = params[:build_id]
end
def pretext
......@@ -35,7 +43,19 @@ module ChatMessage
private
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
def format(string)
......@@ -64,11 +84,11 @@ module ChatMessage
end
def branch_link
"[#{ref}](#{branch_url})"
link(ref, branch_url)
end
def project_link
"[#{project_name}](#{project_url})"
link(project_name, project_url)
end
def commit_url
......@@ -76,7 +96,7 @@ module ChatMessage
end
def commit_link
"[#{Commit.truncate_sha(sha)}](#{commit_url})"
link(Commit.truncate_sha(sha), commit_url)
end
end
end
......@@ -55,11 +55,11 @@ module ChatMessage
end
def project_link
"[#{project_name}](#{project_url})"
link(project_name, project_url)
end
def issue_link
"[#{issue_title}](#{issue_url})"
link(issue_title, issue_url)
end
def issue_title
......
......@@ -43,7 +43,7 @@ module ChatMessage
end
def project_link
"[#{project_name}](#{project_url})"
link(project_name, project_url)
end
def merge_request_message
......@@ -51,7 +51,7 @@ module ChatMessage
end
def merge_request_link
"[merge request !#{merge_request_id}](#{merge_request_url})"
link("merge request !#{merge_request_id}", merge_request_url)
end
def merge_request_url
......
......@@ -3,10 +3,9 @@ module ChatMessage
attr_reader :message
attr_reader :user_name
attr_reader :project_name
attr_reader :project_link
attr_reader :project_url
attr_reader :note
attr_reader :note_url
attr_reader :title
def initialize(params)
params = HashWithIndifferentAccess.new(params)
......@@ -69,15 +68,15 @@ module ChatMessage
end
def description_message
[{ text: format(@note), color: attachment_color }]
[{ text: format(note), color: attachment_color }]
end
def project_link
"[#{@project_name}](#{@project_url})"
link(project_name, project_url)
end
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
# 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
}
end
def repository_storage_path
project.repository_storage_path
end
private
def init_repo(path_with_namespace)
......
......@@ -14,7 +14,7 @@ class RemoteMirror < ActiveRecord::Base
validates :url, presence: true, url: { protocols: %w(ssh git http https), allow_blank: true }
validates :sync_time,
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? }
......
......@@ -27,7 +27,7 @@ class Service < ActiveRecord::Base
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 :external_wikis, -> { where(type: 'ExternalWikiService').active }
scope :active, -> { where(active: true) }
......
......@@ -23,7 +23,6 @@ class User < ActiveRecord::Base
default_value_for :can_create_team, false
default_value_for :hide_no_ssh_key, false
default_value_for :hide_no_password, false
default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
......
class BaseService
include Gitlab::Allowable
include Gitlab::CurrentSettings
attr_accessor :project, :current_user, :params
......@@ -7,10 +8,6 @@ class BaseService
@project, @current_user, @params = project, user, params.dup
end
def can?(object, action, subject)
Ability.allowed?(object, action, subject)
end
def notification_service
NotificationService.new
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
end
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.
# Skip repository removal because we remove directory with namespace
# that contain all these repositories
......
......@@ -18,5 +18,11 @@ module MergeRequests
todo_service.merge_request_build_retried(merge_request)
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
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
escaped_filename = filename.gsub("]", "\\]")
markdown = "[#{escaped_filename}](#{self.secure_url})"
markdown.prepend("!") if image_or_video?
markdown.prepend("!") if image_or_video? || dangerous?
{
alt: filename,
......
# Extra methods for uploader
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
# 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
# on IE >= 9.
# http://archive.sublimevideo.info/20150912/docs.sublimevideo.net/troubleshooting.html
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?
extension_match?(IMAGE_EXT)
......@@ -20,6 +23,10 @@ module UploaderHelper
image? || video?
end
def dangerous?
extension_match?(DANGEROUS_EXT)
end
def extension_match?(extensions)
return false unless file
......
......@@ -12,6 +12,7 @@
%th.wide Message
%th Action
= render @abuse_reports
= paginate @abuse_reports, theme: 'gitlab'
- else
.empty-state
.text-center
......
......@@ -71,6 +71,10 @@
%span.help-block#repository_size_limit_help_block
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")
.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
= f.label :session_expire_delay, 'Session duration (minutes)', class: 'control-label col-sm-2'
.col-sm-10
......@@ -205,7 +209,7 @@
= f.label :max_pages_size, 'Maximum size of pages (MB)', class: 'control-label col-sm-2'
.col-sm-10
= f.number_field :max_pages_size, class: 'form-control'
.help-block Zero for unlimited
.help-block 0 for unlimited
%fieldset
%legend Continuous Integration
......@@ -581,7 +585,7 @@
= f.number_field :terminal_max_session_time, class: 'form-control'
.help-block
Maximum time for web terminal websocket connection (in seconds).
Set to 0 for unlimited time.
0 for unlimited.
.form-actions
= f.submit 'Save', class: 'btn btn-save'
......@@ -8,15 +8,14 @@
%div{ class: container_class }
%ul.nav-links.log-tabs
- 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}",
'data-toggle' => 'tab'
.row-content-block
To prevent performance issues admin logs output the last 2000 lines
.tab-content
- loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''),
id: klass::file_name_noext }
.tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
.file-holder#README
.js-file-title.file-title
%i.fa.fa-file
......
......@@ -48,13 +48,13 @@
= link_to admin_projects_path do
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
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
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
Public
......
......@@ -22,7 +22,7 @@
%td
#{runner.builds.count(:all)}
%td
- runner.tag_list.each do |tag|
- runner.tag_list.sort.each do |tag|
%span.label.label-primary
= tag
%td
......
......@@ -39,31 +39,31 @@
.nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.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
Active
%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
Admins
%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
2FA Enabled
%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
2FA Disabled
%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
External
%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
Blocked
%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
Without projects
%small.badge= number_with_delimiter(User.without_projects.count)
......
.top-area
%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
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
Starred Projects
......@@ -4,15 +4,13 @@
- if current_user.todos.any?
.top-area
%ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
%li{ class: "todos-pending #{todo_pending_active}" }>
%li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
= link_to todos_filter_path(state: 'pending') do
%span
To do
%span.badge
= number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done')
%li{ class: "todos-done #{todo_done_active}" }>
%li.todos-done{ class: active_when(params[:state] == 'done') }>
= link_to todos_filter_path(state: 'done') do
%span
Done
......
......@@ -10,7 +10,7 @@
= render 'devise/sessions/new_kerberos'
- @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
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
......
......@@ -6,8 +6,8 @@
%li{ class: (:active unless crowd_enabled? || ldap_enabled?) }
= link_to "Kerberos", "#kerberos", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
%li{ class: (:active if i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
%li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#ldap-standard', 'data-toggle' => 'tab'
......
......@@ -13,7 +13,7 @@
= link_to filter_projects_path(visibility_level: nil) do
Any
- 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
= visibility_level_icon(level)
= visibility_level_label(level)
......@@ -34,7 +34,7 @@
Any
- @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
= icon('tag')
= tag.name
......@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# 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 }
......@@ -23,7 +23,7 @@
%title= page_title(site_name)
%meta{ name: "description", content: page_description }
= favicon_link_tag 'favicon.ico'
= favicon_link_tag favicon
= stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print"
......
.page-with-sidebar{ class: "#{page_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'
.page-with-sidebar{ class: page_gutter_class }
- if defined?(nav) && nav
.layout-nav
.container-fluid
......
!!! 5
%html{ lang: "en", class: "#{page_class}" }
= 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
= 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