Commit 4657c287 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'master' into fix/keep-artifacts-button-visibility

* master: (579 commits)

Conflicts:
	spec/models/build_spec.rb
parents 5456859b daaaf5ef
...@@ -233,7 +233,13 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21 ...@@ -233,7 +233,13 @@ spinach 9 10 ruby21: *spinach-knapsack-ruby21
script: script:
- bundle exec $CI_BUILD_NAME - bundle exec $CI_BUILD_NAME
rubocop: *exec rubocop:
<<: *ruby-static-analysis
<<: *dedicated-runner
stage: test
script:
- bundle exec "rubocop --require rubocop-rspec"
rake haml_lint: *exec rake haml_lint: *exec
rake scss_lint: *exec rake scss_lint: *exec
rake brakeman: *exec rake brakeman: *exec
......
...@@ -343,10 +343,6 @@ Style/ParenthesesAroundCondition: ...@@ -343,10 +343,6 @@ Style/ParenthesesAroundCondition:
Style/RedundantParentheses: Style/RedundantParentheses:
Enabled: true Enabled: true
# Don't use return where it's not required.
Style/RedundantReturn:
Enabled: true
# Don't use semicolons to terminate expressions. # Don't use semicolons to terminate expressions.
Style/Semicolon: Style/Semicolon:
Enabled: true Enabled: true
......
This diff is collapsed.
...@@ -2,9 +2,19 @@ ...@@ -2,9 +2,19 @@
documentation](doc/development/changelog.md) for instructions on adding your own documentation](doc/development/changelog.md) for instructions on adding your own
entry. entry.
## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176
- Speed up group milestone index by passing group_id to IssuesFinder. !8363
- Don't instrument 405 Grape calls. !8445
- Update the gitlab-markup gem to the version 1.5.1. !8509
- Updated Turbolinks to mitigate potential XSS attacks.
- Re-order update steps in the 8.14 -> 8.15 upgrade guide.
- Re-add Google Cloud Storage as a backup strategy.
## 8.15.3 (2017-01-06) ## 8.15.3 (2017-01-06)
- Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !0 (8425) - Rename wiki_events to wiki_page_events in project hooks API to avoid errors. !8425
- Rename projects wth reserved names. !8234 - Rename projects wth reserved names. !8234
- Cache project authorizations even when user has access to zero projects. !8327 - Cache project authorizations even when user has access to zero projects. !8327
- Fix a minor grammar error in merge request widget. !8337 - Fix a minor grammar error in merge request widget. !8337
...@@ -255,6 +265,11 @@ entry. ...@@ -255,6 +265,11 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227 - Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237 - Adds back CSS for progress-bars. !8237
## 8.14.6 (2017-01-10)
- Update the gitlab-markup gem to the version 1.5.1. !8509
- Updated Turbolinks to mitigate potential XSS attacks.
## 8.14.5 (2016-12-14) ## 8.14.5 (2016-12-14)
- Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600 - Moved Leave Project and Leave Group buttons to access_request_buttons from the settings dropdown. !7600
...@@ -532,6 +547,11 @@ entry. ...@@ -532,6 +547,11 @@ entry.
- Fix "Without projects" filter. !6611 (Ben Bodenmiller) - Fix "Without projects" filter. !6611 (Ben Bodenmiller)
- Fix 404 when visit /projects page - Fix 404 when visit /projects page
## 8.13.11 (2017-01-10)
- Update the gitlab-markup gem to the version 1.5.1. !8509
- Updated Turbolinks to mitigate potential XSS attacks.
## 8.13.10 (2016-12-14) ## 8.13.10 (2016-12-14)
- API: Memoize the current_user so that sudo can work properly. !8017 - API: Memoize the current_user so that sudo can work properly. !8017
......
...@@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5' ...@@ -101,7 +101,7 @@ gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing # Markdown and HTML processing
gem 'html-pipeline', '~> 1.11.0' gem 'html-pipeline', '~> 1.11.0'
gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie' gem 'deckar01-task_list', '1.0.6', require: 'task_list/railtie'
gem 'gitlab-markup', '~> 1.5.0' gem 'gitlab-markup', '~> 1.5.1'
gem 'redcarpet', '~> 3.3.3' gem 'redcarpet', '~> 3.3.3'
gem 'RedCloth', '~> 4.3.2' gem 'RedCloth', '~> 4.3.2'
gem 'rdoc', '~> 4.2' gem 'rdoc', '~> 4.2'
...@@ -109,6 +109,7 @@ gem 'org-ruby', '~> 0.9.12' ...@@ -109,6 +109,7 @@ gem 'org-ruby', '~> 0.9.12'
gem 'creole', '~> 0.5.0' gem 'creole', '~> 0.5.0'
gem 'wikicloth', '0.8.1' gem 'wikicloth', '0.8.1'
gem 'asciidoctor', '~> 1.5.2' gem 'asciidoctor', '~> 1.5.2'
gem 'asciidoctor-plantuml', '0.0.6'
gem 'rouge', '~> 2.0' gem 'rouge', '~> 2.0'
gem 'truncato', '~> 0.7.8' gem 'truncato', '~> 0.7.8'
...@@ -221,8 +222,7 @@ gem 'chronic_duration', '~> 0.10.6' ...@@ -221,8 +222,7 @@ gem 'chronic_duration', '~> 0.10.6'
gem 'sass-rails', '~> 5.0.6' gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0' gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2' gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0' gem 'gitlab-turbolinks-classic', '~> 2.5', '>= 2.5.6'
gem 'jquery-turbolinks', '~> 2.1.0'
gem 'addressable', '~> 2.3.8' gem 'addressable', '~> 2.3.8'
gem 'bootstrap-sass', '~> 3.3.0' gem 'bootstrap-sass', '~> 3.3.0'
...@@ -300,8 +300,8 @@ group :development, :test do ...@@ -300,8 +300,8 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0' gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2' gem 'spring-commands-teaspoon', '~> 0.0.2'
gem 'rubocop', '~> 0.43.0', require: false gem 'rubocop', '~> 0.46.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false gem 'rubocop-rspec', '~> 1.9.1', require: false
gem 'scss_lint', '~> 0.47.0', require: false gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false gem 'simplecov', '0.12.0', require: false
......
...@@ -54,6 +54,8 @@ GEM ...@@ -54,6 +54,8 @@ GEM
faraday_middleware-multi_json (~> 0.0) faraday_middleware-multi_json (~> 0.0)
oauth2 (~> 1.0) oauth2 (~> 1.0)
asciidoctor (1.5.3) asciidoctor (1.5.3)
asciidoctor-plantuml (0.0.6)
asciidoctor (~> 1.5)
ast (2.3.0) ast (2.3.0)
attr_encrypted (3.0.3) attr_encrypted (3.0.3)
encryptor (~> 3.0.0) encryptor (~> 3.0.0)
...@@ -263,7 +265,9 @@ GEM ...@@ -263,7 +265,9 @@ GEM
diff-lcs (~> 1.1) diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3) mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3) posix-spawn (~> 0.3)
gitlab-markup (1.5.0) gitlab-markup (1.5.1)
gitlab-turbolinks-classic (2.5.6)
coffee-rails
gitlab_omniauth-ldap (1.2.1) gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9) net-ldap (~> 0.9)
omniauth (~> 1.0) omniauth (~> 1.0)
...@@ -370,9 +374,6 @@ GEM ...@@ -370,9 +374,6 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
jquery-turbolinks (2.1.0)
railties (>= 3.1.0)
turbolinks
jquery-ui-rails (5.0.5) jquery-ui-rails (5.0.5)
railties (>= 3.2.16) railties (>= 3.2.16)
json (1.8.3) json (1.8.3)
...@@ -649,14 +650,14 @@ GEM ...@@ -649,14 +650,14 @@ GEM
rspec-retry (0.4.5) rspec-retry (0.4.5)
rspec-core rspec-core
rspec-support (3.5.0) rspec-support (3.5.0)
rubocop (0.43.0) rubocop (0.46.0)
parser (>= 2.3.1.1, < 3.0) parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1) powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0) rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.7) ruby-progressbar (~> 1.7)
unicode-display_width (~> 1.0, >= 1.0.1) unicode-display_width (~> 1.0, >= 1.0.1)
rubocop-rspec (1.5.0) rubocop-rspec (1.9.1)
rubocop (>= 0.40.0) rubocop (>= 0.42.0)
ruby-fogbugz (0.2.1) ruby-fogbugz (0.2.1)
crack (~> 0.4) crack (~> 0.4)
ruby-prof (0.16.2) ruby-prof (0.16.2)
...@@ -782,8 +783,6 @@ GEM ...@@ -782,8 +783,6 @@ GEM
truncato (0.7.8) truncato (0.7.8)
htmlentities (~> 4.3.1) htmlentities (~> 4.3.1)
nokogiri (~> 1.6.1) nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2) tzinfo (1.2.2)
thread_safe (~> 0.1) thread_safe (~> 0.1)
u2f (0.2.1) u2f (0.2.1)
...@@ -844,6 +843,7 @@ DEPENDENCIES ...@@ -844,6 +843,7 @@ DEPENDENCIES
allocations (~> 1.0) allocations (~> 1.0)
asana (~> 0.4.0) asana (~> 0.4.0)
asciidoctor (~> 1.5.2) asciidoctor (~> 1.5.2)
asciidoctor-plantuml (= 0.0.6)
attr_encrypted (~> 3.0.0) attr_encrypted (~> 3.0.0)
awesome_print (~> 1.2.0) awesome_print (~> 1.2.0)
babosa (~> 1.0.2) babosa (~> 1.0.2)
...@@ -892,7 +892,8 @@ DEPENDENCIES ...@@ -892,7 +892,8 @@ DEPENDENCIES
gemojione (~> 3.0) gemojione (~> 3.0)
github-linguist (~> 4.7.0) github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1) gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.0) gitlab-markup (~> 1.5.1)
gitlab-turbolinks-classic (~> 2.5, >= 2.5.6)
gitlab_omniauth-ldap (~> 1.2.1) gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2) gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2) gollum-rugged_adapter (~> 0.4.2)
...@@ -911,7 +912,6 @@ DEPENDENCIES ...@@ -911,7 +912,6 @@ DEPENDENCIES
jira-ruby (~> 1.1.2) jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2) jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0) jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
jquery-ui-rails (~> 5.0.0) jquery-ui-rails (~> 5.0.0)
json-schema (~> 2.6.2) json-schema (~> 2.6.2)
jwt jwt
...@@ -974,8 +974,8 @@ DEPENDENCIES ...@@ -974,8 +974,8 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7) rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0) rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5) rspec-retry (~> 0.4.5)
rubocop (~> 0.43.0) rubocop (~> 0.46.0)
rubocop-rspec (~> 1.5.0) rubocop-rspec (~> 1.9.1)
ruby-fogbugz (~> 0.2.1) ruby-fogbugz (~> 0.2.1)
ruby-prof (~> 0.16.2) ruby-prof (~> 0.16.2)
rugged (~> 0.24.0) rugged (~> 0.24.0)
...@@ -1010,7 +1010,6 @@ DEPENDENCIES ...@@ -1010,7 +1010,6 @@ DEPENDENCIES
thin (~> 1.7.0) thin (~> 1.7.0)
timecop (~> 0.8.0) timecop (~> 0.8.0)
truncato (~> 0.7.8) truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1) u2f (~> 0.2.1)
uglifier (~> 2.7.2) uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0) underscore-rails (~> 1.8.0)
......
...@@ -58,6 +58,7 @@ ...@@ -58,6 +58,7 @@
/*= require_directory ./extensions */ /*= require_directory ./extensions */
/*= require_directory ./lib/utils */ /*= require_directory ./lib/utils */
/*= require_directory ./u2f */ /*= require_directory ./u2f */
/*= require_directory ./droplab */
/*= require_directory . */ /*= require_directory . */
/*= require fuzzaldrin-plus */ /*= require fuzzaldrin-plus */
/*= require es6-promise.auto */ /*= require es6-promise.auto */
......
...@@ -5,6 +5,7 @@ ...@@ -5,6 +5,7 @@
(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); }; };
var AUTO_SCROLL_OFFSET = 75; var AUTO_SCROLL_OFFSET = 75;
var DOWN_BUILD_TRACE = '#down-build-trace';
this.Build = (function() { this.Build = (function() {
Build.interval = null; Build.interval = null;
...@@ -26,7 +27,7 @@ ...@@ -26,7 +27,7 @@
this.$autoScrollStatus = $('#autoscroll-status'); this.$autoScrollStatus = $('#autoscroll-status');
this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text'); this.$autoScrollStatusText = this.$autoScrollStatus.find('.status-text');
this.$upBuildTrace = $('#up-build-trace'); this.$upBuildTrace = $('#up-build-trace');
this.$downBuildTrace = $('#down-build-trace'); this.$downBuildTrace = $(DOWN_BUILD_TRACE);
this.$scrollTopBtn = $('#scroll-top'); this.$scrollTopBtn = $('#scroll-top');
this.$scrollBottomBtn = $('#scroll-bottom'); this.$scrollBottomBtn = $('#scroll-bottom');
this.$buildRefreshAnimation = $('.js-build-refresh'); this.$buildRefreshAnimation = $('.js-build-refresh');
...@@ -91,6 +92,9 @@ ...@@ -91,6 +92,9 @@
dataType: 'json', dataType: 'json',
success: function(buildData) { success: function(buildData) {
$('.js-build-output').html(buildData.trace_html); $('.js-build-output').html(buildData.trace_html);
if (window.location.hash === DOWN_BUILD_TRACE) {
$("html,body").scrollTop(this.$buildTrace.height());
}
if (removeRefreshStatuses.indexOf(buildData.status) >= 0) { if (removeRefreshStatuses.indexOf(buildData.status) >= 0) {
this.$buildRefreshAnimation.remove(); this.$buildRefreshAnimation.remove();
return this.initScrollMonitor(); return this.initScrollMonitor();
...@@ -105,6 +109,8 @@ ...@@ -105,6 +109,8 @@
dataType: "json", dataType: "json",
success: (function(_this) { success: (function(_this) {
return function(log) { return function(log) {
var pageUrl;
if (log.state) { if (log.state) {
_this.state = log.state; _this.state = log.state;
} }
...@@ -116,7 +122,12 @@ ...@@ -116,7 +122,12 @@
} }
return _this.checkAutoscroll(); return _this.checkAutoscroll();
} else if (log.status !== _this.buildStatus) { } else if (log.status !== _this.buildStatus) {
return Turbolinks.visit(_this.pageUrl); pageUrl = _this.pageUrl;
if (_this.$autoScrollStatus.data('state') === 'enabled') {
pageUrl += DOWN_BUILD_TRACE;
}
return Turbolinks.visit(pageUrl);
} }
}; };
})(this) })(this)
......
(() => {
window.gl = window.gl || {};
class CILintEditor {
constructor() {
this.editor = window.ace.edit('ci-editor');
this.textarea = document.querySelector('#content');
this.editor.getSession().setMode('ace/mode/yaml');
this.editor.on('input', () => {
const content = this.editor.getSession().getValue();
this.textarea.value = content;
});
}
}
gl.CILintEditor = CILintEditor;
})();
...@@ -84,6 +84,9 @@ ...@@ -84,6 +84,9 @@
break; break;
case 'projects:merge_requests:index': case 'projects:merge_requests:index':
case 'projects:issues:index': case 'projects:issues:index':
if (gl.FilteredSearchManager) {
new gl.FilteredSearchManager();
}
Issuable.init(); Issuable.init();
new gl.IssuableBulkActions({ new gl.IssuableBulkActions({
prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_', prefixId: page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_',
...@@ -184,11 +187,6 @@ ...@@ -184,11 +187,6 @@
new TreeView(); new TreeView();
} }
break; break;
case 'projects:pipelines:index':
new gl.MiniPipelineGraph({
container: '.js-pipeline-table',
});
break;
case 'projects:pipelines:builds': case 'projects:pipelines:builds':
case 'projects:pipelines:show': case 'projects:pipelines:show':
const { controllerAction } = document.querySelector('.js-pipeline-container').dataset; const { controllerAction } = document.querySelector('.js-pipeline-container').dataset;
...@@ -273,6 +271,10 @@ ...@@ -273,6 +271,10 @@
case 'projects:variables:index': case 'projects:variables:index':
new gl.ProjectVariables(); new gl.ProjectVariables();
break; break;
case 'ci:lints:create':
case 'ci:lints:show':
new gl.CILintEditor();
break;
} }
switch (path.first()) { switch (path.first()) {
case 'admin': case 'admin':
......
This diff is collapsed.
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
function droplabAjaxException(message) {
this.message = message;
}
w.droplabAjax = {
_loadUrlData: function _loadUrlData(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
init: function init(hook) {
var self = this;
var config = hook.config.droplabAjax;
if (!config || !config.endpoint || !config.method) {
return;
}
if (config.method !== 'setData' && config.method !== 'addData') {
return;
}
if (config.loadingTemplate) {
var dynamicList = hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', '');
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
this._loadUrlData(config.endpoint)
.then(function(d) {
if (config.loadingTemplate) {
var dataLoadingTemplate = hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
hook.list[config.method].call(hook.list, d);
}).catch(function(e) {
throw new droplabAjaxException(e.message || e);
});
},
destroy: function() {
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.ajax||(g.ajax = {}));g=(g.datasource||(g.datasource = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabAjaxFilter = {
init: function(hook) {
this.destroyed = false;
this.hook = hook;
this.notLoading();
this.debounceTriggerWrapper = this.debounceTrigger.bind(this);
this.hook.trigger.addEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.addEventListener('focus', this.debounceTriggerWrapper);
this.trigger(true);
},
notLoading: function notLoading() {
this.loading = false;
},
debounceTrigger: function debounceTrigger(e) {
var NON_CHARACTER_KEYS = [16, 17, 18, 20, 37, 38, 39, 40, 91, 93];
var invalidKeyPressed = NON_CHARACTER_KEYS.indexOf(e.detail.which || e.detail.keyCode) > -1;
var focusEvent = e.type === 'focus';
if (invalidKeyPressed || this.loading) {
return;
}
if (this.timeout) {
clearTimeout(this.timeout);
}
this.timeout = setTimeout(this.trigger.bind(this, focusEvent), 200);
},
trigger: function trigger(getEntireList) {
var config = this.hook.config.droplabAjaxFilter;
var searchValue = this.trigger.value;
if (!config || !config.endpoint || !config.searchKey) {
return;
}
if (config.searchValueFunction) {
searchValue = config.searchValueFunction();
}
if (config.loadingTemplate && this.hook.list.data === undefined ||
this.hook.list.data.length === 0) {
var dynamicList = this.hook.list.list.querySelector('[data-dynamic]');
var loadingTemplate = document.createElement('div');
loadingTemplate.innerHTML = config.loadingTemplate;
loadingTemplate.setAttribute('data-loading-template', true);
this.listTemplate = dynamicList.outerHTML;
dynamicList.outerHTML = loadingTemplate.outerHTML;
}
if (getEntireList) {
searchValue = '';
}
if (config.searchKey === searchValue) {
return this.list.show();
}
this.loading = true;
var params = config.params || {};
params[config.searchKey] = searchValue;
var self = this;
this._loadUrlData(config.endpoint + this.buildParams(params)).then(function(data) {
if (config.loadingTemplate && self.hook.list.data === undefined ||
self.hook.list.data.length === 0) {
const dataLoadingTemplate = self.hook.list.list.querySelector('[data-loading-template]');
if (dataLoadingTemplate) {
dataLoadingTemplate.outerHTML = self.listTemplate;
}
}
if (!self.destroyed) {
var hookListChildren = self.hook.list.list.children;
var onlyDynamicList = hookListChildren.length === 1 && hookListChildren[0].hasAttribute('data-dynamic');
if (onlyDynamicList && data.length === 0) {
self.hook.list.hide();
}
self.hook.list.setData.call(self.hook.list, data);
}
self.notLoading();
});
},
_loadUrlData: function _loadUrlData(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.onreadystatechange = function () {
if(xhr.readyState === XMLHttpRequest.DONE) {
if (xhr.status === 200) {
var data = JSON.parse(xhr.responseText);
return resolve(data);
} else {
return reject([xhr.responseText, xhr.status]);
}
}
};
xhr.send();
});
},
buildParams: function(params) {
if (!params) return '';
var paramsArray = Object.keys(params).map(function(param) {
return param + '=' + (params[param] || '');
});
return '?' + paramsArray.join('&');
},
destroy: function destroy() {
if (this.timeout) {
clearTimeout(this.timeout);
}
this.destroyed = true;
this.hook.trigger.removeEventListener('keydown.dl', this.debounceTriggerWrapper);
this.hook.trigger.removeEventListener('focus', this.debounceTriggerWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/* eslint-disable */
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g=(g.droplab||(g.droplab = {}));g=(g.filter||(g.filter = {}));g.js = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/* global droplab */
require('../window')(function(w){
w.droplabFilter = {
keydownWrapper: function(e){
var list = e.detail.hook.list;
var data = list.data;
var value = e.detail.hook.trigger.value.toLowerCase();
var config = e.detail.hook.config.droplabFilter;
var matches = [];
var filterFunction;
// will only work on dynamically set data
if(!data){
return;
}
if (config && config.filterFunction && typeof config.filterFunction === 'function') {
filterFunction = config.filterFunction;
} else {
filterFunction = function(o){
// cheap string search
o.droplab_hidden = o[config.template].toLowerCase().indexOf(value) === -1;
return o;
};
}
matches = data.map(function(o) {
return filterFunction(o, value);
});
list.render(matches);
},
init: function init(hookInput) {
var config = hookInput.config.droplabFilter;
if (!config || (!config.template && !config.filterFunction)) {
return;
}
this.hookInput = hookInput;
this.hookInput.trigger.addEventListener('keyup.dl', this.keydownWrapper);
},
destroy: function destroy(){
this.hookInput.trigger.removeEventListener('keyup.dl', this.keydownWrapper);
}
};
});
},{"../window":2}],2:[function(require,module,exports){
module.exports = function(callback) {
return (function() {
callback(this);
}).call(null);
};
},{}]},{},[1])(1)
});
\ No newline at end of file
/*= require filtered_search/filtered_search_dropdown */
/* global droplabFilter */
(() => {
class DropdownHint extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
droplabFilter: {
template: 'hint',
filterFunction: gl.DropdownUtils.filterHint,
},
};
}
itemClicked(e) {
const { selected } = e.detail;
if (selected.tagName === 'LI') {
if (selected.hasAttribute('data-value')) {
this.dismissDropdown();
} else {
const token = selected.querySelector('.js-filter-hint').innerText.trim();
const tag = selected.querySelector('.js-filter-tag').innerText.trim();
if (tag.length) {
gl.FilteredSearchDropdownManager.addWordToInput(token.replace(':', ''));
}
this.dismissDropdown();
this.dispatchInputEvent();
}
}
}
renderContent() {
const dropdownData = [{
icon: 'fa-pencil',
hint: 'author:',
tag: '&lt;@author&gt;',
}, {
icon: 'fa-user',
hint: 'assignee:',
tag: '&lt;@assignee&gt;',
}, {
icon: 'fa-clock-o',
hint: 'milestone:',
tag: '&lt;%milestone&gt;',
}, {
icon: 'fa-tag',
hint: 'label:',
tag: '&lt;~label&gt;',
}];
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabFilter], this.config);
this.droplab.setData(this.hookId, dropdownData);
}
init() {
this.droplab.addHook(this.input, this.dropdown, [droplabFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownHint = DropdownHint;
})();
/*= require filtered_search/filtered_search_dropdown */
/* global droplabAjax */
/* global droplabFilter */
(() => {
class DropdownNonUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter, endpoint, symbol) {
super(droplab, dropdown, input, filter);
this.symbol = symbol;
this.config = {
droplabAjax: {
endpoint,
method: 'setData',
loadingTemplate: this.loadingTemplate,
},
droplabFilter: {
filterFunction: gl.DropdownUtils.filterWithSymbol.bind(null, this.symbol),
},
};
}
itemClicked(e) {
super.itemClicked(e, (selected) => {
const title = selected.querySelector('.js-data-value').innerText.trim();
return `${this.symbol}${gl.DropdownUtils.getEscapedText(title)}`;
});
}
renderContent(forceShowList = false) {
this.droplab
.changeHookList(this.hookId, this.dropdown, [droplabAjax, droplabFilter], this.config);
super.renderContent(forceShowList);
}
init() {
this.droplab
.addHook(this.input, this.dropdown, [droplabAjax, droplabFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownNonUser = DropdownNonUser;
})();
/*= require filtered_search/filtered_search_dropdown */
/* global droplabAjaxFilter */
(() => {
class DropdownUser extends gl.FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
super(droplab, dropdown, input, filter);
this.config = {
droplabAjaxFilter: {
endpoint: '/autocomplete/users.json',
searchKey: 'search',
params: {
per_page: 20,
active: true,
project_id: this.getProjectId(),
current_user: true,
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
},
};
}
itemClicked(e) {
super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
}
renderContent(forceShowList = false) {
this.droplab.changeHookList(this.hookId, this.dropdown, [droplabAjaxFilter], this.config);
super.renderContent(forceShowList);
}
getProjectId() {
return this.input.getAttribute('data-project-id');
}
getSearchInput() {
const query = this.input.value.trim();
const { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
return lastToken.value || '';
}
init() {
this.droplab.addHook(this.input, this.dropdown, [droplabAjaxFilter], this.config).init();
}
}
window.gl = window.gl || {};
gl.DropdownUser = DropdownUser;
})();
(() => {
class DropdownUtils {
static getEscapedText(text) {
let escapedText = text;
const hasSpace = text.indexOf(' ') !== -1;
const hasDoubleQuote = text.indexOf('"') !== -1;
// Encapsulate value with quotes if it has spaces
// Known side effect: values's with both single and double quotes
// won't escape properly
if (hasSpace) {
if (hasDoubleQuote) {
escapedText = `'${text}'`;
} else {
// Encapsulate singleQuotes or if it hasSpace
escapedText = `"${text}"`;
}
}
return escapedText;
}
static filterWithSymbol(filterSymbol, item, query) {
const updatedItem = item;
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(query);
if (lastToken !== searchToken) {
const title = updatedItem.title.toLowerCase();
let value = lastToken.value.toLowerCase();
if ((value[0] === '"' || value[0] === '\'') && title.indexOf(' ') !== -1) {
value = value.slice(1);
}
// Eg. filterSymbol = ~ for labels
const matchWithoutSymbol = lastToken.symbol === filterSymbol && title.indexOf(value) !== -1;
const match = title.indexOf(`${lastToken.symbol}${value}`) !== -1;
updatedItem.droplab_hidden = !match && !matchWithoutSymbol;
} else {
updatedItem.droplab_hidden = false;
}
return updatedItem;
}
static filterHint(item, query) {
const updatedItem = item;
let { lastToken } = gl.FilteredSearchTokenizer.processTokens(query);
lastToken = lastToken.key || lastToken || '';
if (!lastToken || query.split('').last() === ' ') {
updatedItem.droplab_hidden = false;
} else if (lastToken) {
const split = lastToken.split(':');
const tokenName = split[0].split(' ').last();
const match = updatedItem.hint.indexOf(tokenName.toLowerCase()) === -1;
updatedItem.droplab_hidden = tokenName ? match : false;
}
return updatedItem;
}
static setDataValueIfSelected(filter, selected) {
const dataValue = selected.getAttribute('data-value');
if (dataValue) {
gl.FilteredSearchDropdownManager.addWordToInput(filter, dataValue);
}
// Return boolean based on whether it was set
return dataValue !== null;
}
}
window.gl = window.gl || {};
gl.DropdownUtils = DropdownUtils;
})();
// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
/*= require_tree . */
(() => {
const DATA_DROPDOWN_TRIGGER = 'data-dropdown-trigger';
class FilteredSearchDropdown {
constructor(droplab, dropdown, input, filter) {
this.droplab = droplab;
this.hookId = input.getAttribute('data-id');
this.input = input;
this.filter = filter;
this.dropdown = dropdown;
this.loadingTemplate = `<div class="filter-dropdown-loading">
<i class="fa fa-spinner fa-spin"></i>
</div>`;
this.bindEvents();
}
bindEvents() {
this.itemClickedWrapper = this.itemClicked.bind(this);
this.dropdown.addEventListener('click.dl', this.itemClickedWrapper);
}
unbindEvents() {
this.dropdown.removeEventListener('click.dl', this.itemClickedWrapper);
}
getCurrentHook() {
return this.droplab.hooks.filter(h => h.id === this.hookId)[0] || null;
}
itemClicked(e, getValueFunction) {
const { selected } = e.detail;
if (selected.tagName === 'LI' && selected.innerHTML) {
const dataValueSet = gl.DropdownUtils.setDataValueIfSelected(this.filter, selected);
if (!dataValueSet) {
const value = getValueFunction(selected);
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value);
}
this.dismissDropdown();
}
}
setAsDropdown() {
this.input.setAttribute(DATA_DROPDOWN_TRIGGER, `#${this.dropdown.id}`);
}
setOffset(offset = 0) {
this.dropdown.style.left = `${offset}px`;
}
renderContent(forceShowList = false) {
if (forceShowList && this.getCurrentHook().list.hidden) {
this.getCurrentHook().list.show();
}
}
render(forceRenderContent = false, forceShowList = false) {
this.setAsDropdown();
const currentHook = this.getCurrentHook();
const firstTimeInitialized = currentHook === null;
if (firstTimeInitialized || forceRenderContent) {
this.renderContent(forceShowList);
} else if (currentHook.list.list.id !== this.dropdown.id) {
this.renderContent(forceShowList);
}
}
dismissDropdown() {
// Focusing on the input will dismiss dropdown
// (default droplab functionality)
this.input.focus();
}
dispatchInputEvent() {
// Propogate input change to FilteredSearchDropdownManager
// so that it can determine which dropdowns to open
this.input.dispatchEvent(new Event('input'));
}
hideDropdown() {
this.getCurrentHook().list.hide();
}
resetFilters() {
const hook = this.getCurrentHook();
const data = hook.list.data;
const results = data.map((o) => {
const updated = o;
updated.droplab_hidden = false;
return updated;
});
hook.list.render(results);
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdown = FilteredSearchDropdown;
})();
/* global DropLab */
(() => {
class FilteredSearchDropdownManager {
constructor() {
this.tokenizer = gl.FilteredSearchTokenizer;
this.filteredSearchInput = document.querySelector('.filtered-search');
this.setupMapping();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
}
cleanup() {
if (this.droplab) {
this.droplab.destroy();
this.droplab = null;
}
this.setupMapping();
document.removeEventListener('page:fetch', this.cleanupWrapper);
}
setupMapping() {
this.mapping = {
author: {
reference: null,
gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-author'),
},
assignee: {
reference: null,
gl: 'DropdownUser',
element: document.querySelector('#js-dropdown-assignee'),
},
milestone: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: ['milestones.json', '%'],
element: document.querySelector('#js-dropdown-milestone'),
},
label: {
reference: null,
gl: 'DropdownNonUser',
extraArguments: ['labels.json', '~'],
element: document.querySelector('#js-dropdown-label'),
},
hint: {
reference: null,
gl: 'DropdownHint',
element: document.querySelector('#js-dropdown-hint'),
},
};
}
static addWordToInput(tokenName, tokenValue = '') {
const input = document.querySelector('.filtered-search');
const word = `${tokenName}:${tokenValue}`;
const { lastToken, searchToken } = gl.FilteredSearchTokenizer.processTokens(input.value);
const lastSearchToken = searchToken.split(' ').last();
const lastInputCharacter = input.value[input.value.length - 1];
const lastInputTrimmedCharacter = input.value.trim()[input.value.trim().length - 1];
// Remove the typed tokenName
if (word.indexOf(lastSearchToken) === 0 && searchToken !== '') {
// Remove spaces after the colon
if (lastInputCharacter === ' ' && lastInputTrimmedCharacter === ':') {
input.value = input.value.trim();
}
input.value = input.value.slice(0, -1 * lastSearchToken.length);
} else if (lastInputCharacter !== ' ' || (lastToken && lastToken.value[lastToken.value.length - 1] === ' ')) {
// Remove the existing tokenValue
const lastTokenString = `${lastToken.key}:${lastToken.symbol}${lastToken.value}`;
input.value = input.value.slice(0, -1 * lastTokenString.length);
}
input.value += word;
}
updateCurrentDropdownOffset() {
this.updateDropdownOffset(this.currentDropdown);
}
updateDropdownOffset(key) {
if (!this.font) {
this.font = window.getComputedStyle(this.filteredSearchInput).font;
}
const filterIconPadding = 27;
const offset = gl.text
.getTextWidth(this.filteredSearchInput.value, this.font) + filterIconPadding;
this.mapping[key].reference.setOffset(offset);
}
load(key, firstLoad = false) {
const mappingKey = this.mapping[key];
const glClass = mappingKey.gl;
const element = mappingKey.element;
let forceShowList = false;
if (!mappingKey.reference) {
const dl = this.droplab;
const defaultArguments = [null, dl, element, this.filteredSearchInput, key];
const glArguments = defaultArguments.concat(mappingKey.extraArguments || []);
// Passing glArguments to `new gl[glClass](<arguments>)`
mappingKey.reference = new (Function.prototype.bind.apply(gl[glClass], glArguments))();
}
if (firstLoad) {
mappingKey.reference.init();
}
if (this.currentDropdown === 'hint') {
// Force the dropdown to show if it was clicked from the hint dropdown
forceShowList = true;
}
this.updateDropdownOffset(key);
mappingKey.reference.render(firstLoad, forceShowList);
this.currentDropdown = key;
}
loadDropdown(dropdownName = '') {
let firstLoad = false;
if (!this.droplab) {
firstLoad = true;
this.droplab = new DropLab();
}
const match = gl.FilteredSearchTokenKeys.searchByKey(dropdownName.toLowerCase());
const shouldOpenFilterDropdown = match && this.currentDropdown !== match.key
&& this.mapping[match.key];
const shouldOpenHintDropdown = !match && this.currentDropdown !== 'hint';
if (shouldOpenFilterDropdown || shouldOpenHintDropdown) {
const key = match && match.key ? match.key : 'hint';
this.load(key, firstLoad);
}
}
setDropdown() {
const { lastToken, searchToken } = this.tokenizer
.processTokens(this.filteredSearchInput.value);
if (this.filteredSearchInput.value.split('').last() === ' ') {
this.updateCurrentDropdownOffset();
}
if (lastToken === searchToken && lastToken !== null) {
// Token is not fully initialized yet because it has no value
// Eg. token = 'label:'
const split = lastToken.split(':');
const dropdownName = split[0].split(' ').last();
this.loadDropdown(split.length > 1 ? dropdownName : '');
} else if (lastToken) {
// Token has been initialized into an object because it has a value
this.loadDropdown(lastToken.key);
} else {
this.loadDropdown('hint');
}
}
resetDropdowns() {
// Force current dropdown to hide
this.mapping[this.currentDropdown].reference.hideDropdown();
// Re-Load dropdown
this.setDropdown();
// Reset filters for current dropdown
this.mapping[this.currentDropdown].reference.resetFilters();
// Reposition dropdown so that it is aligned with cursor
this.updateDropdownOffset(this.currentDropdown);
}
destroyDroplab() {
this.droplab.destroy();
}
}
window.gl = window.gl || {};
gl.FilteredSearchDropdownManager = FilteredSearchDropdownManager;
})();
/* global Turbolinks */
(() => {
class FilteredSearchManager {
constructor() {
this.filteredSearchInput = document.querySelector('.filtered-search');
this.clearSearchButton = document.querySelector('.clear-search');
if (this.filteredSearchInput) {
this.tokenizer = gl.FilteredSearchTokenizer;
this.dropdownManager = new gl.FilteredSearchDropdownManager();
this.bindEvents();
this.loadSearchParamsFromURL();
this.dropdownManager.setDropdown();
this.cleanupWrapper = this.cleanup.bind(this);
document.addEventListener('page:fetch', this.cleanupWrapper);
}
}
cleanup() {
this.unbindEvents();
document.removeEventListener('page:fetch', this.cleanupWrapper);
}
bindEvents() {
this.setDropdownWrapper = this.dropdownManager.setDropdown.bind(this.dropdownManager);
this.toggleClearSearchButtonWrapper = this.toggleClearSearchButton.bind(this);
this.checkForEnterWrapper = this.checkForEnter.bind(this);
this.clearSearchWrapper = this.clearSearch.bind(this);
this.checkForBackspaceWrapper = this.checkForBackspace.bind(this);
this.filteredSearchInput.addEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.addEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.addEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
}
unbindEvents() {
this.filteredSearchInput.removeEventListener('input', this.setDropdownWrapper);
this.filteredSearchInput.removeEventListener('input', this.toggleClearSearchButtonWrapper);
this.filteredSearchInput.removeEventListener('keydown', this.checkForEnterWrapper);
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
}
checkForBackspace(e) {
// 8 = Backspace Key
// 46 = Delete Key
if (e.keyCode === 8 || e.keyCode === 46) {
// Reposition dropdown so that it is aligned with cursor
this.dropdownManager.updateCurrentDropdownOffset();
}
}
checkForEnter(e) {
if (e.keyCode === 13) {
e.preventDefault();
// Prevent droplab from opening dropdown
this.dropdownManager.destroyDroplab();
this.search();
}
}
toggleClearSearchButton(e) {
if (e.target.value) {
this.clearSearchButton.classList.remove('hidden');
} else {
this.clearSearchButton.classList.add('hidden');
}
}
clearSearch(e) {
e.preventDefault();
this.filteredSearchInput.value = '';
this.clearSearchButton.classList.add('hidden');
this.dropdownManager.resetDropdowns();
}
loadSearchParamsFromURL() {
const params = gl.utils.getUrlParamsArray();
const inputValues = [];
params.forEach((p) => {
const split = p.split('=');
const keyParam = decodeURIComponent(split[0]);
const value = split[1];
// Check if it matches edge conditions listed in gl.FilteredSearchTokenKeys
const condition = gl.FilteredSearchTokenKeys.searchByConditionUrl(p);
if (condition) {
inputValues.push(`${condition.tokenKey}:${condition.value}`);
} else {
// Sanitize value since URL converts spaces into +
// Replace before decode so that we know what was originally + versus the encoded +
const sanitizedValue = value ? decodeURIComponent(value.replace(/\+/g, ' ')) : value;
const match = gl.FilteredSearchTokenKeys.searchByKeyParam(keyParam);
if (match) {
const indexOf = keyParam.indexOf('_');
const sanitizedKey = indexOf !== -1 ? keyParam.slice(0, keyParam.indexOf('_')) : keyParam;
const symbol = match.symbol;
let quotationsToUse = '';
if (sanitizedValue.indexOf(' ') !== -1) {
// Prefer ", but use ' if required
quotationsToUse = sanitizedValue.indexOf('"') === -1 ? '"' : '\'';
}
inputValues.push(`${sanitizedKey}:${symbol}${quotationsToUse}${sanitizedValue}${quotationsToUse}`);
} else if (!match && keyParam === 'search') {
inputValues.push(sanitizedValue);
}
}
});
// Trim the last space value
this.filteredSearchInput.value = inputValues.join(' ');
if (inputValues.length > 0) {
this.clearSearchButton.classList.remove('hidden');
}
}
search() {
const paths = [];
const { tokens, searchToken } = this.tokenizer.processTokens(this.filteredSearchInput.value);
const currentState = gl.utils.getParameterByName('state') || 'opened';
paths.push(`state=${currentState}`);
tokens.forEach((token) => {
const condition = gl.FilteredSearchTokenKeys
.searchByConditionKeyValue(token.key, token.value.toLowerCase());
const { param } = gl.FilteredSearchTokenKeys.searchByKey(token.key);
const keyParam = param ? `${token.key}_${param}` : token.key;
let tokenPath = '';
if (condition) {
tokenPath = condition.url;
} else {
let tokenValue = token.value;
if ((tokenValue[0] === '\'' && tokenValue[tokenValue.length - 1] === '\'') ||
(tokenValue[0] === '"' && tokenValue[tokenValue.length - 1] === '"')) {
tokenValue = tokenValue.slice(1, tokenValue.length - 1);
}
tokenPath = `${keyParam}=${encodeURIComponent(tokenValue)}`;
}
paths.push(tokenPath);
});
if (searchToken) {
paths.push(`search=${encodeURIComponent(searchToken)}`);
}
Turbolinks.visit(`?scope=all&utf8=✓&${paths.join('&')}`);
}
}
window.gl = window.gl || {};
gl.FilteredSearchManager = FilteredSearchManager;
})();
(() => {
const tokenKeys = [{
key: 'author',
type: 'string',
param: 'username',
symbol: '@',
}, {
key: 'assignee',
type: 'string',
param: 'username',
symbol: '@',
}, {
key: 'milestone',
type: 'string',
param: 'title',
symbol: '%',
}, {
key: 'label',
type: 'array',
param: 'name[]',
symbol: '~',
}];
const conditions = [{
url: 'assignee_id=0',
tokenKey: 'assignee',
value: 'none',
}, {
url: 'milestone_title=No+Milestone',
tokenKey: 'milestone',
value: 'none',
}, {
url: 'milestone_title=%23upcoming',
tokenKey: 'milestone',
value: 'upcoming',
}, {
url: 'label_name[]=No+Label',
tokenKey: 'label',
value: 'none',
}];
class FilteredSearchTokenKeys {
static get() {
return tokenKeys;
}
static getConditions() {
return conditions;
}
static searchByKey(key) {
return tokenKeys.find(tokenKey => tokenKey.key === key) || null;
}
static searchBySymbol(symbol) {
return tokenKeys.find(tokenKey => tokenKey.symbol === symbol) || null;
}
static searchByKeyParam(keyParam) {
return tokenKeys.find((tokenKey) => {
let tokenKeyParam = tokenKey.key;
if (tokenKey.param) {
tokenKeyParam += `_${tokenKey.param}`;
}
return keyParam === tokenKeyParam;
}) || null;
}
static searchByConditionUrl(url) {
return conditions.find(condition => condition.url === url) || null;
}
static searchByConditionKeyValue(key, value) {
return conditions
.find(condition => condition.tokenKey === key && condition.value === value) || null;
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenKeys = FilteredSearchTokenKeys;
})();
(() => {
class FilteredSearchTokenizer {
static processTokens(input) {
// Regex extracts `(token):(symbol)(value)`
// Values that start with a double quote must end in a double quote (same for single)
const tokenRegex = /(\w+):([~%@]?)(?:('[^']*'{0,1})|("[^"]*"{0,1})|(\S+))/g;
const tokens = [];
let lastToken = null;
const searchToken = input.replace(tokenRegex, (match, key, symbol, v1, v2, v3) => {
let tokenValue = v1 || v2 || v3;
let tokenSymbol = symbol;
if (tokenValue === '~' || tokenValue === '%' || tokenValue === '@') {
tokenSymbol = tokenValue;
tokenValue = '';
}
tokens.push({
key,
value: tokenValue || '',
symbol: tokenSymbol || '',
});
return '';
}).replace(/\s{2,}/g, ' ').trim() || '';
if (tokens.length > 0) {
const last = tokens[tokens.length - 1];
const lastString = `${last.key}:${last.symbol}${last.value}`;
lastToken = input.lastIndexOf(lastString) ===
input.length - lastString.length ? last : searchToken;
} else {
lastToken = searchToken;
}
return {
tokens,
lastToken,
searchToken,
};
}
}
window.gl = window.gl || {};
gl.FilteredSearchTokenizer = FilteredSearchTokenizer;
})();
...@@ -124,6 +124,12 @@ ...@@ -124,6 +124,12 @@
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname; return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : '/' + parsedUrl.pathname;
}; };
gl.utils.getUrlParamsArray = function () {
// We can trust that each param has one & since values containing & will be encoded
// Remove the first character of search as it is always ?
return window.location.search.slice(1).split('&');
};
gl.utils.isMetaKey = function(e) { gl.utils.isMetaKey = function(e) {
return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey; return e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
}; };
...@@ -139,6 +145,21 @@ ...@@ -139,6 +145,21 @@
}, 200); }, 200);
}; };
/**
this will take in the `name` of the param you want to parse in the url
if the name does not exist this function will return `null`
otherwise it will return the value of the param key provided
*/
w.gl.utils.getParameterByName = (name) => {
const url = window.location.href;
name = name.replace(/[[\]]/g, '\\$&');
const regex = new RegExp(`[?&]${name}(=([^&#]*)|&|#|$)`);
const results = regex.exec(url);
if (!results) return null;
if (!results[2]) return '';
return decodeURIComponent(results[2].replace(/\+/g, ' '));
};
})(window); })(window);
}).call(this); }).call(this);
...@@ -17,6 +17,21 @@ ...@@ -17,6 +17,21 @@
gl.text.replaceRange = function(s, start, end, substitute) { gl.text.replaceRange = function(s, start, end, substitute) {
return s.substring(0, start) + substitute + s.substring(end); return s.substring(0, start) + substitute + s.substring(end);
}; };
gl.text.getTextWidth = function(text, font) {
/**
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
*
* @param {String} text The text to be rendered.
* @param {String} font The css font descriptor that text is to be rendered with (e.g. "bold 14px verdana").
*
* @see http://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
*/
// re-use canvas object for better performance
var canvas = gl.text.getTextWidth.canvas || (gl.text.getTextWidth.canvas = document.createElement('canvas'));
var context = canvas.getContext('2d');
context.font = font;
return context.measureText(text).width;
};
gl.text.selectedText = function(text, textarea) { gl.text.selectedText = function(text, textarea) {
return text.substring(textarea.selectionStart, textarea.selectionEnd); return text.substring(textarea.selectionStart, textarea.selectionEnd);
}; };
......
...@@ -142,8 +142,9 @@ ...@@ -142,8 +142,9 @@
} }
getCategoryContents() { getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils; var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, userName, utils;
userId = gon.current_user_id; userId = gon.current_user_id;
userName = gon.current_username;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions; utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
if (utils.isInGroupsPage() && groupOptions) { if (utils.isInGroupsPage() && groupOptions) {
options = groupOptions[utils.getGroupSlug()]; options = groupOptions[utils.getGroupSlug()];
...@@ -158,10 +159,10 @@ ...@@ -158,10 +159,10 @@
header: "" + name header: "" + name
}, { }, {
text: 'Issues assigned to me', text: 'Issues assigned to me',
url: issuesPath + "/?assignee_id=" + userId url: issuesPath + "/?assignee_username=" + userName
}, { }, {
text: "Issues I've created", text: "Issues I've created",
url: issuesPath + "/?author_id=" + userId url: issuesPath + "/?author_username=" + userName
}, 'separator', { }, 'separator', {
text: 'Merge requests assigned to me', text: 'Merge requests assigned to me',
url: mrPath + "/?assignee_id=" + userId url: mrPath + "/?assignee_id=" + userId
......
/* global Vue, gl */
/* eslint-disable no-param-reassign, no-plusplus */
((gl) => {
const PAGINATION_UI_BUTTON_LIMIT = 4;
const UI_LIMIT = 6;
const SPREAD = '...';
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
gl.VueGlPagination = Vue.extend({
props: {
/**
This function will take the information given by the pagination component
And make a new Turbolinks call
Here is an example `change` method:
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
*/
change: {
type: Function,
required: true,
},
/**
pageInfo will come from the headers of the API call
in the `.then` clause of the VueResource API call
there should be a function that contructs the pageInfo for this component
This is an example:
const pageInfo = headers => ({
perPage: +headers['X-Per-Page'],
page: +headers['X-Page'],
total: +headers['X-Total'],
totalPages: +headers['X-Total-Pages'],
nextPage: +headers['X-Next-Page'],
previousPage: +headers['X-Prev-Page'],
});
*/
pageInfo: {
type: Object,
required: true,
},
},
methods: {
changePage(e) {
let apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;
switch (text) {
case SPREAD:
break;
case LAST:
this.change(totalPages, apiScope);
break;
case NEXT:
this.change(nextPage, apiScope);
break;
case PREV:
this.change(previousPage, apiScope);
break;
case FIRST:
this.change(1, apiScope);
break;
default:
this.change(+text, apiScope);
break;
}
},
},
computed: {
prev() {
return this.pageInfo.previousPage;
},
next() {
return this.pageInfo.nextPage;
},
getItems() {
const total = this.pageInfo.totalPages;
const page = this.pageInfo.page;
const items = [];
if (page > 1) items.push({ title: FIRST });
if (page > 1) {
items.push({ title: PREV, prev: true });
} else {
items.push({ title: PREV, disabled: true, prev: true });
}
if (page > UI_LIMIT) items.push({ title: SPREAD, separator: true });
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i++) {
const isActive = i === page;
items.push({ title: i, active: isActive, page: true });
}
if (total - page > PAGINATION_UI_BUTTON_LIMIT) {
items.push({ title: SPREAD, separator: true, page: true });
}
if (page === total) {
items.push({ title: NEXT, disabled: true, next: true });
} else if (total - page >= 1) {
items.push({ title: NEXT, next: true });
}
if (total - page >= 1) items.push({ title: LAST, last: true });
return items;
},
},
template: `
<div class="gl-pagination">
<ul class="pagination clearfix">
<li v-for='item in getItems'
:class='{
page: item.page,
prev: item.prev,
next: item.next,
separator: item.separator,
active: item.active,
disabled: item.disabled
}'
>
<a @click="changePage($event)">{{item.title}}</a>
</li>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, VueResource, gl */
/*= require vue_common_component/commit */
/*= require vue_pagination/index */
/*= require vue-resource
/*= require boards/vue_resource_interceptor */
/*= require ./status.js.es6 */
/*= require ./store.js.es6 */
/*= require ./pipeline_url.js.es6 */
/*= require ./stage.js.es6 */
/*= require ./stages.js.es6 */
/*= require ./pipeline_actions.js.es6 */
/*= require ./time_ago.js.es6 */
/*= require ./pipelines.js.es6 */
(() => {
const project = document.querySelector('.pipelines');
const entry = document.querySelector('.vue-pipelines-index');
const svgs = document.querySelector('.pipeline-svgs');
Vue.use(VueResource);
if (!entry) return null;
return new Vue({
el: entry,
data: {
scope: project.dataset.url,
store: new gl.PipelineStore(),
svgs: svgs.dataset,
},
components: {
'vue-pipelines': gl.VuePipelines,
},
template: `
<vue-pipelines
:scope='scope'
:store='store'
:svgs='svgs'
>
</vue-pipelines>
`,
});
})();
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineActions = Vue.extend({
props: ['pipeline', 'svgs'],
computed: {
actions() {
return this.pipeline.details.manual_actions.length > 0;
},
artifacts() {
return this.pipeline.details.artifacts.length > 0;
},
},
methods: {
download(name) {
return `Download ${name} artifacts`;
},
},
template: `
<td class="pipeline-actions hidden-xs">
<div class="controls pull-right">
<div class="btn-group inline">
<div class="btn-group">
<a
v-if='actions'
class="dropdown-toggle btn btn-default js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual build"
alt="Manual Build"
>
<span v-html='svgs.iconPlay'></span>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href='action.path'
title="Manual build"
>
<span v-html='svgs.iconPlay'></span>
<span title="Manual build">{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group">
<a
v-if='artifacts'
class="dropdown-toggle btn btn-default build-artifacts js-pipeline-dropdown-download"
data-toggle="dropdown"
type="button"
>
<i class="fa fa-download"></i>
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href='artifact.path'
>
<i class="fa fa-download"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
</ul>
</div>
</div>
<div class="cancel-retry-btns inline">
<a
v-if='pipeline.flags.retryable'
class="btn has-tooltip"
title="Retry"
rel="nofollow"
data-method="post"
:href='pipeline.retry_path'
>
<i class="fa fa-repeat"></i>
</a>
<a
v-if='pipeline.flags.cancelable'
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
data-method="post"
:href='pipeline.cancel_path'
data-original-title="Cancel"
>
<i class="fa fa-remove"></i>
</a>
</div>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelineUrl = Vue.extend({
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a :href='pipeline.path'>
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
v-if='user'
:href='pipeline.user.web_url'
>
<img
v-if='user'
class="avatar has-tooltip s20 "
:title='pipeline.user.name'
data-container="body"
:src='pipeline.user.avatar_url'
>
</a>
<span
v-if='!user'
class="api monospace"
>
API
</span>
<span
v-if='pipeline.flags.latest'
class="label label-success has-tooltip"
title="Latest pipeline for this branch"
data-original-title="Latest pipeline for this branch"
>
latest
</span>
<span
v-if='pipeline.flags.yaml_errors'
class="label label-danger has-tooltip"
:title='pipeline.yaml_errors'
:data-original-title='pipeline.yaml_errors'
>
yaml invalid
</span>
<span
v-if='pipeline.flags.stuck'
class="label label-warning"
>
stuck
</span>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, Turbolinks, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VuePipelines = Vue.extend({
components: {
runningPipeline: gl.VueRunningPipeline,
pipelineActions: gl.VuePipelineActions,
stages: gl.VueStages,
commit: gl.CommitComponent,
pipelineUrl: gl.VuePipelineUrl,
pipelineHead: gl.VuePipelineHead,
glPagination: gl.VueGlPagination,
statusScope: gl.VueStatusScope,
timeAgo: gl.VueTimeAgo,
},
data() {
return {
pipelines: [],
timeLoopInterval: '',
intervalId: '',
apiScope: 'all',
pageInfo: {},
pagenum: 1,
count: { all: 0, running_or_pending: 0 },
pageRequest: false,
};
},
props: ['scope', 'store', 'svgs'],
created() {
const pagenum = gl.utils.getParameterByName('p');
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: {
change(pagenum, apiScope) {
Turbolinks.visit(`?scope=${apiScope}&p=${pagenum}`);
},
author(pipeline) {
if (!pipeline.commit) return { avatar_url: '', web_url: '', username: '' };
if (pipeline.commit.author) return pipeline.commit.author;
return {
avatar_url: pipeline.commit.author_gravatar_url,
web_url: `mailto:${pipeline.commit.author_email}`,
username: pipeline.commit.author_name,
};
},
ref(pipeline) {
const { ref } = pipeline;
return { name: ref.name, tag: ref.tag, ref_url: ref.path };
},
commitTitle(pipeline) {
return pipeline.commit ? pipeline.commit.title : '';
},
commitSha(pipeline) {
return pipeline.commit ? pipeline.commit.short_id : '';
},
commitUrl(pipeline) {
return pipeline.commit ? pipeline.commit.commit_path : '';
},
match(string) {
return string.replace(/_([a-z])/g, (m, w) => w.toUpperCase());
},
},
template: `
<div>
<div class="pipelines realtime-loading" v-if='pipelines.length < 1'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder" v-if='pipelines.length'>
<table class="table ci-table">
<thead>
<tr>
<th class="pipeline-status">Status</th>
<th class="pipeline-info">Pipeline</th>
<th class="pipeline-commit">Commit</th>
<th class="pipeline-stages">Stages</th>
<th class="pipeline-date"></th>
<th class="pipeline-actions hidden-xs"></th>
</tr>
</thead>
<tbody>
<tr class="commit" v-for='pipeline in pipelines'>
<status-scope
:pipeline='pipeline'
:match='match'
:svgs='svgs'
>
</status-scope>
<pipeline-url :pipeline='pipeline'></pipeline-url>
<td>
<commit
:commit-icon-svg='svgs.commitIconSvg'
:author='author(pipeline)'
:tag="pipeline.ref.tag"
:title='commitTitle(pipeline)'
:commit-ref='ref(pipeline)'
:short-sha='commitSha(pipeline)'
:commit-url='commitUrl(pipeline)'
>
</commit>
</td>
<stages
:pipeline='pipeline'
:svgs='svgs'
:match='match'
>
</stages>
<time-ago :pipeline='pipeline' :svgs='svgs'></time-ago>
<pipeline-actions :pipeline='pipeline' :svgs='svgs'></pipeline-actions>
</tr>
</tbody>
</table>
</div>
<div class="pipelines realtime-loading" v-if='pageRequest'>
<i class="fa fa-spinner fa-spin"></i>
</div>
<gl-pagination
v-if='pageInfo.total > pageInfo.perPage'
:pagenum='pagenum'
:change='change'
:count='count.all'
:pageInfo='pageInfo'
>
</gl-pagination>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStage = Vue.extend({
data() {
return {
request: false,
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
};
},
props: ['stage', 'svgs', 'match'],
methods: {
fetchBuilds() {
if (this.request) return this.clearBuilds();
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.request = true;
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
this.request = false;
return flash;
});
},
clearBuilds() {
this.builds = '';
this.request = false;
},
},
computed: {
buildsOrSpinner() {
return this.request ? this.builds : this.spinner;
},
dropdownClass() {
if (this.request) return 'js-builds-dropdown-container';
return 'js-builds-dropdown-loading builds-dropdown-loading';
},
buildStatus() {
return `Build: ${this.stage.status.label}`;
},
tooltip() {
return `has-tooltip ci-status-icon ci-status-icon-${this.stage.status.group}`;
},
svg() {
const icon = this.stage.status.icon;
const stageIcon = icon.replace(/icon/i, 'stage_icon');
return this.svgs[this.match(stageIcon)];
},
triggerButtonClass() {
return `mini-pipeline-graph-dropdown-toggle has-tooltip js-builds-dropdown-button ci-status-icon-${this.stage.status.group}`;
},
},
template: `
<div>
<button
@click='fetchBuilds'
@blur='fetchBuilds'
:class="triggerButtonClass"
:title='stage.title'
data-placement="top"
data-toggle="dropdown"
type="button">
<span v-html="svg"></span>
<i class="fa fa-caret-down "></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up"></div>
<div :class="dropdownClass" class="js-builds-dropdown-list scrollable-menu" v-html="buildsOrSpinner"></div>
</ul>
</div>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStages = Vue.extend({
components: {
'vue-stage': gl.VueStage,
},
props: ['pipeline', 'svgs', 'match'],
template: `
<td class="stage-cell">
<div
class="stage-container dropdown js-mini-pipeline-graph"
v-for='stage in pipeline.details.stages'
>
<vue-stage :stage='stage' :svgs='svgs' :match='match'></vue-stage>
</div>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueStatusScope = Vue.extend({
props: [
'pipeline', 'svgs', 'match',
],
computed: {
cssClasses() {
const cssObject = { 'ci-status': true };
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
return cssObject;
},
svg() {
return this.svgs[this.match(this.pipeline.details.status.icon)];
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
},
template: `
<td class="commit-link">
<a
:class='cssClasses'
:href='detailsPath'
v-html='svg + pipeline.details.status.text'
>
</a>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
/*= require vue_realtime_listener/index.js */
((gl) => {
const pageValues = (headers) => {
const normalizedHeaders = {};
Object.keys(headers).forEach((e) => {
normalizedHeaders[e.toUpperCase()] = headers[e];
});
const paginationInfo = {
perPage: +normalizedHeaders['X-PER-PAGE'],
page: +normalizedHeaders['X-PAGE'],
total: +normalizedHeaders['X-TOTAL'],
totalPages: +normalizedHeaders['X-TOTAL-PAGES'],
nextPage: +normalizedHeaders['X-NEXT-PAGE'],
previousPage: +normalizedHeaders['X-PREV-PAGE'],
};
return paginationInfo;
};
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
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);
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('Something went wrong on our end.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children
.filter(e => e.$options._componentTag === 'time-ago')
.forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
}
};
})(window.gl || (window.gl = {}));
/* global Vue, gl */
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
return {
currentTime: new Date(),
};
},
props: ['pipeline', 'svgs'],
computed: {
timeAgo() {
return gl.utils.getTimeago();
},
localTimeFinished() {
return gl.utils.formatDate(this.pipeline.details.finished_at);
},
timeStopped() {
const changeTime = this.currentTime;
const options = {
weekday: 'long',
year: 'numeric',
month: 'short',
day: 'numeric',
};
options.timeZoneName = 'short';
const finished = this.pipeline.details.finished_at;
if (!finished && changeTime) return false;
return ({ words: this.timeAgo.format(finished) });
},
duration() {
const { duration } = this.pipeline.details;
const date = new Date(duration * 1000);
let hh = date.getUTCHours();
let mm = date.getUTCMinutes();
let ss = date.getSeconds();
if (hh < 10) hh = `0${hh}`;
if (mm < 10) mm = `0${mm}`;
if (ss < 10) ss = `0${ss}`;
if (duration !== null) return `${hh}:${mm}:${ss}`;
return false;
},
},
methods: {
changeTime() {
this.currentTime = new Date();
},
},
template: `
<td>
<p class="duration" v-if='duration'>
<span v-html='svgs.iconTimer'></span>
{{duration}}
</p>
<p class="finished-at" v-if='timeStopped'>
<i class="fa fa-calendar"></i>
<time
data-toggle="tooltip"
data-placement="top"
data-container="body"
:data-original-title='localTimeFinished'
>
{{timeStopped.words}}
</time>
</p>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* eslint-disable no-param-reassign */
((gl) => {
gl.VueRealtimeListener = (removeIntervals, startIntervals) => {
const removeAll = () => {
removeIntervals();
window.removeEventListener('beforeunload', removeIntervals);
window.removeEventListener('focus', startIntervals);
window.removeEventListener('blur', removeIntervals);
document.removeEventListener('page:fetch', removeAll);
};
window.addEventListener('beforeunload', removeIntervals);
window.addEventListener('focus', startIntervals);
window.addEventListener('blur', removeIntervals);
document.addEventListener('page:fetch', removeAll);
};
})(window.gl || (window.gl = {}));
...@@ -23,3 +23,118 @@ ...@@ -23,3 +23,118 @@
} }
} }
.filtered-search-container {
display: -webkit-flex;
display: flex;
}
.filtered-search-input-container {
display: -webkit-flex;
display: flex;
position: relative;
width: 100%;
.form-control {
padding-left: 25px;
padding-right: 25px;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
}
.fa-filter {
position: absolute;
top: 10px;
left: 10px;
color: $gray-darkest;
}
.fa-times {
right: 10px;
color: $gray-darkest;
}
.clear-search {
width: 35px;
background-color: transparent;
border: none;
position: absolute;
right: 0;
height: 100%;
outline: none;
&:hover .fa-times {
color: $common-gray-dark;
}
}
}
.dropdown-menu .filter-dropdown-item {
padding: 0;
}
.filter-dropdown {
max-height: 215px;
overflow-x: scroll;
}
.filter-dropdown-item {
.btn {
border: none;
width: 100%;
text-align: left;
padding: 8px 16px;
text-overflow: ellipsis;
overflow-y: hidden;
border-radius: 0;
.fa {
width: 15px;
}
.dropdown-label-box {
border-color: $white-light;
border-style: solid;
border-width: 1px;
width: 17px;
height: 17px;
}
&:hover,
&:focus {
background-color: $dropdown-hover-color;
color: $white-light;
text-decoration: none;
.avatar {
border-color: $white-light;
}
}
}
.dropdown-light-content {
font-size: 14px;
font-weight: 400;
}
.dropdown-user {
display: -webkit-flex;
display: flex;
}
.dropdown-user-details {
display: -webkit-flex;
display: flex;
-webkit-flex-direction: column;
flex-direction: column;
}
}
.hint-dropdown {
width: 250px;
}
.filter-dropdown-loading {
padding: 8px 16px;
}
...@@ -41,6 +41,21 @@ body { ...@@ -41,6 +41,21 @@ body {
} }
} }
.alert-link-group {
float: right;
}
/* Center alert text and alert action links on smaller screens */
@media (max-width: $screen-sm-max) {
.alert {
text-align: center;
}
.alert-link-group {
float: none;
}
}
/* Stripe the background colors so that adjacent alert-warnings are distinct from one another */ /* Stripe the background colors so that adjacent alert-warnings are distinct from one another */
.alert-warning { .alert-warning {
transition: background-color 0.15s, border-color 0.15s; transition: background-color 0.15s, border-color 0.15s;
......
...@@ -23,21 +23,21 @@ ...@@ -23,21 +23,21 @@
margin-right: 0; margin-right: 0;
} }
.issues-details-filters, .issues-details-filters:not(.filtered-search-block),
.dash-projects-filters, .dash-projects-filters,
.check-all-holder { .check-all-holder {
display: none; display: none;
} }
.rss-btn { .issues-holder .issue-check {
display: none; display: none;
} }
.project-home-links { .rss-btn {
display: none; display: none;
} }
.project-avatar { .project-home-links {
display: none; display: none;
} }
......
...@@ -183,9 +183,11 @@ ...@@ -183,9 +183,11 @@
&.right-sidebar-expanded { &.right-sidebar-expanded {
.line-resolve-all-container { .line-resolve-all-container {
@media (min-width: $sidebar-breakpoint) {
display: none; display: none;
} }
} }
}
} }
header.header-sidebar-pinned { header.header-sidebar-pinned {
......
...@@ -263,6 +263,11 @@ $dropdown-chevron-size: 10px; ...@@ -263,6 +263,11 @@ $dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%); $dropdown-toggle-active-border-color: darken($border-color, 14%);
/*
* Filtered Search
*/
$dropdown-hover-color: #3b86ff;
/* /*
* Buttons * Buttons
*/ */
......
// Limit MR description for side-by-side diff view
.fixed-width-container {
max-width: $limited-layout-width - ($gl-padding * 2);
margin-left: auto;
margin-right: auto;
}
.limit-container-width {
.detail-page-header {
@extend .fixed-width-container;
}
.issuable-details {
.detail-page-description,
.mr-source-target,
.mr-state-widget,
.merge-manually {
@extend .fixed-width-container;
}
.merge-request-tabs-holder {
&.affix {
border-bottom: 1px solid $border-color;
.nav-links {
border: 0;
}
}
.container-fluid {
@extend .fixed-width-container;
}
}
}
.merge-request-details {
.emoji-list-container {
@extend .fixed-width-container;
}
}
.diffs {
.mr-version-controls,
.files-changed {
@extend .fixed-width-container;
}
}
}
.issuable-details { .issuable-details {
section { section {
.issuable-discussion { .issuable-discussion {
......
...@@ -9,3 +9,13 @@ ...@@ -9,3 +9,13 @@
color: $lint-correct-color; color: $lint-correct-color;
} }
} }
.ci-linter {
.ci-editor {
height: 400px;
}
.ci-template pre {
white-space: pre-wrap;
}
}
...@@ -109,6 +109,10 @@ ...@@ -109,6 +109,10 @@
.avatar { .avatar {
float: none; float: none;
} }
> a:not(:last-of-type) {
margin-right: 5px;
}
} }
} }
......
...@@ -526,8 +526,9 @@ ul.notes { ...@@ -526,8 +526,9 @@ ul.notes {
} }
.line-resolve-all { .line-resolve-all {
vertical-align: middle;
display: inline-block; display: inline-block;
padding: 5px 10px; padding: 6px 10px;
background-color: $gray-light; background-color: $gray-light;
border: 1px solid $border-color; border: 1px solid $border-color;
border-radius: $border-radius-default; border-radius: $border-radius-default;
...@@ -535,18 +536,14 @@ ul.notes { ...@@ -535,18 +536,14 @@ ul.notes {
&.has-next-btn { &.has-next-btn {
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
border-right: 0;
} }
.line-resolve-btn { .line-resolve-btn {
vertical-align: middle;
margin-right: 5px; margin-right: 5px;
} }
} }
.line-resolve-text {
vertical-align: middle;
}
.line-resolve-btn { .line-resolve-btn {
display: inline-block; display: inline-block;
position: relative; position: relative;
......
.pipelines { .pipelines {
.realtime-loading {
font-size: 40px;
text-align: center;
}
.stage { .stage {
max-width: 90px; max-width: 90px;
width: 90px; width: 90px;
...@@ -24,6 +29,10 @@ ...@@ -24,6 +29,10 @@
min-width: 1200px; min-width: 1200px;
table-layout: fixed; table-layout: fixed;
.label {
margin-bottom: 3px;
}
.pipeline-id { .pipeline-id {
color: $black; color: $black;
} }
...@@ -177,6 +186,7 @@ ...@@ -177,6 +186,7 @@
.stage-cell { .stage-cell {
font-size: 0; font-size: 0;
> .stage-container > div > button > span > svg,
> .stage-container > button > svg { > .stage-container > button > svg {
height: 22px; height: 22px;
width: 22px; width: 22px;
......
...@@ -587,11 +587,21 @@ pre.light-well { ...@@ -587,11 +587,21 @@ pre.light-well {
.project-full-name { .project-full-name {
@include str-truncated; @include str-truncated;
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
} }
.controls { .controls {
line-height: $list-text-height; line-height: $list-text-height;
.badge {
@media (max-width: $screen-xs-max) {
display: none;
}
}
a:hover { a:hover {
text-decoration: none; text-decoration: none;
} }
...@@ -605,6 +615,12 @@ pre.light-well { ...@@ -605,6 +615,12 @@ pre.light-well {
top: 2px; top: 2px;
} }
} }
.description p {
@media (max-width: $screen-xs-max) {
max-width: 50%;
}
}
} }
.bottom { .bottom {
......
...@@ -67,69 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController ...@@ -67,69 +67,78 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file] params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit( params.require(:application_setting).permit(
:default_projects_limit, application_setting_params_ce
:default_branch_protection, )
:signup_enabled, end
:signin_enabled,
:require_two_factor_authentication, def application_setting_params_ce
:two_factor_grace_period, [
:gravatar_enabled, :admin_notification_email,
:sign_in_text,
:after_sign_up_text,
:help_page_text,
:home_page_url,
:after_sign_out_path, :after_sign_out_path,
:max_attachment_size, :after_sign_up_text,
:session_expire_delay, :akismet_api_key,
:akismet_enabled,
:container_registry_token_expire_delay,
:default_branch_protection,
:default_group_visibility,
:default_project_visibility, :default_project_visibility,
:default_projects_limit,
:default_snippet_visibility, :default_snippet_visibility,
:default_group_visibility,
:domain_whitelist_raw,
:domain_blacklist_enabled, :domain_blacklist_enabled,
:domain_blacklist_raw,
:domain_blacklist_file, :domain_blacklist_file,
:version_check_enabled, :domain_blacklist_raw,
:admin_notification_email, :domain_whitelist_raw,
:user_oauth_applications, :email_author_in_body,
:user_default_external, :enabled_git_access_protocol,
:shared_runners_enabled, :gravatar_enabled,
:shared_runners_text, :help_page_text,
:home_page_url,
:housekeeping_bitmaps_enabled,
:housekeeping_enabled,
:housekeeping_full_repack_period,
:housekeeping_gc_period,
:housekeeping_incremental_repack_period,
:html_emails_enabled,
:koding_enabled,
:koding_url,
:plantuml_enabled,
:plantuml_url,
:max_artifacts_size, :max_artifacts_size,
:max_attachment_size,
:metrics_enabled, :metrics_enabled,
:metrics_host, :metrics_host,
:metrics_port,
:metrics_pool_size,
:metrics_timeout,
:metrics_method_call_threshold, :metrics_method_call_threshold,
:metrics_packet_size,
:metrics_pool_size,
:metrics_port,
:metrics_sample_interval, :metrics_sample_interval,
:metrics_timeout,
:recaptcha_enabled, :recaptcha_enabled,
:recaptcha_site_key,
:recaptcha_private_key, :recaptcha_private_key,
:sentry_enabled, :recaptcha_site_key,
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
:koding_enabled,
:koding_url,
:email_author_in_body,
:html_emails_enabled,
:repository_checks_enabled, :repository_checks_enabled,
:metrics_packet_size, :require_two_factor_authentication,
:session_expire_delay,
:sign_in_text,
:signin_enabled,
:signup_enabled,
:sentry_dsn,
:sentry_enabled,
:send_user_confirmation_email, :send_user_confirmation_email,
:container_registry_token_expire_delay, :shared_runners_enabled,
:enabled_git_access_protocol, :shared_runners_text,
:sidekiq_throttling_enabled, :sidekiq_throttling_enabled,
:sidekiq_throttling_factor, :sidekiq_throttling_factor,
:housekeeping_enabled, :two_factor_grace_period,
:housekeeping_bitmaps_enabled, :user_default_external,
:housekeeping_incremental_repack_period, :user_oauth_applications,
:housekeeping_full_repack_period, :version_check_enabled,
:housekeeping_gc_period,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [], repository_storages: [],
restricted_visibility_levels: [], restricted_visibility_levels: [],
import_sources: [],
disabled_oauth_sign_in_sources: [],
sidekiq_throttling_queues: [] sidekiq_throttling_queues: []
) ]
end end
end end
...@@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -61,7 +61,11 @@ class Admin::GroupsController < Admin::ApplicationController
end end
def group_params def group_params
params.require(:group).permit( params.require(:group).permit(group_params_ce)
end
def group_params_ce
[
:avatar, :avatar,
:description, :description,
:lfs_enabled, :lfs_enabled,
...@@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController ...@@ -69,6 +73,6 @@ class Admin::GroupsController < Admin::ApplicationController
:path, :path,
:request_access_enabled, :request_access_enabled,
:visibility_level :visibility_level
) ]
end end
end end
...@@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -161,15 +161,6 @@ class Admin::UsersController < Admin::ApplicationController
@user ||= User.find_by!(username: params[:id]) @user ||= User.find_by!(username: params[:id])
end end
def user_params
params.require(:user).permit(
:email, :remember_me, :bio, :name, :username,
:skype, :linkedin, :twitter, :website_url, :color_scheme_id, :theme_id, :force_random_password,
:extern_uid, :provider, :password_expires_at, :avatar, :hide_no_ssh_key, :hide_no_password,
:projects_limit, :can_create_group, :admin, :key_id, :external
)
end
def redirect_back_or_admin_user(options = {}) def redirect_back_or_admin_user(options = {})
redirect_back_or_default(default: default_route, options: options) redirect_back_or_default(default: default_route, options: options)
end end
...@@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController ...@@ -177,4 +168,36 @@ class Admin::UsersController < Admin::ApplicationController
def default_route def default_route
[:admin, @user] [:admin, @user]
end end
def user_params
params.require(:user).permit(user_params_ce)
end
def user_params_ce
[
:admin,
:avatar,
:bio,
:can_create_group,
:color_scheme_id,
:email,
:extern_uid,
:external,
:force_random_password,
:hide_no_password,
:hide_no_ssh_key,
:key_id,
:linkedin,
:name,
:password_expires_at,
:projects_limit,
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url
]
end
end end
module ServiceParams module ServiceParams
extend ActiveSupport::Concern extend ActiveSupport::Concern
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_url, :api_version, :subdomain, ALLOWED_PARAMS_CE = [
:room, :recipients, :project_url, :webhook, :active,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password, :add_pusher,
:build_key, :server, :teamcity_url, :drone_url, :build_type, :api_key,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel, :api_url,
:colorize_messages, :channels, :api_version,
:bamboo_url,
:build_key,
:build_type,
:ca_pem,
:channel,
:channels,
:color,
:colorize_messages,
:confidential_issues_events,
:default_irc_uri,
:description,
:device,
:disable_diffs,
:drone_url,
:enable_ssl_verification,
:external_wiki_url,
# We're using `issues_events` and `merge_requests_events` # We're using `issues_events` and `merge_requests_events`
# in the view so we still need to explicitly state them # in the view so we still need to explicitly state them
# here. `Service#event_names` would only give # here. `Service#event_names` would only give
# `issue_events` and `merge_request_events` (singular!) # `issue_events` and `merge_request_events` (singular!)
# See app/helpers/services_helper.rb for how we # See app/helpers/services_helper.rb for how we
# make those event names plural as special case. # make those event names plural as special case.
:issues_events, :confidential_issues_events, :merge_requests_events, :issues_events,
:notify_only_broken_builds, :notify_only_broken_pipelines, :issues_url,
:add_pusher, :send_from_committer_email, :disable_diffs, :jira_issue_transition_id,
:external_wiki_url, :notify, :color, :merge_requests_events,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification, :namespace,
:jira_issue_transition_id, :url, :project_key, :ca_pem, :namespace] :new_issue_url,
:notify,
:notify_only_broken_builds,
:notify_only_broken_pipelines,
:password,
:priority,
:project_key,
:project_url,
:recipients,
:restrict_to_branch,
:room,
:send_from_committer_email,
:server,
:server_host,
:server_port,
:sound,
:subdomain,
:teamcity_url,
:title,
:token,
:type,
:url,
:user_key,
:username,
:webhook
]
# Parameters to ignore if no value is specified # Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password] FILTER_BLANK_PARAMS = [:password]
def service_params def service_params
dynamic_params = @service.event_channel_names + @service.event_names dynamic_params = @service.event_channel_names + @service.event_names
service_params = params.permit(:id, service: ALLOWED_PARAMS + dynamic_params) service_params = params.permit(:id, service: ALLOWED_PARAMS_CE + dynamic_params)
if service_params[:service].is_a?(Hash) if service_params[:service].is_a?(Hash)
FILTER_BLANK_PARAMS.each do |param| FILTER_BLANK_PARAMS.each do |param|
......
...@@ -125,7 +125,11 @@ class GroupsController < Groups::ApplicationController ...@@ -125,7 +125,11 @@ class GroupsController < Groups::ApplicationController
end end
def group_params def group_params
params.require(:group).permit( params.require(:group).permit(group_params_ce)
end
def group_params_ce
[
:avatar, :avatar,
:description, :description,
:lfs_enabled, :lfs_enabled,
...@@ -135,7 +139,7 @@ class GroupsController < Groups::ApplicationController ...@@ -135,7 +139,7 @@ class GroupsController < Groups::ApplicationController
:request_access_enabled, :request_access_enabled,
:share_with_group_lock, :share_with_group_lock,
:visibility_level :visibility_level
) ]
end end
def load_events def load_events
......
...@@ -409,10 +409,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -409,10 +409,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else else
ci_service = @merge_request.source_project.try(:ci_service) ci_service = @merge_request.source_project.try(:ci_service)
status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service status = ci_service.commit_status(merge_request.diff_head_sha, merge_request.source_branch) if ci_service
if ci_service.respond_to?(:commit_coverage)
coverage = ci_service.commit_coverage(merge_request.diff_head_sha, merge_request.source_branch)
end
end end
response = { response = {
......
...@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController ...@@ -7,11 +7,33 @@ class Projects::PipelinesController < Projects::ApplicationController
def index def index
@scope = params[:scope] @scope = params[:scope]
@pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30) @pipelines = PipelinesFinder
@pipelines = @pipelines.includes(project: :namespace) .new(project)
.execute(scope: @scope)
.page(params[:page])
.per(30)
@running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count @running_or_pending_count = PipelinesFinder
@pipelines_count = PipelinesFinder.new(project).execute.count .new(project).execute(scope: 'running').count
@pipelines_count = PipelinesFinder
.new(project).execute.count
respond_to do |format|
format.html
format.json do
render json: {
pipelines: PipelineSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@pipelines),
count: {
all: @pipelines_count,
running_or_pending: @running_or_pending_count
}
}
end
end
end end
def new def new
......
...@@ -165,31 +165,53 @@ class IssuableFinder ...@@ -165,31 +165,53 @@ class IssuableFinder
end end
end end
def assignee? def assignee_id?
params[:assignee_id].present? params[:assignee_id].present? && params[:assignee_id] != NONE
end
def assignee_username?
params[:assignee_username].present? && params[:assignee_username] != NONE
end
def no_assignee?
# Assignee_id takes precedence over assignee_username
params[:assignee_id] == NONE || params[:assignee_username] == NONE
end end
def assignee def assignee
return @assignee if defined?(@assignee) return @assignee if defined?(@assignee)
@assignee = @assignee =
if assignee? && params[:assignee_id] != NONE if assignee_id?
User.find(params[:assignee_id]) User.find_by(id: params[:assignee_id])
elsif assignee_username?
User.find_by(username: params[:assignee_username])
else else
nil nil
end end
end end
def author? def author_id?
params[:author_id].present? params[:author_id].present? && params[:author_id] != NONE
end
def author_username?
params[:author_username].present? && params[:author_username] != NONE
end
def no_author?
# author_id takes precedence over author_username
params[:author_id] == NONE || params[:author_username] == NONE
end end
def author def author
return @author if defined?(@author) return @author if defined?(@author)
@author = @author =
if author? && params[:author_id] != NONE if author_id?
User.find(params[:author_id]) User.find_by(id: params[:author_id])
elsif author_username?
User.find_by(username: params[:author_username])
else else
nil nil
end end
...@@ -263,16 +285,24 @@ class IssuableFinder ...@@ -263,16 +285,24 @@ class IssuableFinder
end end
def by_assignee(items) def by_assignee(items)
if assignee? if assignee
items = items.where(assignee_id: assignee.try(:id)) items = items.where(assignee_id: assignee.id)
elsif no_assignee?
items = items.where(assignee_id: nil)
elsif assignee_id? || assignee_username? # assignee not found
items = items.none
end end
items items
end end
def by_author(items) def by_author(items)
if author? if author
items = items.where(author_id: author.try(:id)) items = items.where(author_id: author.id)
elsif no_author?
items = items.where(author_id: nil)
elsif author_id? || author_username? # author not found
items = items.none
end end
items items
......
...@@ -244,7 +244,9 @@ module ApplicationHelper ...@@ -244,7 +244,9 @@ module ApplicationHelper
scope: params[:scope], scope: params[:scope],
milestone_title: params[:milestone_title], milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id], assignee_id: params[:assignee_id],
assignee_username: params[:assignee_username],
author_id: params[:author_id], author_id: params[:author_id],
author_username: params[:author_username],
search: params[:search], search: params[:search],
label_name: params[:label_name] label_name: params[:label_name]
} }
......
...@@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -68,6 +68,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true, presence: true,
if: :koding_enabled if: :koding_enabled
validates :plantuml_url,
presence: true,
if: :plantuml_enabled
validates :max_attachment_size, validates :max_attachment_size,
presence: true, presence: true,
numericality: { only_integer: true, greater_than: 0 } numericality: { only_integer: true, greater_than: 0 }
...@@ -184,6 +188,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -184,6 +188,8 @@ class ApplicationSetting < ActiveRecord::Base
akismet_enabled: false, akismet_enabled: false,
koding_enabled: false, koding_enabled: false,
koding_url: nil, koding_url: nil,
plantuml_enabled: false,
plantuml_url: nil,
repository_checks_enabled: true, repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [], disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false, send_user_confirmation_email: false,
......
...@@ -142,7 +142,7 @@ module Ci ...@@ -142,7 +142,7 @@ module Ci
end end
def artifacts def artifacts
builds.latest.with_artifacts_not_expired builds.latest.with_artifacts_not_expired.includes(project: [:namespace])
end end
def project_id def project_id
...@@ -191,7 +191,11 @@ module Ci ...@@ -191,7 +191,11 @@ module Ci
end end
def manual_actions def manual_actions
builds.latest.manual_actions builds.latest.manual_actions.includes(project: [:namespace])
end
def stuck?
builds.pending.any?(&:stuck?)
end end
def retryable? def retryable?
...@@ -283,6 +287,10 @@ module Ci ...@@ -283,6 +287,10 @@ module Ci
end end
end end
def has_yaml_errors?
yaml_errors.present?
end
def environments def environments
builds.where.not(environment: nil).success.pluck(:environment).uniq builds.where.not(environment: nil).success.pluck(:environment).uniq
end end
......
...@@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base ...@@ -137,4 +137,10 @@ class CommitStatus < ActiveRecord::Base
.new(self, current_user) .new(self, current_user)
.fabricate! .fabricate!
end end
def sortable_name
name.split(/(\d+)/).map do |v|
v =~ /\d+/ ? v.to_i : v
end
end
end end
...@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility ...@@ -32,6 +32,6 @@ module ProjectFeaturesCompatibility
build_project_feature unless project_feature build_project_feature unless project_feature
access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.update_attribute(field, access_level) project_feature.send(:write_attribute, field, access_level)
end end
end end
...@@ -55,30 +55,30 @@ module ReactiveCaching ...@@ -55,30 +55,30 @@ module ReactiveCaching
self.reactive_cache_refresh_interval = 1.minute self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 10.minutes self.reactive_cache_lifetime = 10.minutes
def calculate_reactive_cache def calculate_reactive_cache(*args)
raise NotImplementedError raise NotImplementedError
end end
def with_reactive_cache(&blk) def with_reactive_cache(*args, &blk)
within_reactive_cache_lifetime do within_reactive_cache_lifetime(*args) do
data = Rails.cache.read(full_reactive_cache_key) data = Rails.cache.read(full_reactive_cache_key(*args))
yield data if data.present? yield data if data.present?
end end
ensure ensure
Rails.cache.write(full_reactive_cache_key('alive'), true, expires_in: self.class.reactive_cache_lifetime) Rails.cache.write(alive_reactive_cache_key(*args), true, expires_in: self.class.reactive_cache_lifetime)
ReactiveCachingWorker.perform_async(self.class, id) ReactiveCachingWorker.perform_async(self.class, id, *args)
end end
def clear_reactive_cache! def clear_reactive_cache!(*args)
Rails.cache.delete(full_reactive_cache_key) Rails.cache.delete(full_reactive_cache_key(*args))
end end
def exclusively_update_reactive_cache! def exclusively_update_reactive_cache!(*args)
locking_reactive_cache do locking_reactive_cache(*args) do
within_reactive_cache_lifetime do within_reactive_cache_lifetime(*args) do
enqueuing_update do enqueuing_update(*args) do
value = calculate_reactive_cache value = calculate_reactive_cache(*args)
Rails.cache.write(full_reactive_cache_key, value) Rails.cache.write(full_reactive_cache_key(*args), value)
end end
end end
end end
...@@ -93,22 +93,26 @@ module ReactiveCaching ...@@ -93,22 +93,26 @@ module ReactiveCaching
([prefix].flatten + qualifiers).join(':') ([prefix].flatten + qualifiers).join(':')
end end
def locking_reactive_cache def alive_reactive_cache_key(*qualifiers)
lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key, timeout: reactive_cache_lease_timeout) full_reactive_cache_key(*(qualifiers + ['alive']))
end
def locking_reactive_cache(*args)
lease = Gitlab::ExclusiveLease.new(full_reactive_cache_key(*args), timeout: reactive_cache_lease_timeout)
uuid = lease.try_obtain uuid = lease.try_obtain
yield if uuid yield if uuid
ensure ensure
Gitlab::ExclusiveLease.cancel(full_reactive_cache_key, uuid) Gitlab::ExclusiveLease.cancel(full_reactive_cache_key(*args), uuid)
end end
def within_reactive_cache_lifetime def within_reactive_cache_lifetime(*args)
yield if Rails.cache.read(full_reactive_cache_key('alive')) yield if Rails.cache.read(alive_reactive_cache_key(*args))
end end
def enqueuing_update def enqueuing_update(*args)
yield yield
ensure ensure
ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id) ReactiveCachingWorker.perform_in(self.class.reactive_cache_refresh_interval, self.class, id, *args)
end end
end end
end end
module ReactiveService
extend ActiveSupport::Concern
included do
include ReactiveCaching
# Default cache key: class name + project_id
self.reactive_cache_key = ->(service) { [ service.class.model_name.singular, service.project_id ] }
end
end
module ValidAttribute
extend ActiveSupport::Concern
# Checks whether an attribute has failed validation or not
#
# +attribute+ The symbolised name of the attribute i.e :name
def valid_attribute?(attribute)
self.errors.empty? || self.errors.messages[attribute].nil?
end
end
...@@ -31,7 +31,7 @@ class CycleAnalytics ...@@ -31,7 +31,7 @@ class CycleAnalytics
repository = @project.repository.raw_repository repository = @project.repository.raw_repository
sha = @project.repository.commit(ref).sha sha = @project.repository.commit(ref).sha
cmd = %W(git --git-dir=#{repository.path} log) cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{repository.path} log)
cmd << '--format=%H' cmd << '--format=%H'
cmd << "--after=#{@from.iso8601}" cmd << "--after=#{@from.iso8601}"
cmd << sha cmd << sha
......
...@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base ...@@ -87,7 +87,7 @@ class Environment < ActiveRecord::Base
end end
def update_merge_request_metrics? def update_merge_request_metrics?
self.name == "production" (environment_type || name) == "production"
end end
def first_deployment_for(commit) def first_deployment_for(commit)
......
class ForkedProjectLink < ActiveRecord::Base class ForkedProjectLink < ActiveRecord::Base
belongs_to :forked_to_project, class_name: Project belongs_to :forked_to_project, class_name: 'Project'
belongs_to :forked_from_project, class_name: Project belongs_to :forked_from_project, class_name: 'Project'
end end
...@@ -49,6 +49,10 @@ class Key < ActiveRecord::Base ...@@ -49,6 +49,10 @@ class Key < ActiveRecord::Base
"key-#{id}" "key-#{id}"
end end
def update_last_used_at
UseKeyWorker.perform_async(self.id)
end
def add_to_shell def add_to_shell
GitlabShellWorker.perform_async( GitlabShellWorker.perform_async(
:add_key, :add_key,
......
...@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base ...@@ -26,6 +26,7 @@ class Label < ActiveRecord::Base
# Don't allow ',' for label titles # Don't allow ',' for label titles
validates :title, presence: true, format: { with: /\A[^,]+\z/ } validates :title, presence: true, format: { with: /\A[^,]+\z/ }
validates :title, uniqueness: { scope: [:group_id, :project_id] } validates :title, uniqueness: { scope: [:group_id, :project_id] }
validates :title, length: { maximum: 255 }
default_scope { order(title: :asc) } default_scope { order(title: :asc) }
......
...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base ...@@ -37,6 +37,10 @@ class NotificationSetting < ActiveRecord::Base
:success_pipeline :success_pipeline
] ]
EXCLUDED_WATCHER_EVENTS = [
:success_pipeline
]
store :events, accessors: EMAIL_EVENTS, coder: JSON store :events, accessors: EMAIL_EVENTS, coder: JSON
before_create :set_events before_create :set_events
......
...@@ -12,6 +12,7 @@ class Project < ActiveRecord::Base ...@@ -12,6 +12,7 @@ class Project < ActiveRecord::Base
include AfterCommitQueue include AfterCommitQueue
include CaseSensitivity include CaseSensitivity
include TokenAuthenticatable include TokenAuthenticatable
include ValidAttribute
include ProjectFeaturesCompatibility include ProjectFeaturesCompatibility
include SelectForProjectAuthorization include SelectForProjectAuthorization
include Routable include Routable
...@@ -65,6 +66,8 @@ class Project < ActiveRecord::Base ...@@ -65,6 +66,8 @@ class Project < ActiveRecord::Base
end end
end end
after_validation :check_pending_delete
ActsAsTaggableOn.strict_case_match = true ActsAsTaggableOn.strict_case_match = true
acts_as_taggable_on :tags acts_as_taggable_on :tags
...@@ -119,7 +122,7 @@ class Project < ActiveRecord::Base ...@@ -119,7 +122,7 @@ class Project < ActiveRecord::Base
# Merge Requests for target project should be removed with it # Merge Requests for target project should be removed with it
has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id' has_many :merge_requests, dependent: :destroy, foreign_key: 'target_project_id'
# Merge requests from source project should be kept when source project was removed # Merge requests from source project should be kept when source project was removed
has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: MergeRequest has_many :fork_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
has_many :issues, dependent: :destroy has_many :issues, dependent: :destroy
has_many :labels, dependent: :destroy, class_name: 'ProjectLabel' has_many :labels, dependent: :destroy, class_name: 'ProjectLabel'
has_many :services, dependent: :destroy has_many :services, dependent: :destroy
...@@ -130,7 +133,7 @@ class Project < ActiveRecord::Base ...@@ -130,7 +133,7 @@ class Project < ActiveRecord::Base
has_many :hooks, dependent: :destroy, class_name: 'ProjectHook' has_many :hooks, dependent: :destroy, class_name: 'ProjectHook'
has_many :protected_branches, dependent: :destroy has_many :protected_branches, dependent: :destroy
has_many :project_authorizations, dependent: :destroy has_many :project_authorizations
has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User' has_many :authorized_users, through: :project_authorizations, source: :user, class_name: 'User'
has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source has_many :project_members, -> { where(requested_at: nil) }, dependent: :destroy, as: :source
alias_method :members, :project_members alias_method :members, :project_members
...@@ -1029,7 +1032,7 @@ class Project < ActiveRecord::Base ...@@ -1029,7 +1032,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}", "refs/heads/#{branch}",
force: true) force: true)
repository.copy_gitattributes(branch) repository.copy_gitattributes(branch)
repository.expire_avatar_cache repository.after_change_head
reload_default_branch reload_default_branch
end end
...@@ -1320,4 +1323,21 @@ class Project < ActiveRecord::Base ...@@ -1320,4 +1323,21 @@ class Project < ActiveRecord::Base
stats = statistics || build_statistics stats = statistics || build_statistics
stats.update(namespace_id: namespace_id) stats.update(namespace_id: namespace_id)
end end
def check_pending_delete
return if valid_attribute?(:name) && valid_attribute?(:path)
return unless pending_delete_twin
%i[route route.path name path].each do |error|
errors.delete(error)
end
errors.add(:base, "The project is still being deleted. Please try again later.")
end
def pending_delete_twin
return false unless path
Project.unscoped.where(pending_delete: true).find_with_namespace(path_with_namespace)
end
end end
class BambooService < CiService class BambooService < CiService
include ReactiveService
prop_accessor :bamboo_url, :build_key, :username, :password prop_accessor :bamboo_url, :build_key, :username, :password
validates :bamboo_url, presence: true, url: true, if: :activated? validates :bamboo_url, presence: true, url: true, if: :activated?
...@@ -58,31 +60,46 @@ class BambooService < CiService ...@@ -58,31 +60,46 @@ class BambooService < CiService
%w(push) %w(push)
end end
def build_info(sha) def build_page(sha, ref)
@response = get_path("rest/api/latest/result?label=#{sha}") with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
end end
def build_page(sha, ref) def commit_status(sha, ref)
build_info(sha) if @response.nil? || !@response.code with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
end
if @response.code != 200 || @response['results']['results']['size'] == '0' def execute(data)
return unless supported_events.include?(data[:object_kind])
get_path("updateAndBuild.action?buildKey=#{build_key}")
end
def calculate_reactive_cache(sha, ref)
response = get_path("rest/api/latest/result?label=#{sha}")
{ build_page: read_build_page(response), commit_status: read_commit_status(response) }
end
private
def read_build_page(response)
if response.code != 200 || response['results']['results']['size'] == '0'
# If actual build link can't be determined, send user to build summary page. # If actual build link can't be determined, send user to build summary page.
URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s URI.join("#{bamboo_url}/", "browse/#{build_key}").to_s
else else
# If actual build link is available, go to build result page. # If actual build link is available, go to build result page.
result_key = @response['results']['results']['result']['planResultKey']['key'] result_key = response['results']['results']['result']['planResultKey']['key']
URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s URI.join("#{bamboo_url}/", "browse/#{result_key}").to_s
end end
end end
def commit_status(sha, ref) def read_commit_status(response)
build_info(sha) if @response.nil? || !@response.code return :error unless response.code == 200 || response.code == 404
return :error unless @response.code == 200 || @response.code == 404
status = if @response.code == 404 || @response['results']['results']['size'] == '0' status = if response.code == 404 || response['results']['results']['size'] == '0'
'Pending' 'Pending'
else else
@response['results']['results']['result']['buildState'] response['results']['results']['result']['buildState']
end end
if status.include?('Success') if status.include?('Success')
...@@ -96,14 +113,6 @@ class BambooService < CiService ...@@ -96,14 +113,6 @@ class BambooService < CiService
end end
end end
def execute(data)
return unless supported_events.include?(data[:object_kind])
get_path("updateAndBuild.action?buildKey=#{build_key}")
end
private
def build_url(path) def build_url(path)
URI.join("#{bamboo_url}/", path).to_s URI.join("#{bamboo_url}/", path).to_s
end end
......
require "addressable/uri" require "addressable/uri"
class BuildkiteService < CiService class BuildkiteService < CiService
include ReactiveService
ENDPOINT = "https://buildkite.com" ENDPOINT = "https://buildkite.com"
prop_accessor :project_url, :token prop_accessor :project_url, :token
...@@ -33,13 +35,7 @@ class BuildkiteService < CiService ...@@ -33,13 +35,7 @@ class BuildkiteService < CiService
end end
def commit_status(sha, ref) def commit_status(sha, ref)
response = HTTParty.get(commit_status_path(sha), verify: false) with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
if response.code == 200 && response['status']
response['status']
else
:error
end
end end
def commit_status_path(sha) def commit_status_path(sha)
...@@ -78,6 +74,19 @@ class BuildkiteService < CiService ...@@ -78,6 +74,19 @@ class BuildkiteService < CiService
] ]
end end
def calculate_reactive_cache(sha, ref)
response = HTTParty.get(commit_status_path(sha), verify: false)
status =
if response.code == 200 && response['status']
response['status']
else
:error
end
{ commit_status: status }
end
private private
def webhook_token def webhook_token
......
...@@ -12,15 +12,7 @@ class CiService < Service ...@@ -12,15 +12,7 @@ class CiService < Service
%w(push) %w(push)
end end
def merge_request_page(iid, sha, ref) # Return complete url to build page
commit_page(sha, ref)
end
def commit_page(sha, ref)
build_page(sha, ref)
end
# Return complete url to merge_request page
# #
# Ex. # Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c # http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
...@@ -29,23 +21,6 @@ class CiService < Service ...@@ -29,23 +21,6 @@ class CiService < Service
# implement inside child # implement inside child
end end
# Return string with build status or :error symbol
#
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
#
#
# Ex.
# @service.merge_request_status(9, '13be4ac', 'dev')
# # => 'success'
#
# @service.merge_request_status(10, '2abe4ac', 'dev)
# # => 'running'
#
#
def merge_request_status(iid, sha, ref)
commit_status(sha, ref)
end
# Return string with build status or :error symbol # Return string with build status or :error symbol
# #
# Allowed states: 'success', 'failed', 'running', 'pending', 'skipped' # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
......
class DroneCiService < CiService class DroneCiService < CiService
include ReactiveService
prop_accessor :drone_url, :token prop_accessor :drone_url, :token
boolean_accessor :enable_ssl_verification boolean_accessor :enable_ssl_verification
...@@ -34,14 +36,6 @@ class DroneCiService < CiService ...@@ -34,14 +36,6 @@ class DroneCiService < CiService
%w(push merge_request tag_push) %w(push merge_request tag_push)
end end
def merge_request_status_path(iid, sha = nil, ref = nil)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
"?access_token=#{token}"]
URI.join(*url).to_s
end
def commit_status_path(sha, ref) def commit_status_path(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
...@@ -50,29 +44,14 @@ class DroneCiService < CiService ...@@ -50,29 +44,14 @@ class DroneCiService < CiService
URI.join(*url).to_s URI.join(*url).to_s
end end
def merge_request_status(iid, sha, ref) def commit_status(sha, ref)
response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification) with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
if response.code == 200 and response['status']
case response['status']
when 'killed'
:canceled
when 'failure', 'error'
# Because drone return error if some test env failed
:failed
else
response["status"]
end
else
:error
end
rescue Errno::ECONNREFUSED
:error
end end
def commit_status(sha, ref) def calculate_reactive_cache(sha, ref)
response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification) response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
status =
if response.code == 200 and response['status'] if response.code == 200 and response['status']
case response['status'] case response['status']
when 'killed' when 'killed'
...@@ -86,18 +65,13 @@ class DroneCiService < CiService ...@@ -86,18 +65,13 @@ class DroneCiService < CiService
else else
:error :error
end end
rescue Errno::ECONNREFUSED
:error
end
def merge_request_page(iid, sha, ref)
url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
URI.join(*url).to_s { commit_status: status }
rescue Errno::ECONNREFUSED
{ commit_status: :error }
end end
def commit_page(sha, ref) def build_page(sha, ref)
url = [drone_url, url = [drone_url,
"gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}", "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
"?branch=#{URI::encode(ref.to_s)}"] "?branch=#{URI::encode(ref.to_s)}"]
...@@ -105,14 +79,6 @@ class DroneCiService < CiService ...@@ -105,14 +79,6 @@ class DroneCiService < CiService
URI.join(*url).to_s URI.join(*url).to_s
end end
def commit_coverage(sha, ref)
nil
end
def build_page(sha, ref)
commit_page(sha, ref)
end
def title def title
'Drone CI' 'Drone CI'
end end
......
class TeamcityService < CiService class TeamcityService < CiService
include ReactiveService
prop_accessor :teamcity_url, :build_type, :username, :password prop_accessor :teamcity_url, :build_type, :username, :password
validates :teamcity_url, presence: true, url: true, if: :activated? validates :teamcity_url, presence: true, url: true, if: :activated?
...@@ -61,43 +63,18 @@ class TeamcityService < CiService ...@@ -61,43 +63,18 @@ class TeamcityService < CiService
] ]
end end
def build_info(sha)
@response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
end
def build_page(sha, ref) def build_page(sha, ref)
build_info(sha) if @response.nil? || !@response.code with_reactive_cache(sha, ref) {|cached| cached[:build_page] }
if @response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = @response['build']['id']
build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end end
def commit_status(sha, ref) def commit_status(sha, ref)
build_info(sha) if @response.nil? || !@response.code with_reactive_cache(sha, ref) {|cached| cached[:commit_status] }
return :error unless @response.code == 200 || @response.code == 404
status = if @response.code == 404
'Pending'
else
@response['build']['status']
end end
if status.include?('SUCCESS') def calculate_reactive_cache(sha, ref)
'success' response = get_path("httpAuth/app/rest/builds/branch:unspecified:any,number:#{sha}")
elsif status.include?('FAILURE')
'failed' { build_page: read_build_page(response), commit_status: read_commit_status(response) }
elsif status.include?('Pending')
'pending'
else
:error
end
end end
def execute(data) def execute(data)
...@@ -122,6 +99,40 @@ class TeamcityService < CiService ...@@ -122,6 +99,40 @@ class TeamcityService < CiService
private private
def read_build_page(response)
if response.code != 200
# If actual build link can't be determined,
# send user to build summary page.
build_url("viewLog.html?buildTypeId=#{build_type}")
else
# If actual build link is available, go to build result page.
built_id = response['build']['id']
build_url("viewLog.html?buildId=#{built_id}&buildTypeId=#{build_type}")
end
end
def read_commit_status(response)
return :error unless response.code == 200 || response.code == 404
status = if response.code == 404
'Pending'
else
response['build']['status']
end
return :error unless status.present?
if status.include?('SUCCESS')
'success'
elsif status.include?('FAILURE')
'failed'
elsif status.include?('Pending')
'pending'
else
:error
end
end
def build_url(path) def build_url(path)
URI.join("#{teamcity_url}/", path).to_s URI.join("#{teamcity_url}/", path).to_s
end end
......
...@@ -439,6 +439,11 @@ class Repository ...@@ -439,6 +439,11 @@ class Repository
expire_content_cache expire_content_cache
end end
# Runs code after the HEAD of a repository is changed.
def after_change_head
expire_method_caches(METHOD_CACHES_FOR_FILE_TYPES.keys)
end
# Runs code after a repository has been forked/imported. # Runs code after a repository has been forked/imported.
def after_import def after_import
expire_content_cache expire_content_cache
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -21,5 +21,5 @@ ...@@ -21,5 +21,5 @@
= render 'shared/group_tips' = render 'shared/group_tips'
.form-actions .form-actions
= f.submit 'Create group', class: "btn btn-create", tabindex: 3 = f.submit 'Create group', class: "btn btn-create"
= link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel' = link_to 'Cancel', dashboard_groups_path, class: 'btn btn-cancel'
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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