Commit 557c5b0d authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'master' into 29575-polling

* master: (71 commits)
  Merge branch 'render-json-leak' into 'security'
  Merge branch 'ssrf' into 'security'
  Merge branch 'ssrf' into 'security'
  Merge branch 'fix-links-target-blank' into 'security'
  Merge branch '28058-hide-emails-in-atom-feeds' into 'security'
  Fix karma test
  Reset filters after click
  Handle Route#name being nil after an update
  Only add frontend code coverage instrumentation when generating coverage report
  fix recompile assets step in 9.0 upgrade guide to use yarn
  Undo explicit conversion to Integer
  Make level_value accept string integers
  Make feature spec more robust
  Removed d3.js from the main application.js bundle
  Update css to be nice and tidy.
  add an index to the ghost column
  Improve rename projects migration
  Add additional check for when inputContainer does not exist
  Make the v3_to_v4.md more consistent
  Fix input token spacing
  ...
parents 19a6cc26 4146be04
{
"presets": [
["latest", { "es2015": { "modules": false } }],
"stage-2"
],
"env": {
"coverage": {
"plugins": [
["istanbul", {
"exclude": [
"app/assets/javascripts/droplab/**/*",
"spec/javascripts/**/*"
]
}],
["transform-define", {
"process.env.BABEL_ENV": "coverage"
}]
]
}
}
}
...@@ -277,6 +277,8 @@ rake karma: ...@@ -277,6 +277,8 @@ rake karma:
stage: test stage: test
<<: *use-db <<: *use-db
<<: *dedicated-runner <<: *dedicated-runner
variables:
BABEL_ENV: "coverage"
script: script:
- bundle exec rake karma - bundle exec rake karma
artifacts: artifacts:
...@@ -389,9 +391,11 @@ trigger_docs: ...@@ -389,9 +391,11 @@ trigger_docs:
cache: {} cache: {}
artifacts: {} artifacts: {}
script: script:
- "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/1794617/trigger/builds" - "HTTP_STATUS=$(curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=${CI_PROJECT_NAME} --silent --output curl.log --write-out '%{http_code}' https://gitlab.com/api/v3/projects/1794617/trigger/builds)"
- if [ "${HTTP_STATUS}" -ne "201" ]; then echo "Error ${HTTP_STATUS}"; cat curl.log; echo; exit 1; fi
only: only:
- master@gitlab-org/gitlab-ce - master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
# Notify slack in the end # Notify slack in the end
notify:slack: notify:slack:
......
...@@ -2,6 +2,11 @@ ...@@ -2,6 +2,11 @@
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.17.4 (2017-03-19)
- Only show public emails in atom feeds.
- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
## 8.17.3 (2017-03-07) ## 8.17.3 (2017-03-07)
- Fix the redirect to custom home page URL. !9518 - Fix the redirect to custom home page URL. !9518
...@@ -210,6 +215,11 @@ entry. ...@@ -210,6 +215,11 @@ entry.
- Remove deprecated GitlabCiService. - Remove deprecated GitlabCiService.
- Requeue pending deletion projects. - Requeue pending deletion projects.
## 8.16.8 (2017-03-19)
- Only show public emails in atom feeds.
- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
## 8.16.7 (2017-02-27) ## 8.16.7 (2017-02-27)
- No changes. - No changes.
...@@ -411,6 +421,11 @@ entry. ...@@ -411,6 +421,11 @@ entry.
- Add margin to markdown math blocks. - Add margin to markdown math blocks.
- Add hover state to MR comment reply button. - Add hover state to MR comment reply button.
## 8.15.8 (2017-03-19)
- Only show public emails in atom feeds.
- To protect against Server-side Request Forgery project import URLs are now prohibited against localhost or the server IP except for the assigned instance URL and port. Imports are also prohibited from ports below 1024 with the exception of ports 22, 80, and 443.
## 8.15.7 (2017-02-15) ## 8.15.7 (2017-02-15)
- No changes. - No changes.
......
...@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0' ...@@ -352,4 +352,4 @@ gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6' gem 'sys-filesystem', '~> 1.1.6'
# Gitaly GRPC client # Gitaly GRPC client
gem 'gitaly', '~> 0.2.1' gem 'gitaly', '~> 0.3.0'
...@@ -250,7 +250,7 @@ GEM ...@@ -250,7 +250,7 @@ GEM
json json
get_process_mem (0.2.0) get_process_mem (0.2.0)
gherkin-ruby (0.3.2) gherkin-ruby (0.3.2)
gitaly (0.2.1) gitaly (0.3.0)
google-protobuf (~> 3.1) google-protobuf (~> 3.1)
grpc (~> 1.0) grpc (~> 1.0)
github-linguist (4.7.6) github-linguist (4.7.6)
...@@ -304,7 +304,7 @@ GEM ...@@ -304,7 +304,7 @@ GEM
multi_json (~> 1.10) multi_json (~> 1.10)
retriable (~> 1.4) retriable (~> 1.4)
signet (~> 0.6) signet (~> 0.6)
google-protobuf (3.2.0) google-protobuf (3.2.0.2)
googleauth (0.5.1) googleauth (0.5.1)
faraday (~> 0.9) faraday (~> 0.9)
jwt (~> 1.4) jwt (~> 1.4)
...@@ -896,7 +896,7 @@ DEPENDENCIES ...@@ -896,7 +896,7 @@ DEPENDENCIES
fuubar (~> 2.0.0) fuubar (~> 2.0.0)
gemnasium-gitlab-service (~> 0.2) gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.0) gemojione (~> 3.0)
gitaly (~> 0.2.1) gitaly (~> 0.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.1) gitlab-markup (~> 1.5.1)
......
...@@ -30,7 +30,7 @@ ...@@ -30,7 +30,7 @@
if (this.activeTab === 'selected') { if (this.activeTab === 'selected') {
obj.title = 'You haven\'t selected any issues yet'; obj.title = 'You haven\'t selected any issues yet';
obj.content = ` obj.content = `
Go back to <strong>All issues</strong> and select some issues Go back to <strong>Open issues</strong> and select some issues
to add to your board. to add to your board.
`; `;
} }
...@@ -59,7 +59,7 @@ ...@@ -59,7 +59,7 @@
class="btn btn-default" class="btn btn-default"
@click="changeTab('all')" @click="changeTab('all')"
v-if="activeTab === 'selected'"> v-if="activeTab === 'selected'">
All issues Open issues
</button> </button>
</div> </div>
</div> </div>
......
...@@ -64,6 +64,7 @@ require('./empty_state'); ...@@ -64,6 +64,7 @@ require('./empty_state');
}, },
filter: { filter: {
handler() { handler() {
this.page = 1;
this.loadIssues(true); this.loadIssues(true);
}, },
deep: true, deep: true,
...@@ -115,6 +116,9 @@ require('./empty_state'); ...@@ -115,6 +116,9 @@ require('./empty_state');
return this.activeTab === 'selected' && this.selectedIssues.length === 0; return this.activeTab === 'selected' && this.selectedIssues.length === 0;
}, },
}, },
created() {
this.page = 1;
},
components: { components: {
'modal-header': gl.issueBoards.ModalHeader, 'modal-header': gl.issueBoards.ModalHeader,
'modal-list': gl.issueBoards.ModalList, 'modal-list': gl.issueBoards.ModalList,
......
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
href="#" href="#"
role="button" role="button"
@click.prevent="changeTab('all')"> @click.prevent="changeTab('all')">
All issues Open issues
<span class="badge"> <span class="badge">
{{ issuesCount }} {{ issuesCount }}
</span> </span>
......
/* eslint-disable no-new, no-param-reassign */ /* eslint-disable no-param-reassign */
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ import CommitPipelinesTable from './pipelines_table';
window.Vue = require('vue'); window.Vue = require('vue');
require('./pipelines_table'); window.Vue.use(require('vue-resource'));
/** /**
* Commits View > Pipelines Tab > Pipelines Table. * Commits View > Pipelines Tab > Pipelines Table.
* Merge Request View > Pipelines Tab > Pipelines Table. * Merge Request View > Pipelines Tab > Pipelines Table.
...@@ -21,7 +22,7 @@ $(() => { ...@@ -21,7 +22,7 @@ $(() => {
} }
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view'); const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView(); gl.commits.pipelines.PipelinesTableBundle = new CommitPipelinesTable();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) { if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl); gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
......
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
/**
* Pipelines service.
*
* Used to fetch the data used to render the pipelines table.
* Uses Vue.Resource
*/
class PipelinesService {
/**
* FIXME: The url provided to request the pipelines in the new merge request
* page already has `.json`.
* This should be fixed when the endpoint is improved.
*
* @param {String} root
*/
constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`;
} else {
endpoint = root;
}
this.pipelines = Vue.resource(endpoint);
}
/**
* Given the root param provided when the class is initialized, will
* make a GET request.
*
* @return {Promise}
*/
all() {
return this.pipelines.get();
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesService = PipelinesService;
/* eslint-disable no-new, no-param-reassign */ /* eslint-disable no-new*/
/* global Vue, CommitsPipelineStore, PipelinesService, Flash */ /* global Flash */
import Vue from 'vue';
window.Vue = require('vue'); import PipelinesTableComponent from '../../vue_shared/components/pipelines_table';
window.Vue.use(require('vue-resource')); import PipelinesService from '../../vue_pipelines_index/services/pipelines_service';
require('../../lib/utils/common_utils'); import PipelineStore from '../../vue_pipelines_index/stores/pipelines_store';
require('../../vue_shared/vue_resource_interceptor'); import eventHub from '../../vue_pipelines_index/event_hub';
require('../../vue_shared/components/pipelines_table'); import '../../lib/utils/common_utils';
require('./pipelines_service'); import '../../vue_shared/vue_resource_interceptor';
const PipelineStore = require('./pipelines_store');
/** /**
* *
...@@ -20,15 +19,9 @@ const PipelineStore = require('./pipelines_store'); ...@@ -20,15 +19,9 @@ const PipelineStore = require('./pipelines_store');
* as soon as we have Webpack and can load them directly into JS files. * as soon as we have Webpack and can load them directly into JS files.
*/ */
(() => { export default Vue.component('pipelines-table', {
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesTableView = Vue.component('pipelines-table', {
components: { components: {
'pipelines-table-component': gl.pipelines.PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
}, },
/** /**
...@@ -58,10 +51,27 @@ const PipelineStore = require('./pipelines_store'); ...@@ -58,10 +51,27 @@ const PipelineStore = require('./pipelines_store');
* *
*/ */
beforeMount() { beforeMount() {
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint); this.service = new PipelinesService(this.endpoint);
this.fetchPipelines();
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
this.store.startTimeAgoLoops.call(this, Vue);
}
},
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
methods: {
fetchPipelines() {
this.isLoading = true; this.isLoading = true;
return pipelinesService.all() return this.service.getPipelines()
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
// depending of the endpoint the response can either bring a `pipelines` key or not. // depending of the endpoint the response can either bring a `pipelines` key or not.
...@@ -71,14 +81,9 @@ const PipelineStore = require('./pipelines_store'); ...@@ -71,14 +81,9 @@ const PipelineStore = require('./pipelines_store');
}) })
.catch(() => { .catch(() => {
this.isLoading = false; this.isLoading = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.', 'alert'); new Flash('An error occurred while fetching the pipelines, please reload the page again.');
}); });
}, },
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
}, },
template: ` template: `
...@@ -96,9 +101,10 @@ const PipelineStore = require('./pipelines_store'); ...@@ -96,9 +101,10 @@ const PipelineStore = require('./pipelines_store');
<div class="table-holder pipelines" <div class="table-holder pipelines"
v-if="!isLoading && state.pipelines.length > 0"> v-if="!isLoading && state.pipelines.length > 0">
<pipelines-table-component :pipelines="state.pipelines"/> <pipelines-table-component
:pipelines="state.pipelines"
:service="service" />
</div> </div>
</div> </div>
`, `,
}); });
})();
...@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg'; ...@@ -25,6 +25,7 @@ import collapseIcon from '../icons/collapse_icon.svg';
role="button" role="button"
data-container="body" data-container="body"
data-placement="top" data-placement="top"
data-html="true"
:data-line-type="lineType" :data-line-type="lineType"
:title="note.authorName + ': ' + note.noteTruncated" :title="note.authorName + ': ' + note.noteTruncated"
:src="note.authorAvatar" :src="note.authorAvatar"
......
import PrometheusGraph from './monitoring/prometheus_graph'; // TODO: Maybe Make this a bundle
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */
/* global UsernameValidator */ /* global UsernameValidator */
/* global ActiveTabMemoizer */ /* global ActiveTabMemoizer */
...@@ -329,8 +328,6 @@ const UserCallout = require('./user_callout'); ...@@ -329,8 +328,6 @@ const UserCallout = require('./user_callout');
case 'ci:lints:show': case 'ci:lints:show':
new gl.CILintEditor(); new gl.CILintEditor();
break; break;
case 'projects:environments:metrics':
new PrometheusGraph();
case 'users:show': case 'users:show':
new UserCallout(); new UserCallout();
break; break;
......
...@@ -132,7 +132,7 @@ class DueDateSelect { ...@@ -132,7 +132,7 @@ class DueDateSelect {
const selectedDateValue = this.datePayload[this.abilityName].due_date; const selectedDateValue = this.datePayload[this.abilityName].due_date;
const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value'; const displayedDateStyle = this.displayedDate !== 'No due date' ? 'bold' : 'no-value';
this.$loading.fadeIn(); this.$loading.removeClass('hidden').fadeIn();
if (isDropdown) { if (isDropdown) {
this.$dropdown.trigger('loading.gl.dropdown'); this.$dropdown.trigger('loading.gl.dropdown');
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from './environments_table'; import EnvironmentTable from './environments_table';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
export default Vue.component('environment-component', { export default Vue.component('environment-component', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination, 'table-pagination': TablePaginationComponent,
}, },
data() { data() {
...@@ -59,7 +56,6 @@ export default Vue.component('environment-component', { ...@@ -59,7 +56,6 @@ export default Vue.component('environment-component', {
canCreateEnvironmentParsed() { canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
}, },
}, },
/** /**
......
...@@ -14,6 +14,7 @@ export default { ...@@ -14,6 +14,7 @@ export default {
class="btn external_url" class="btn external_url"
:href="externalUrl" :href="externalUrl"
target="_blank" target="_blank"
rel="noopener noreferrer"
title="Environment external URL"> title="Environment external URL">
<i class="fa fa-external-link" aria-hidden="true"></i> <i class="fa fa-external-link" aria-hidden="true"></i>
</a> </a>
......
import Timeago from 'timeago.js'; import Timeago from 'timeago.js';
import '../../lib/utils/text_utility';
import ActionsComponent from './environment_actions'; import ActionsComponent from './environment_actions';
import ExternalUrlComponent from './environment_external_url'; import ExternalUrlComponent from './environment_external_url';
import StopComponent from './environment_stop'; import StopComponent from './environment_stop';
import RollbackComponent from './environment_rollback'; import RollbackComponent from './environment_rollback';
import TerminalButtonComponent from './environment_terminal_button'; import TerminalButtonComponent from './environment_terminal_button';
import '../../lib/utils/text_utility'; import CommitComponent from '../../vue_shared/components/commit';
import '../../vue_shared/components/commit';
/** /**
* Envrionment Item Component * Envrionment Item Component
* *
* Renders a table row for each environment. * Renders a table row for each environment.
*/ */
const timeagoInstance = new Timeago(); const timeagoInstance = new Timeago();
export default { export default {
components: { components: {
'commit-component': gl.CommitComponent, 'commit-component': CommitComponent,
'actions-component': ActionsComponent, 'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent, 'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent, 'stop-component': StopComponent,
......
/** /**
* Render environments table. * Render environments table.
*/ */
import EnvironmentItem from './environment_item'; import EnvironmentTableRowComponent from './environment_item';
export default { export default {
components: { components: {
'environment-item': EnvironmentItem, 'environment-item': EnvironmentTableRowComponent,
}, },
props: { props: {
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-new */
/* global Flash */ /* global Flash */
import Vue from 'vue';
import EnvironmentsService from '../services/environments_service'; import EnvironmentsService from '../services/environments_service';
import EnvironmentTable from '../components/environments_table'; import EnvironmentTable from '../components/environments_table';
import EnvironmentsStore from '../stores/environments_store'; import EnvironmentsStore from '../stores/environments_store';
import TablePaginationComponent from '../../vue_shared/components/table_pagination';
const Vue = window.Vue = require('vue'); import '../../lib/utils/common_utils';
window.Vue.use(require('vue-resource')); import '../../vue_shared/vue_resource_interceptor';
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
export default Vue.component('environment-folder-view', { export default Vue.component('environment-folder-view', {
components: { components: {
'environment-table': EnvironmentTable, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination, 'table-pagination': TablePaginationComponent,
}, },
data() { data() {
......
/* eslint-disable class-methods-use-this */ /* eslint-disable class-methods-use-this */
import Vue from 'vue'; import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class EnvironmentsService { export default class EnvironmentsService {
constructor(endpoint) { constructor(endpoint) {
......
import '~/lib/utils/common_utils'; import '~/lib/utils/common_utils';
/** /**
* Environments Store. * Environments Store.
* *
......
...@@ -38,6 +38,7 @@ ...@@ -38,6 +38,7 @@
gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true); gl.FilteredSearchDropdownManager.addWordToInput(this.filter, value, true);
} }
this.resetFilters();
this.dismissDropdown(); this.dismissDropdown();
this.dispatchInputEvent(); this.dispatchInputEvent();
} }
...@@ -107,7 +108,7 @@ ...@@ -107,7 +108,7 @@
const hook = this.getCurrentHook(); const hook = this.getCurrentHook();
if (hook) { if (hook) {
const data = hook.list.data; const data = hook.list.data || [];
const results = data.map((o) => { const results = data.map((o) => {
const updated = o; const updated = o;
updated.droplab_hidden = false; updated.droplab_hidden = false;
......
...@@ -40,6 +40,8 @@ import FilteredSearchContainer from './container'; ...@@ -40,6 +40,8 @@ import FilteredSearchContainer from './container';
this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this); this.unselectEditTokensWrapper = this.unselectEditTokens.bind(this);
this.editTokenWrapper = this.editToken.bind(this); this.editTokenWrapper = this.editToken.bind(this);
this.tokenChange = this.tokenChange.bind(this); this.tokenChange = this.tokenChange.bind(this);
this.addInputContainerFocusWrapper = this.addInputContainerFocus.bind(this);
this.removeInputContainerFocusWrapper = this.removeInputContainerFocus.bind(this);
this.filteredSearchInputForm = this.filteredSearchInput.form; this.filteredSearchInputForm = this.filteredSearchInput.form;
this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit); this.filteredSearchInputForm.addEventListener('submit', this.handleFormSubmit);
...@@ -51,11 +53,13 @@ import FilteredSearchContainer from './container'; ...@@ -51,11 +53,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.addEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.addEventListener('click', this.tokenChange); this.filteredSearchInput.addEventListener('click', this.tokenChange);
this.filteredSearchInput.addEventListener('keyup', this.tokenChange); this.filteredSearchInput.addEventListener('keyup', this.tokenChange);
this.filteredSearchInput.addEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.addEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.addEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.addEventListener('click', this.clearSearchWrapper); this.clearSearchButton.addEventListener('click', this.clearSearchWrapper);
document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.addEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.addEventListener('click', this.unselectEditTokensWrapper); document.addEventListener('click', this.unselectEditTokensWrapper);
document.addEventListener('click', this.removeInputContainerFocusWrapper);
document.addEventListener('keydown', this.removeSelectedTokenWrapper); document.addEventListener('keydown', this.removeSelectedTokenWrapper);
} }
...@@ -69,11 +73,13 @@ import FilteredSearchContainer from './container'; ...@@ -69,11 +73,13 @@ import FilteredSearchContainer from './container';
this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper); this.filteredSearchInput.removeEventListener('keyup', this.checkForBackspaceWrapper);
this.filteredSearchInput.removeEventListener('click', this.tokenChange); this.filteredSearchInput.removeEventListener('click', this.tokenChange);
this.filteredSearchInput.removeEventListener('keyup', this.tokenChange); this.filteredSearchInput.removeEventListener('keyup', this.tokenChange);
this.filteredSearchInput.removeEventListener('focus', this.addInputContainerFocusWrapper);
this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken); this.tokensContainer.removeEventListener('click', FilteredSearchManager.selectToken);
this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper); this.tokensContainer.removeEventListener('dblclick', this.editTokenWrapper);
this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper); this.clearSearchButton.removeEventListener('click', this.clearSearchWrapper);
document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens); document.removeEventListener('click', gl.FilteredSearchVisualTokens.unselectTokens);
document.removeEventListener('click', this.unselectEditTokensWrapper); document.removeEventListener('click', this.unselectEditTokensWrapper);
document.removeEventListener('click', this.removeInputContainerFocusWrapper);
document.removeEventListener('keydown', this.removeSelectedTokenWrapper); document.removeEventListener('keydown', this.removeSelectedTokenWrapper);
} }
...@@ -124,6 +130,26 @@ import FilteredSearchContainer from './container'; ...@@ -124,6 +130,26 @@ import FilteredSearchContainer from './container';
} }
} }
addInputContainerFocus() {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
if (inputContainer) {
inputContainer.classList.add('focus');
}
}
removeInputContainerFocus(e) {
const inputContainer = this.filteredSearchInput.closest('.filtered-search-input-container');
const isElementInFilteredSearch = inputContainer && inputContainer.contains(e.target);
const isElementInDynamicFilterDropdown = e.target.closest('.filter-dropdown') !== null;
const isElementInStaticFilterDropdown = e.target.closest('ul[data-dropdown]') !== null;
if (!isElementInFilteredSearch && !isElementInDynamicFilterDropdown &&
!isElementInStaticFilterDropdown && inputContainer) {
inputContainer.classList.remove('focus');
}
}
static selectToken(e) { static selectToken(e) {
const button = e.target.closest('.selectable'); const button = e.target.closest('.selectable');
......
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* global Flash */ /* global Flash */
require('./flash'); require('./flash');
require('~/lib/utils/text_utility');
require('vendor/jquery.waitforimages'); require('vendor/jquery.waitforimages');
require('./task_list'); require('./task_list');
...@@ -50,20 +51,21 @@ class Issue { ...@@ -50,20 +51,21 @@ class Issue {
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text()); let total = Number($('.issue_counter').text().replace(/[^\d]/, ''));
if (isClose) { if (isClose) {
$('a.btn-close').addClass('hidden'); $('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden'); $('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden'); $('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1); total -= 1;
} else { } else {
$('a.btn-reopen').addClass('hidden'); $('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden'); $('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden'); $('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden'); $('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1); total += 1;
} }
$('.issue_counter').text(gl.text.addDelimiter(total));
} else { } else {
new Flash(issueFailMessage, 'alert'); new Flash(issueFailMessage, 'alert');
} }
......
...@@ -76,7 +76,7 @@ ...@@ -76,7 +76,7 @@
if (!selected.length) { if (!selected.length) {
data[abilityName].label_ids = ['']; data[abilityName].label_ids = [''];
} }
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
...@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown'; ...@@ -14,13 +14,13 @@ import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
<%= ci_success_icon %> <%= ci_success_icon %>
<span> <span>
Deployed to Deployed to
<a href="<%- url %>" target="_blank" class="environment"> <a href="<%- url %>" target="_blank" rel="noopener noreferrer" class="environment">
<%- name %> <%- name %>
</a> </a>
<span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>"> <span class="js-environment-timeago" data-toggle="tooltip" data-placement="top" data-title="<%- deployed_at_formatted %>">
<%- deployed_at %> <%- deployed_at %>
</span> </span>
<a class="js-environment-link" href="<%- external_url %>" target="_blank"> <a class="js-environment-link" href="<%- external_url %>" target="_blank" rel="noopener noreferrer">
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
View on <%- external_url_formatted %> View on <%- external_url_formatted %>
</a> </a>
......
...@@ -159,7 +159,7 @@ ...@@ -159,7 +159,7 @@
} }
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
...@@ -171,7 +171,7 @@ ...@@ -171,7 +171,7 @@
data = {}; data = {};
data[abilityName] = {}; data[abilityName] = {};
data[abilityName].milestone_id = selected != null ? selected : null; data[abilityName].milestone_id = selected != null ? selected : null;
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
import PrometheusGraph from './prometheus_graph';
document.addEventListener('DOMContentLoaded', function onLoad() {
document.removeEventListener('DOMContentLoaded', onLoad, false);
return new PrometheusGraph();
}, false);
...@@ -2,10 +2,9 @@ ...@@ -2,10 +2,9 @@
/* global Flash */ /* global Flash */
import d3 from 'd3'; import d3 from 'd3';
import _ from 'underscore';
import statusCodes from '~/lib/utils/http_status'; import statusCodes from '~/lib/utils/http_status';
import '~/lib/utils/common_utils'; import '../lib/utils/common_utils';
import '~/flash'; import '../flash';
const prometheusGraphsContainer = '.prometheus-graph'; const prometheusGraphsContainer = '.prometheus-graph';
const metricsEndpoint = 'metrics.json'; const metricsEndpoint = 'metrics.json';
...@@ -31,22 +30,21 @@ class PrometheusGraph { ...@@ -31,22 +30,21 @@ class PrometheusGraph {
} }
createGraph() { createGraph() {
const self = this; Object.keys(this.data).forEach((key) => {
_.each(this.data, (value, key) => { const value = this.data[key];
if (value.length > 0 && (key === 'cpu_values' || key === 'memory_values')) { if (value.length > 0) {
self.plotValues(value, key); this.plotValues(value, key);
} }
}); });
} }
init() { init() {
const self = this;
this.getData().then((metricsResponse) => { this.getData().then((metricsResponse) => {
if (metricsResponse === {}) { if (Object.keys(metricsResponse).length === 0) {
new Flash('Empty metrics', 'alert'); new Flash('Empty metrics', 'alert');
} else { } else {
self.transformData(metricsResponse); this.transformData(metricsResponse);
self.createGraph(); this.createGraph();
} }
}); });
} }
...@@ -321,12 +319,14 @@ class PrometheusGraph { ...@@ -321,12 +319,14 @@ class PrometheusGraph {
transformData(metricsResponse) { transformData(metricsResponse) {
const metricTypes = {}; const metricTypes = {};
_.each(metricsResponse.metrics, (value, key) => { Object.keys(metricsResponse.metrics).forEach((key) => {
const metricValues = value[0].values; if (key === 'cpu_values' || key === 'memory_values') {
metricTypes[key] = _.map(metricValues, metric => ({ const metricValues = (metricsResponse.metrics[key])[0];
metricTypes[key] = metricValues.values.map(metric => ({
time: new Date(metric[0] * 1000), time: new Date(metric[0] * 1000),
value: metric[1], value: metric[1],
})); }));
}
}); });
this.data = metricTypes; this.data = metricTypes;
} }
......
...@@ -53,7 +53,7 @@ ...@@ -53,7 +53,7 @@
$loading = $block.find('.block-loading').fadeOut(); $loading = $block.find('.block-loading').fadeOut();
var updateIssueBoardsIssue = function () { var updateIssueBoardsIssue = function () {
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update')) gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
.then(function () { .then(function () {
$loading.fadeOut(); $loading.fadeOut();
...@@ -90,7 +90,7 @@ ...@@ -90,7 +90,7 @@
data = {}; data = {};
data[abilityName] = {}; data[abilityName] = {};
data[abilityName].assignee_id = selected != null ? selected : null; data[abilityName].assignee_id = selected != null ? selected : null;
$loading.fadeIn(); $loading.removeClass('hidden').fadeIn();
$dropdown.trigger('loading.gl.dropdown'); $dropdown.trigger('loading.gl.dropdown');
return $.ajax({ return $.ajax({
type: 'PUT', type: 'PUT',
......
/* eslint-disable no-new, no-alert */
/* global Flash */
import '~/flash';
import eventHub from '../event_hub';
export default {
props: {
endpoint: {
type: String,
required: true,
},
service: {
type: Object,
required: true,
},
title: {
type: String,
required: true,
},
icon: {
type: String,
required: true,
},
cssClass: {
type: String,
required: true,
},
confirmActionMessage: {
type: String,
required: false,
},
},
data() {
return {
isLoading: false,
};
},
computed: {
iconClass() {
return `fa fa-${this.icon}`;
},
buttonClass() {
return `btn has-tooltip ${this.cssClass}`;
},
},
methods: {
onClick() {
if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
this.makeRequest();
} else if (!this.confirmActionMessage) {
this.makeRequest();
}
},
makeRequest() {
this.isLoading = true;
this.service.postAction(this.endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<button
type="button"
@click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
data-placement="top"
:disabled="isLoading">
<i :class="iconClass" aria-hidden="true"/>
<i class="fa fa-spinner fa-spin" aria-hidden="true" v-if="isLoading" />
</button>
`,
};
export default {
props: [
'pipeline',
],
computed: {
user() {
return !!this.pipeline.user;
},
},
template: `
<td>
<a
:href="pipeline.path"
class="js-pipeline-url-link">
<span class="pipeline-id">#{{pipeline.id}}</span>
</a>
<span>by</span>
<a
class="js-pipeline-url-user"
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="js-pipeline-url-api api monospace">
API
</span>
<span
v-if="pipeline.flags.latest"
class="js-pipeline-url-lastest 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="js-pipeline-url-yaml 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="js-pipeline-url-stuck label label-warning">
stuck
</span>
</td>
`,
};
/* eslint-disable no-new */
/* global Flash */
import '~/flash';
import playIconSvg from 'icons/_icon_play.svg';
import eventHub from '../event_hub';
export default {
props: {
actions: {
type: Array,
required: true,
},
service: {
type: Object,
required: true,
},
},
data() {
return {
playIconSvg,
isLoading: false,
};
},
methods: {
onClickAction(endpoint) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => {
this.isLoading = false;
eventHub.$emit('refreshPipelines');
})
.catch(() => {
this.isLoading = false;
new Flash('An error occured while making the request.');
});
},
},
template: `
<div class="btn-group" v-if="actions">
<button
type="button"
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
title="Manual job"
data-toggle="dropdown"
data-placement="top"
aria-label="Manual job"
:disabled="isLoading">
${playIconSvg}
<i class="fa fa-caret-down" aria-hidden="true"></i>
<i v-if="isLoading" class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<button
type="button"
class="js-pipeline-action-link no-btn"
@click="onClickAction(action.path)">
${playIconSvg}
<span>{{action.name}}</span>
</button>
</li>
</ul>
</div>
`,
};
export default {
props: {
artifacts: {
type: Array,
required: true,
},
},
template: `
<div class="btn-group" role="group">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="artifact in artifacts">
<a
rel="nofollow"
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>Download {{artifact.name}} artifacts</span>
</a>
</li>
</ul>
</div>
`,
};
/* global Flash */
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdSvg from 'icons/_icon_status_created_borderless.svg';
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
import runningSvg from 'icons/_icon_status_running_borderless.svg';
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
import successSvg from 'icons/_icon_status_success_borderless.svg';
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
export default {
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
svg: svgsDictionary[this.stage.status.icon],
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const ariaExpanded = e.currentTarget.attributes['aria-expanded'];
if (ariaExpanded && (ariaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el.querySelectorAll('.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item')).on('click', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) 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}`;
},
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($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></div>
<div
:class="dropdownClass"
class="js-builds-dropdown-list scrollable-menu"
v-html="buildsOrSpinner">
</div>
</ul>
</div>
`,
};
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
export default {
props: {
pipeline: {
type: Object,
required: true,
},
},
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
return `ci-status ci-${this.pipeline.details.status.group}`;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
};
import iconTimerSvg from 'icons/_icon_timer.svg';
import '../../lib/utils/datetime_utility';
export default {
data() {
return {
currentTime: new Date(),
iconTimerSvg,
};
},
props: ['pipeline'],
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 class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></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>
`,
};
import Vue from 'vue';
export default new Vue();
/* eslint-disable no-param-reassign */ import PipelinesStore from './stores/pipelines_store';
/* global Vue, VueResource, gl */ import PipelinesComponent from './pipelines';
window.Vue = require('vue'); import '../vue_shared/vue_resource_interceptor';
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource')); window.Vue.use(require('vue-resource'));
require('../lib/utils/common_utils');
require('../vue_shared/vue_resource_interceptor');
require('./pipelines');
$(() => new Vue({ $(() => new Vue({
el: document.querySelector('.vue-pipelines-index'), el: document.querySelector('.vue-pipelines-index'),
data() { data() {
const project = document.querySelector('.pipelines'); const project = document.querySelector('.pipelines');
const store = new PipelinesStore();
return { return {
scope: project.dataset.url, store,
store: new gl.PipelineStore(), endpoint: project.dataset.url,
}; };
}, },
components: { components: {
'vue-pipelines': gl.VuePipelines, 'vue-pipelines': PipelinesComponent,
}, },
template: ` template: `
<vue-pipelines <vue-pipelines
:scope="scope" :endpoint="endpoint"
:store="store"> :store="store" />
</vue-pipelines>
`, `,
})); }));
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign, no-alert */
const playIconSvg = require('icons/_icon_play.svg');
((gl) => {
gl.VuePipelineActions = Vue.extend({
props: ['pipeline'],
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`;
},
/**
* Shows a dialog when the user clicks in the cancel button.
* We need to prevent the default behavior and stop propagation because the
* link relies on UJS.
*
* @param {Event} event
*/
confirmAction(event) {
if (!confirm('Are you sure you want to cancel this pipeline?')) {
event.preventDefault();
event.stopPropagation();
}
},
},
data() {
return { playIconSvg };
},
template: `
<td class="pipeline-actions">
<div class="pull-right">
<div class="btn-group">
<div class="btn-group" v-if="actions">
<button
class="dropdown-toggle btn btn-default has-tooltip js-pipeline-dropdown-manual-actions"
data-toggle="dropdown"
title="Manual job"
data-placement="top"
data-container="body"
aria-label="Manual job">
<span v-html="playIconSvg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='action in pipeline.details.manual_actions'>
<a
rel="nofollow"
data-method="post"
:href="action.path" >
<span v-html="playIconSvg" aria-hidden="true"></span>
<span>{{action.name}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group" v-if="artifacts">
<button
class="dropdown-toggle btn btn-default build-artifacts has-tooltip js-pipeline-dropdown-download"
title="Artifacts"
data-placement="top"
data-container="body"
data-toggle="dropdown"
aria-label="Artifacts">
<i class="fa fa-download" aria-hidden="true"></i>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
:href="artifact.path">
<i class="fa fa-download" aria-hidden="true"></i>
<span>{{download(artifact.name)}}</span>
</a>
</li>
</ul>
</div>
<div class="btn-group" v-if="pipeline.flags.retryable">
<a
class="btn btn-default btn-retry has-tooltip"
title="Retry"
rel="nofollow"
data-method="post"
data-placement="top"
data-container="body"
data-toggle="dropdown"
:href='pipeline.retry_path'
aria-label="Retry">
<i class="fa fa-repeat" aria-hidden="true"></i>
</a>
</div>
<div class="btn-group" v-if="pipeline.flags.cancelable">
<a
class="btn btn-remove has-tooltip"
title="Cancel"
rel="nofollow"
data-method="post"
data-placement="top"
data-container="body"
data-toggle="dropdown"
:href='pipeline.cancel_path'
aria-label="Cancel">
<i class="fa fa-remove" aria-hidden="true"></i>
</a>
</div>
</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, gl */ /* global Flash */
/* eslint-disable no-param-reassign */ /* eslint-disable no-new */
import '~/flash';
import Vue from 'vue';
import PipelinesService from './services/pipelines_service';
import eventHub from './event_hub';
import PipelinesTableComponent from '../vue_shared/components/pipelines_table';
import TablePaginationComponent from '../vue_shared/components/table_pagination';
window.Vue = require('vue'); export default {
require('../vue_shared/components/table_pagination'); props: {
require('./store'); endpoint: {
require('../vue_shared/components/pipelines_table'); type: String,
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store'); required: true,
},
((gl) => { store: {
gl.VuePipelines = Vue.extend({ type: Object,
required: true,
},
},
components: { components: {
'gl-pagination': gl.VueGlPagination, 'gl-pagination': TablePaginationComponent,
'pipelines-table-component': gl.pipelines.PipelinesTableComponent, 'pipelines-table-component': PipelinesTableComponent,
}, },
data() { data() {
return { return {
pipelines: [], state: this.store.state,
timeLoopInterval: '',
intervalId: '',
apiScope: 'all', apiScope: 'all',
pageInfo: {},
pagenum: 1, pagenum: 1,
count: {},
pageRequest: false, pageRequest: false,
}; };
}, },
props: ['scope', 'store'],
created() { created() {
const pagenum = gl.utils.getParameterByName('page'); this.service = new PipelinesService(this.endpoint);
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum; this.fetchPipelines();
if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); eventHub.$on('refreshPipelines', this.fetchPipelines);
}, },
beforeUpdate() { beforeUpdate() {
if (this.pipelines.length && this.$children) { if (this.state.pipelines.length && this.$children) {
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue); this.store.startTimeAgoLoops.call(this, Vue);
} }
}, },
beforeDestroyed() {
eventHub.$off('refreshPipelines');
},
methods: { methods: {
/** /**
* Will change the page number and update the URL. * Will change the page number and update the URL.
...@@ -55,33 +64,58 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s ...@@ -55,33 +64,58 @@ const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_s
gl.utils.visitUrl(param); gl.utils.visitUrl(param);
return param; return param;
}, },
fetchPipelines() {
const pageNumber = gl.utils.getParameterByName('page') || this.pagenum;
const scope = gl.utils.getParameterByName('scope') || this.apiScope;
this.pageRequest = true;
return this.service.getPipelines(scope, pageNumber)
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeCount(response.body.count);
this.store.storePipelines(response.body.pipelines);
this.store.storePagination(response.headers);
})
.then(() => {
this.pageRequest = false;
})
.catch(() => {
this.pageRequest = false;
new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
},
}, },
template: ` template: `
<div> <div>
<div class="pipelines realtime-loading" v-if='pageRequest'> <div class="pipelines realtime-loading" v-if="pageRequest">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div> </div>
<div class="blank-state blank-state-no-icon" <div class="blank-state blank-state-no-icon"
v-if="!pageRequest && pipelines.length === 0"> v-if="!pageRequest && state.pipelines.length === 0">
<h2 class="blank-state-title js-blank-state-title"> <h2 class="blank-state-title js-blank-state-title">
No pipelines to show No pipelines to show
</h2> </h2>
</div> </div>
<div class="table-holder" v-if='!pageRequest && pipelines.length'> <div class="table-holder" v-if="!pageRequest && state.pipelines.length">
<pipelines-table-component :pipelines='pipelines'/> <pipelines-table-component
:pipelines="state.pipelines"
:service="service"/>
</div> </div>
<gl-pagination <gl-pagination
v-if='!pageRequest && pipelines.length && pageInfo.total > pageInfo.perPage' v-if="!pageRequest && state.pipelines.length && state.pageInfo.total > state.pageInfo.perPage"
:pagenum='pagenum' :pagenum="pagenum"
:change='change' :change="change"
:count='count.all' :count="state.count.all"
:pageInfo='pageInfo' :pageInfo="state.pageInfo"
> >
</gl-pagination> </gl-pagination>
</div> </div>
`, `,
}); };
})(window.gl || (window.gl = {}));
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
Vue.use(VueResource);
export default class PipelinesService {
/**
* Commits and merge request endpoints need to be requested with `.json`.
*
* The url provided to request the pipelines in the new merge request
* page already has `.json`.
*
* @param {String} root
*/
constructor(root) {
let endpoint;
if (root.indexOf('.json') === -1) {
endpoint = `${root}.json`;
} else {
endpoint = root;
}
this.pipelines = Vue.resource(endpoint);
}
getPipelines(scope, page) {
return this.pipelines.get({ scope, page });
}
/**
* Post request for all pipelines actions.
* Endpoint content type needs to be:
* `Content-Type:application/x-www-form-urlencoded`
*
* @param {String} endpoint
* @return {Promise}
*/
postAction(endpoint) {
return Vue.http.post(endpoint, {}, { emulateJSON: true });
}
}
/* global Vue, Flash, gl */
/* eslint-disable no-param-reassign */
import canceledSvg from 'icons/_icon_status_canceled_borderless.svg';
import createdSvg from 'icons/_icon_status_created_borderless.svg';
import failedSvg from 'icons/_icon_status_failed_borderless.svg';
import manualSvg from 'icons/_icon_status_manual_borderless.svg';
import pendingSvg from 'icons/_icon_status_pending_borderless.svg';
import runningSvg from 'icons/_icon_status_running_borderless.svg';
import skippedSvg from 'icons/_icon_status_skipped_borderless.svg';
import successSvg from 'icons/_icon_status_success_borderless.svg';
import warningSvg from 'icons/_icon_status_warning_borderless.svg';
((gl) => {
gl.VueStage = Vue.extend({
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
builds: '',
spinner: '<span class="fa fa-spinner fa-spin"></span>',
svg: svgsDictionary[this.stage.status.icon],
};
},
props: {
stage: {
type: Object,
required: true,
},
},
updated() {
if (this.builds) {
this.stopDropdownClickPropagation();
}
},
methods: {
fetchBuilds(e) {
const areaExpanded = e.currentTarget.attributes['aria-expanded'];
if (areaExpanded && (areaExpanded.textContent === 'true')) return null;
return this.$http.get(this.stage.dropdown_path)
.then((response) => {
this.builds = JSON.parse(response.body).html;
}, () => {
const flash = new Flash('Something went wrong on our end.');
return flash;
});
},
/**
* When the user right clicks or cmd/ctrl + click in the job name
* the dropdown should not be closed and the link should open in another tab,
* so we stop propagation of the click event inside the dropdown.
*
* Since this component is rendered multiple times per page we need to guarantee we only
* target the click event of this component.
*/
stopDropdownClickPropagation() {
$(this.$el).on('click', '.js-builds-dropdown-list a.mini-pipeline-graph-dropdown-item', (e) => {
e.stopPropagation();
});
},
},
computed: {
buildsOrSpinner() {
return this.builds ? this.builds : this.spinner;
},
dropdownClass() {
if (this.builds) 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}`;
},
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($event)"
:class="triggerButtonClass"
:title="stage.title"
data-placement="top"
data-toggle="dropdown"
type="button"
:aria-label="stage.title">
<span v-html="svg" aria-hidden="true"></span>
<i class="fa fa-caret-down" aria-hidden="true"></i>
</button>
<ul class="dropdown-menu mini-pipeline-graph-dropdown-menu js-builds-dropdown-container">
<div class="arrow-up" aria-hidden="true"></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 */
import canceledSvg from 'icons/_icon_status_canceled.svg';
import createdSvg from 'icons/_icon_status_created.svg';
import failedSvg from 'icons/_icon_status_failed.svg';
import manualSvg from 'icons/_icon_status_manual.svg';
import pendingSvg from 'icons/_icon_status_pending.svg';
import runningSvg from 'icons/_icon_status_running.svg';
import skippedSvg from 'icons/_icon_status_skipped.svg';
import successSvg from 'icons/_icon_status_success.svg';
import warningSvg from 'icons/_icon_status_warning.svg';
((gl) => {
gl.VueStatusScope = Vue.extend({
props: [
'pipeline',
],
data() {
const svgsDictionary = {
icon_status_canceled: canceledSvg,
icon_status_created: createdSvg,
icon_status_failed: failedSvg,
icon_status_manual: manualSvg,
icon_status_pending: pendingSvg,
icon_status_running: runningSvg,
icon_status_skipped: skippedSvg,
icon_status_success: successSvg,
icon_status_warning: warningSvg,
};
return {
svg: svgsDictionary[this.pipeline.details.status.icon],
};
},
computed: {
cssClasses() {
const cssObject = { 'ci-status': true };
cssObject[`ci-${this.pipeline.details.status.group}`] = true;
return cssObject;
},
detailsPath() {
const { status } = this.pipeline.details;
return status.has_details ? status.details_path : false;
},
content() {
return `${this.svg} ${this.pipeline.details.status.text}`;
},
},
template: `
<td class="commit-link">
<a
:class="cssClasses"
:href="detailsPath"
v-html="content">
</a>
</td>
`,
});
})(window.gl || (window.gl = {}));
/* global gl, Flash */
/* eslint-disable no-param-reassign */
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.parseIntPagination(normalized);
return paginationInfo;
};
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true;
return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
}
};
})(window.gl || (window.gl = {}));
/* eslint-disable no-underscore-dangle*/ /* eslint-disable no-underscore-dangle*/
/** import '../../vue_realtime_listener';
* Pipelines' Store for commits view.
*
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
require('../../vue_realtime_listener');
class PipelinesStore { export default class PipelinesStore {
constructor() { constructor() {
this.state = {}; this.state = {};
this.state.pipelines = []; this.state.pipelines = [];
this.state.count = {};
this.state.pageInfo = {};
} }
storePipelines(pipelines = []) { storePipelines(pipelines = []) {
this.state.pipelines = pipelines; this.state.pipelines = pipelines;
}
return pipelines; storeCount(count = {}) {
this.state.count = count;
}
storePagination(pagination = {}) {
let paginationInfo;
if (Object.keys(pagination).length) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
paginationInfo = gl.utils.parseIntPagination(normalizedHeaders);
} else {
paginationInfo = pagination;
}
this.state.pageInfo = paginationInfo;
} }
/** /**
* FIXME: Move this inside the component.
*
* Once the data is received we will start the time ago loops. * Once the data is received we will start the time ago loops.
* *
* Everytime a request is made like retry or cancel a pipeline, every 10 seconds we * Everytime a request is made like retry or cancel a pipeline, every 10 seconds we
* update the time to show how long as passed. * update the time to show how long as passed.
* *
*/ */
static startTimeAgoLoops() { startTimeAgoLoops() {
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => { this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => { this.$children[0].$children.reduce((acc, component) => {
...@@ -44,5 +59,3 @@ class PipelinesStore { ...@@ -44,5 +59,3 @@ class PipelinesStore {
gl.VueRealtimeListener(removeIntervals, startIntervals); gl.VueRealtimeListener(removeIntervals, startIntervals);
} }
} }
module.exports = PipelinesStore;
/* global Vue, gl */
/* eslint-disable no-param-reassign */
window.Vue = require('vue');
require('../lib/utils/datetime_utility');
const iconTimerSvg = require('../../../views/shared/icons/_icon_timer.svg');
((gl) => {
gl.VueTimeAgo = Vue.extend({
data() {
return {
currentTime: new Date(),
iconTimerSvg,
};
},
props: ['pipeline'],
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 class="pipelines-time-ago">
<p class="duration" v-if='duration'>
<span v-html="iconTimerSvg"></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 = {}));
/* global Vue */ import commitIconSvg from 'icons/_icon_commit.svg';
window.Vue = require('vue');
const commitIconSvg = require('icons/_icon_commit.svg');
(() => {
window.gl = window.gl || {};
window.gl.CommitComponent = Vue.component('commit-component', {
export default {
props: { props: {
/** /**
* Indicates the existance of a tag. * Indicates the existance of a tag.
...@@ -160,5 +154,4 @@ const commitIconSvg = require('icons/_icon_commit.svg'); ...@@ -160,5 +154,4 @@ const commitIconSvg = require('icons/_icon_commit.svg');
</p> </p>
</div> </div>
`, `,
}); };
})();
/* eslint-disable no-param-reassign */ import PipelinesTableRowComponent from './pipelines_table_row';
/* global Vue */
require('./pipelines_table_row');
/** /**
* Pipelines Table Component. * Pipelines Table Component.
* *
* Given an array of objects, renders a table. * Given an array of objects, renders a table.
*/ */
export default {
(() => {
window.gl = window.gl || {};
gl.pipelines = gl.pipelines || {};
gl.pipelines.PipelinesTableComponent = Vue.component('pipelines-table-component', {
props: { props: {
pipelines: { pipelines: {
type: Array, type: Array,
...@@ -21,10 +13,14 @@ require('./pipelines_table_row'); ...@@ -21,10 +13,14 @@ require('./pipelines_table_row');
default: () => ([]), default: () => ([]),
}, },
service: {
type: Object,
required: true,
},
}, },
components: { components: {
'pipelines-table-row-component': gl.pipelines.PipelinesTableRowComponent, 'pipelines-table-row-component': PipelinesTableRowComponent,
}, },
template: ` template: `
...@@ -43,10 +39,10 @@ require('./pipelines_table_row'); ...@@ -43,10 +39,10 @@ require('./pipelines_table_row');
<template v-for="model in pipelines" <template v-for="model in pipelines"
v-bind:model="model"> v-bind:model="model">
<tr is="pipelines-table-row-component" <tr is="pipelines-table-row-component"
:pipeline="model"></tr> :pipeline="model"
:service="service"></tr>
</template> </template>
</tbody> </tbody>
</table> </table>
`, `,
}); };
})();
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
/* global Vue */
import AsyncButtonComponent from '../../vue_pipelines_index/components/async_button';
require('../../vue_pipelines_index/status'); import PipelinesActionsComponent from '../../vue_pipelines_index/components/pipelines_actions';
require('../../vue_pipelines_index/pipeline_url'); import PipelinesArtifactsComponent from '../../vue_pipelines_index/components/pipelines_artifacts';
require('../../vue_pipelines_index/stage'); import PipelinesStatusComponent from '../../vue_pipelines_index/components/status';
require('../../vue_pipelines_index/pipeline_actions'); import PipelinesStageComponent from '../../vue_pipelines_index/components/stage';
require('../../vue_pipelines_index/time_ago'); import PipelinesUrlComponent from '../../vue_pipelines_index/components/pipeline_url';
require('./commit'); import PipelinesTimeagoComponent from '../../vue_pipelines_index/components/time_ago';
import CommitComponent from './commit';
/** /**
* Pipeline table row. * Pipeline table row.
* *
* Given the received object renders a table row in the pipelines' table. * Given the received object renders a table row in the pipelines' table.
*/ */
(() => { export default {
window.gl = window.gl || {};
gl.pipelines = gl.pipelines || {};
gl.pipelines.PipelinesTableRowComponent = Vue.component('pipelines-table-row-component', {
props: { props: {
pipeline: { pipeline: {
type: Object, type: Object,
required: true, required: true,
default: () => ({}),
}, },
service: {
type: Object,
required: true,
},
}, },
components: { components: {
'commit-component': gl.CommitComponent, 'async-button-component': AsyncButtonComponent,
'pipeline-actions': gl.VuePipelineActions, 'pipelines-actions-component': PipelinesActionsComponent,
'dropdown-stage': gl.VueStage, 'pipelines-artifacts-component': PipelinesArtifactsComponent,
'pipeline-url': gl.VuePipelineUrl, 'commit-component': CommitComponent,
'status-scope': gl.VueStatusScope, 'dropdown-stage': PipelinesStageComponent,
'time-ago': gl.VueTimeAgo, 'pipeline-url': PipelinesUrlComponent,
'status-scope': PipelinesStatusComponent,
'time-ago': PipelinesTimeagoComponent,
}, },
computed: { computed: {
...@@ -192,8 +194,35 @@ require('./commit'); ...@@ -192,8 +194,35 @@ require('./commit');
<time-ago :pipeline="pipeline"/> <time-ago :pipeline="pipeline"/>
<pipeline-actions :pipeline="pipeline" /> <td class="pipeline-actions">
<div class="pull-right btn-group">
<pipelines-actions-component
v-if="pipeline.details.manual_actions.length"
:actions="pipeline.details.manual_actions"
:service="service" />
<pipelines-artifacts-component
v-if="pipeline.details.artifacts.length"
:artifacts="pipeline.details.artifacts" />
<async-button-component
v-if="pipeline.flags.retryable"
:service="service"
:endpoint="pipeline.retry_path"
css-class="js-pipelines-retry-button btn-default btn-retry"
title="Retry"
icon="repeat" />
<async-button-component
v-if="pipeline.flags.cancelable"
:service="service"
:endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove"
title="Cancel"
icon="remove"
confirm-action-message="Are you sure you want to cancel this pipeline?" />
</div>
</td>
</tr> </tr>
`, `,
}); };
})();
/* global Vue, gl */ const PAGINATION_UI_BUTTON_LIMIT = 4;
/* eslint-disable no-param-reassign, no-plusplus */ const UI_LIMIT = 6;
const SPREAD = '...';
window.Vue = require('vue'); const PREV = 'Prev';
const NEXT = 'Next';
((gl) => { const FIRST = '<< First';
const PAGINATION_UI_BUTTON_LIMIT = 4; const LAST = 'Last >>';
const UI_LIMIT = 6;
const SPREAD = '...'; export default {
const PREV = 'Prev';
const NEXT = 'Next';
const FIRST = '<< First';
const LAST = 'Last >>';
gl.VueGlPagination = Vue.extend({
props: { props: {
// TODO: Consider refactoring in light of turbolinks removal.
/** /**
This function will take the information given by the pagination component This function will take the information given by the pagination component
...@@ -26,7 +17,6 @@ window.Vue = require('vue'); ...@@ -26,7 +17,6 @@ window.Vue = require('vue');
gl.utils.visitUrl(`?page=${pagenum}`); gl.utils.visitUrl(`?page=${pagenum}`);
}, },
*/ */
change: { change: {
type: Function, type: Function,
required: true, required: true,
...@@ -48,7 +38,6 @@ window.Vue = require('vue'); ...@@ -48,7 +38,6 @@ window.Vue = require('vue');
previousPage: +headers['X-Prev-Page'], previousPage: +headers['X-Prev-Page'],
}); });
*/ */
pageInfo: { pageInfo: {
type: Object, type: Object,
required: true, required: true,
...@@ -105,7 +94,7 @@ window.Vue = require('vue'); ...@@ -105,7 +94,7 @@ window.Vue = require('vue');
const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1); const start = Math.max(page - PAGINATION_UI_BUTTON_LIMIT, 1);
const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total); const end = Math.min(page + PAGINATION_UI_BUTTON_LIMIT, total);
for (let i = start; i <= end; i++) { for (let i = start; i <= end; i += 1) {
const isActive = i === page; const isActive = i === page;
items.push({ title: i, active: isActive, page: true }); items.push({ title: i, active: isActive, page: true });
} }
...@@ -143,5 +132,4 @@ window.Vue = require('vue'); ...@@ -143,5 +132,4 @@ window.Vue = require('vue');
</ul> </ul>
</div> </div>
`, `,
}); };
})(window.gl || (window.gl = {}));
/* eslint-disable func-names, prefer-arrow-callback, no-unused-vars, /* eslint-disable no-param-reassign, no-plusplus */
no-param-reassign, no-plusplus */ import Vue from 'vue';
/* global Vue */ import VueResource from 'vue-resource';
Vue.use(VueResource);
Vue.http.interceptors.push((request, next) => { Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1; Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
next((response) => { next(() => {
Vue.activeResources--; Vue.activeResources--;
}); });
}); });
......
...@@ -429,3 +429,9 @@ table { ...@@ -429,3 +429,9 @@ table {
@include str-truncated(100%); @include str-truncated(100%);
} }
} }
.tooltip {
.tooltip-inner {
word-wrap: break-word;
}
}
...@@ -76,12 +76,14 @@ ...@@ -76,12 +76,14 @@
} }
.input-token { .input-token {
flex: 1; max-width: 200px;
-webkit-flex: 1;
} }
.filtered-search-token + .input-token:not(:last-child) { .input-token:only-child,
max-width: 200px; .input-token:last-child {
flex: 1;
-webkit-flex: 1;
max-width: initial;
} }
} }
...@@ -158,8 +160,8 @@ ...@@ -158,8 +160,8 @@
background-color: $white-light; background-color: $white-light;
@media (max-width: $screen-xs-min) { @media (max-width: $screen-xs-min) {
-webkit-flex: 1 1 100%; -webkit-flex: 1 1 auto;
flex: 1 1 100%; flex: 1 1 auto;
margin-bottom: 10px; margin-bottom: 10px;
.dropdown-menu { .dropdown-menu {
...@@ -171,17 +173,26 @@ ...@@ -171,17 +173,26 @@
} }
} }
&:hover {
@extend .form-control:hover;
}
&.focus,
&.focus:hover {
border-color: $dropdown-input-focus-border;
box-shadow: 0 0 4px $search-input-focus-shadow-color;
}
&.focus .fa-filter {
color: $common-gray-dark;
}
.form-control { .form-control {
position: relative; position: relative;
min-width: 200px; min-width: 200px;
padding-left: 0; padding: 5px 25px 6px 0;
padding-right: 25px;
border-color: transparent; border-color: transparent;
&:focus ~ .fa-filter {
color: $common-gray-dark;
}
&:focus, &:focus,
&:hover { &:hover {
outline: none; outline: none;
...@@ -221,6 +232,10 @@ ...@@ -221,6 +232,10 @@
.filter-dropdown-container { .filter-dropdown-container {
display: -webkit-flex; display: -webkit-flex;
display: flex; display: flex;
.dropdown-toggle {
line-height: 22px;
}
} }
.dropdown-menu .filter-dropdown-item { .dropdown-menu .filter-dropdown-item {
...@@ -246,7 +261,9 @@ ...@@ -246,7 +261,9 @@
background-color: $white-light; background-color: $white-light;
border-top: 0; border-top: 0;
} }
}
@media (max-width: $screen-xs) {
.filter-dropdown-container { .filter-dropdown-container {
.dropdown-toggle, .dropdown-toggle,
.dropdown { .dropdown {
......
...@@ -138,7 +138,6 @@ ...@@ -138,7 +138,6 @@
.nav-links { .nav-links {
display: inline-block; display: inline-block;
width: 50%;
margin-bottom: 0; margin-bottom: 0;
border-bottom: none; border-bottom: none;
......
...@@ -306,6 +306,11 @@ a > code { ...@@ -306,6 +306,11 @@ a > code {
* Textareas intended for GFM * Textareas intended for GFM
* *
*/ */
textarea.js-gfm-input {
font-family: $monospace_font;
font-size: 13px;
}
.strikethrough { .strikethrough {
text-decoration: line-through; text-decoration: line-through;
} }
......
...@@ -148,6 +148,18 @@ ...@@ -148,6 +148,18 @@
.error-alert > .alert { .error-alert > .alert {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;
&.alert-dismissable {
.close {
color: $white-light;
opacity: 0.85;
font-weight: normal;
&:hover {
opacity: 1;
}
}
}
} }
.discussion-body, .discussion-body,
......
...@@ -72,11 +72,6 @@ ...@@ -72,11 +72,6 @@
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
font-size: 14px; font-size: 14px;
} }
svg,
.fa {
margin-right: 0;
}
} }
.btn-group { .btn-group {
...@@ -921,3 +916,22 @@ ...@@ -921,3 +916,22 @@
} }
} }
} }
/**
* Play button with icon in dropdowns
*/
.ci-table .no-btn {
border: none;
background: none;
outline: none;
width: 100%;
text-align: left;
.icon-play {
position: relative;
top: 2px;
margin-right: 5px;
height: 13px;
width: 12px;
}
}
...@@ -37,7 +37,6 @@ module ServiceParams ...@@ -37,7 +37,6 @@ module ServiceParams
:namespace, :namespace,
:new_issue_url, :new_issue_url,
:notify, :notify,
:notify_only_broken_builds,
:notify_only_broken_pipelines, :notify_only_broken_pipelines,
:password, :password,
:priority, :priority,
......
...@@ -51,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController ...@@ -51,7 +51,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
private private
def find_todos def find_todos
@todos ||= TodosFinder.new(current_user, params).execute @todos ||= TodosFinder.new(current_user, params.merge(include_associations: true)).execute
end end
def todos_counts def todos_counts
......
...@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -6,6 +6,8 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections include IssuableCollections
include SpammableActions include SpammableActions
prepend_before_action :authenticate_user!, only: [:new]
before_action :redirect_to_external_issue_tracker, only: [:index, :new] before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :module_enabled before_action :module_enabled
before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests, before_action :issue, only: [:edit, :update, :show, :referenced_merge_requests,
...@@ -146,7 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController ...@@ -146,7 +148,7 @@ class Projects::IssuesController < Projects::ApplicationController
end end
format.json do format.json do
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) render json: @issue.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end end
end end
......
...@@ -308,7 +308,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -308,7 +308,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
format.json do format.json do
render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short]) render json: @merge_request.to_json(include: { milestone: {}, assignee: { only: [:name, :username], methods: [:avatar_url] }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end end
end end
rescue ActiveRecord::StaleObjectError rescue ActiveRecord::StaleObjectError
......
...@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController ...@@ -45,8 +45,9 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project) return render('empty') unless can?(current_user, :create_wiki, @project)
@page = @project_wiki.find_page(params[:id]) @page = @project_wiki.find_page(params[:id])
@page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page) if @page.valid?
redirect_to( redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page), namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.' notice: 'Wiki was successfully updated.'
......
...@@ -24,6 +24,7 @@ class TodosFinder ...@@ -24,6 +24,7 @@ class TodosFinder
def execute def execute
items = current_user.todos items = current_user.todos
items = include_associations(items)
items = by_action_id(items) items = by_action_id(items)
items = by_action(items) items = by_action(items)
items = by_author(items) items = by_author(items)
...@@ -38,6 +39,17 @@ class TodosFinder ...@@ -38,6 +39,17 @@ class TodosFinder
private private
def include_associations(items)
return items unless params[:include_associations]
items.includes(
[
target: { project: [:route, namespace: :route] },
author: { namespace: :route },
]
)
end
def action_id? def action_id?
action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i) action_id.present? && Todo::ACTION_NAMES.has_key?(action_id.to_i)
end end
......
...@@ -215,6 +215,6 @@ module BlobHelper ...@@ -215,6 +215,6 @@ module BlobHelper
end end
def open_raw_file_button(path) def open_raw_file_button(path)
link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', title: 'Open raw', data: { container: 'body' } link_to icon('file-code-o'), path, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
end end
end end
...@@ -211,7 +211,7 @@ module CommitsHelper ...@@ -211,7 +211,7 @@ module CommitsHelper
external_url = environment.external_url_for(diff_new_path, commit_sha) external_url = environment.external_url_for(diff_new_path, commit_sha)
return unless external_url return unless external_url
link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do link_to(external_url, class: 'btn btn-file-option has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: "View on #{environment.formatted_external_url}", data: { container: 'body' }) do
icon('external-link') icon('external-link')
end end
end end
......
...@@ -7,7 +7,7 @@ module ImportHelper ...@@ -7,7 +7,7 @@ module ImportHelper
def provider_project_link(provider, path_with_namespace) def provider_project_link(provider, path_with_namespace)
url = __send__("#{provider}_project_url", path_with_namespace) url = __send__("#{provider}_project_url", path_with_namespace)
link_to path_with_namespace, url, target: '_blank' link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
end end
private private
......
...@@ -16,6 +16,7 @@ module NavHelper ...@@ -16,6 +16,7 @@ module NavHelper
"page-gutter build-sidebar right-sidebar-expanded" "page-gutter build-sidebar right-sidebar-expanded"
elsif current_path?('wikis#show') || elsif current_path?('wikis#show') ||
current_path?('wikis#edit') || current_path?('wikis#edit') ||
current_path?('wikis#update') ||
current_path?('wikis#history') || current_path?('wikis#history') ||
current_path?('wikis#git_access') current_path?('wikis#git_access')
"page-gutter wiki-sidebar right-sidebar-expanded" "page-gutter wiki-sidebar right-sidebar-expanded"
......
...@@ -39,9 +39,13 @@ module TodosHelper ...@@ -39,9 +39,13 @@ module TodosHelper
namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project, namespace_project_commit_path(todo.project.namespace.becomes(Namespace), todo.project,
todo.target, anchor: anchor) todo.target, anchor: anchor)
else else
path = [todo.project.namespace.becomes(Namespace), todo.project, todo.target] if todo.build_failed?
# associated namespace and route would be loaded from the db again if todo.project was used
path.unshift(:pipelines) if todo.build_failed? project = todo.target.project
path = [:pipelines, project.namespace.becomes(Namespace), project, todo.target]
else
path = [todo.target]
end
polymorphic_path(path, anchor: anchor) polymorphic_path(path, anchor: anchor)
end end
......
module Emails
module Builds
def build_fail_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
add_project_headers
add_build_headers('failed')
mail(to: to, subject: subject("Build failed for #{@project.name}", @build.short_sha))
end
def build_success_email(build_id, to)
@build = Ci::Build.find(build_id)
@project = @build.project
add_project_headers
add_build_headers('success')
mail(to: to, subject: subject("Build success for #{@project.name}", @build.short_sha))
end
private
def add_build_headers(status)
headers['X-GitLab-Build-Id'] = @build.id
headers['X-GitLab-Build-Ref'] = @build.ref
headers['X-GitLab-Build-Status'] = status.to_s
end
end
end
...@@ -6,7 +6,6 @@ class Notify < BaseMailer ...@@ -6,7 +6,6 @@ class Notify < BaseMailer
include Emails::Notes include Emails::Notes
include Emails::Projects include Emails::Projects
include Emails::Profile include Emails::Profile
include Emails::Builds
include Emails::Pipelines include Emails::Pipelines
include Emails::Members include Emails::Members
......
...@@ -163,6 +163,8 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -163,6 +163,8 @@ class ApplicationSetting < ActiveRecord::Base
end end
def self.current def self.current
ensure_cache_setup
Rails.cache.fetch(CACHE_KEY) do Rails.cache.fetch(CACHE_KEY) do
ApplicationSetting.last ApplicationSetting.last
end end
...@@ -176,9 +178,16 @@ class ApplicationSetting < ActiveRecord::Base ...@@ -176,9 +178,16 @@ class ApplicationSetting < ActiveRecord::Base
end end
def self.cached def self.cached
ensure_cache_setup
Rails.cache.fetch(CACHE_KEY) Rails.cache.fetch(CACHE_KEY)
end end
def self.ensure_cache_setup
# This is a workaround for a Rails bug that causes attribute methods not
# to be loaded when read from cache: https://github.com/rails/rails/issues/27348
ApplicationSetting.define_attribute_methods
end
def self.defaults_ce def self.defaults_ce
{ {
after_sign_up_text: nil, after_sign_up_text: nil,
......
...@@ -15,7 +15,7 @@ module Ci ...@@ -15,7 +15,7 @@ module Ci
def persisted_environment def persisted_environment
@persisted_environment ||= Environment.find_by( @persisted_environment ||= Environment.find_by(
name: expanded_environment_name, name: expanded_environment_name,
project_id: gl_project_id project: project
) )
end end
...@@ -223,7 +223,8 @@ module Ci ...@@ -223,7 +223,8 @@ module Ci
def merge_request def merge_request
merge_requests = MergeRequest.includes(:merge_request_diff) merge_requests = MergeRequest.includes(:merge_request_diff)
.where(source_branch: ref, source_project_id: pipeline.gl_project_id) .where(source_branch: ref,
source_project: pipeline.project)
.reorder(iid: :asc) .reorder(iid: :asc)
merge_requests.find do |merge_request| merge_requests.find do |merge_request|
...@@ -231,10 +232,6 @@ module Ci ...@@ -231,10 +232,6 @@ module Ci
end end
end end
def project_id
gl_project_id
end
def repo_url def repo_url
auth = "gitlab-ci-token:#{ensure_token!}@" auth = "gitlab-ci-token:#{ensure_token!}@"
project.http_url_to_repo.sub(/^https?:\/\//) do |prefix| project.http_url_to_repo.sub(/^https?:\/\//) do |prefix|
...@@ -542,6 +539,16 @@ module Ci ...@@ -542,6 +539,16 @@ module Ci
Gitlab::Ci::Build::Credentials::Factory.new(self).create! Gitlab::Ci::Build::Credentials::Factory.new(self).create!
end end
def dependencies
depended_jobs = depends_on_builds
return depended_jobs unless options[:dependencies].present?
depended_jobs.select do |job|
options[:dependencies].include?(job.name)
end
end
private private
def update_artifacts_size def update_artifacts_size
...@@ -561,7 +568,7 @@ module Ci ...@@ -561,7 +568,7 @@ module Ci
end end
def unscoped_project def unscoped_project
@unscoped_project ||= Project.unscoped.find_by(id: gl_project_id) @unscoped_project ||= Project.unscoped.find_by(id: project_id)
end end
CI_REGISTRY_USER = 'gitlab-ci-token'.freeze CI_REGISTRY_USER = 'gitlab-ci-token'.freeze
......
...@@ -5,9 +5,7 @@ module Ci ...@@ -5,9 +5,7 @@ module Ci
include Importable include Importable
include AfterCommitQueue include AfterCommitQueue
self.table_name = 'ci_commits' belongs_to :project
belongs_to :project, foreign_key: :gl_project_id
belongs_to :user belongs_to :user
has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id has_many :statuses, class_name: 'CommitStatus', foreign_key: :commit_id
......
...@@ -9,7 +9,7 @@ module Ci ...@@ -9,7 +9,7 @@ module Ci
has_many :builds has_many :builds
has_many :runner_projects, dependent: :destroy has_many :runner_projects, dependent: :destroy
has_many :projects, through: :runner_projects, foreign_key: :gl_project_id has_many :projects, through: :runner_projects
has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build' has_one :last_build, ->() { order('id DESC') }, class_name: 'Ci::Build'
...@@ -24,7 +24,7 @@ module Ci ...@@ -24,7 +24,7 @@ module Ci
scope :owned_or_shared, ->(project_id) do scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id') joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id) .where("ci_runner_projects.project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end end
scope :assignable_for, ->(project) do scope :assignable_for, ->(project) do
......
...@@ -3,8 +3,8 @@ module Ci ...@@ -3,8 +3,8 @@ module Ci
extend Ci::Model extend Ci::Model
belongs_to :runner belongs_to :runner
belongs_to :project, foreign_key: :gl_project_id belongs_to :project
validates :runner_id, uniqueness: { scope: :gl_project_id } validates :runner_id, uniqueness: { scope: :project_id }
end end
end end
...@@ -4,7 +4,7 @@ module Ci ...@@ -4,7 +4,7 @@ module Ci
acts_as_paranoid acts_as_paranoid
belongs_to :project, foreign_key: :gl_project_id belongs_to :project
belongs_to :owner, class_name: "User" belongs_to :owner, class_name: "User"
has_many :trigger_requests, dependent: :destroy has_many :trigger_requests, dependent: :destroy
......
...@@ -2,11 +2,11 @@ module Ci ...@@ -2,11 +2,11 @@ module Ci
class Variable < ActiveRecord::Base class Variable < ActiveRecord::Base
extend Ci::Model extend Ci::Model
belongs_to :project, foreign_key: :gl_project_id belongs_to :project
validates :key, validates :key,
presence: true, presence: true,
uniqueness: { scope: :gl_project_id }, uniqueness: { scope: :project_id },
length: { maximum: 255 }, length: { maximum: 255 },
format: { with: /\A[a-zA-Z0-9_]+\z/, format: { with: /\A[a-zA-Z0-9_]+\z/,
message: "can contain only letters, digits and '_'." } message: "can contain only letters, digits and '_'." }
......
...@@ -321,8 +321,15 @@ class Commit ...@@ -321,8 +321,15 @@ class Commit
end end
def raw_diffs(*args) def raw_diffs(*args)
use_gitaly = Gitlab::GitalyClient.feature_enabled?(:commit_raw_diffs)
deltas_only = args.last.is_a?(Hash) && args.last[:deltas_only]
if use_gitaly && !deltas_only
Gitlab::GitalyClient::Commit.diff_from_parent(self, *args)
else
raw.diffs(*args) raw.diffs(*args)
end end
end
def diffs(diff_options = nil) def diffs(diff_options = nil)
Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options) Gitlab::Diff::FileCollection::Commit.new(self, diff_options: diff_options)
......
...@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base ...@@ -5,7 +5,7 @@ class CommitStatus < ActiveRecord::Base
self.table_name = 'ci_builds' self.table_name = 'ci_builds'
belongs_to :project, foreign_key: :gl_project_id belongs_to :project
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id
belongs_to :user belongs_to :user
...@@ -133,6 +133,12 @@ class CommitStatus < ActiveRecord::Base ...@@ -133,6 +133,12 @@ class CommitStatus < ActiveRecord::Base
false false
end end
# Added in 9.0 to keep backward compatibility for projects exported in 8.17
# and prior.
def gl_project_id
'dummy'
end
def detailed_status(current_user) def detailed_status(current_user)
Gitlab::Ci::Status::Factory Gitlab::Ci::Status::Factory
.new(self, current_user) .new(self, current_user)
......
...@@ -48,11 +48,13 @@ module Issuable ...@@ -48,11 +48,13 @@ module Issuable
delegate :name, delegate :name,
:email, :email,
:public_email,
to: :author, to: :author,
prefix: true prefix: true
delegate :name, delegate :name,
:email, :email,
:public_email,
to: :assignee, to: :assignee,
allow_nil: true, allow_nil: true,
prefix: true prefix: true
......
...@@ -51,10 +51,12 @@ module Routable ...@@ -51,10 +51,12 @@ module Routable
paths.each do |path| paths.each do |path|
path = connection.quote(path) path = connection.quote(path)
where = "(routes.path = #{path})"
where =
if cast_lower if cast_lower
where = "(#{where} OR (LOWER(routes.path) = LOWER(#{path})))" "(LOWER(routes.path) = LOWER(#{path}))"
else
"(routes.path = #{path})"
end end
wheres << where wheres << where
......
...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base ...@@ -16,7 +16,7 @@ class Event < ActiveRecord::Base
RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour RESET_PROJECT_ACTIVITY_INTERVAL = 1.hour
delegate :name, :email, to: :author, prefix: true, allow_nil: true delegate :name, :email, :public_email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true delegate :title, to: :issue, prefix: true, allow_nil: true
delegate :title, to: :merge_request, prefix: true, allow_nil: true delegate :title, to: :merge_request, prefix: true, allow_nil: true
delegate :title, to: :note, prefix: true, allow_nil: true delegate :title, to: :note, prefix: true, allow_nil: true
......
...@@ -55,6 +55,14 @@ class Issue < ActiveRecord::Base ...@@ -55,6 +55,14 @@ class Issue < ActiveRecord::Base
state :opened state :opened
state :reopened state :reopened
state :closed state :closed
before_transition any => :closed do |issue|
issue.closed_at = Time.zone.now
end
before_transition closed: any do |issue|
issue.closed_at = nil
end
end end
def hook_attrs def hook_attrs
......
...@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base ...@@ -7,6 +7,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :target_project, class_name: "Project" belongs_to :target_project, class_name: "Project"
belongs_to :source_project, class_name: "Project" belongs_to :source_project, class_name: "Project"
belongs_to :project, foreign_key: :target_project_id
belongs_to :merge_user, class_name: "User" belongs_to :merge_user, class_name: "User"
has_many :merge_request_diffs, dependent: :destroy has_many :merge_request_diffs, dependent: :destroy
...@@ -540,10 +541,6 @@ class MergeRequest < ActiveRecord::Base ...@@ -540,10 +541,6 @@ class MergeRequest < ActiveRecord::Base
target_project != source_project target_project != source_project
end end
def project
target_project
end
# If the merge request closes any issues, save this information in the # If the merge request closes any issues, save this information in the
# `MergeRequestsClosingIssues` model. This is a performance optimization. # `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires # Calculating this information for a number of merge requests requires
......
...@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base ...@@ -89,7 +89,6 @@ class Project < ActiveRecord::Base
has_one :campfire_service, dependent: :destroy has_one :campfire_service, dependent: :destroy
has_one :drone_ci_service, dependent: :destroy has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy has_one :emails_on_push_service, dependent: :destroy
has_one :builds_email_service, dependent: :destroy
has_one :pipelines_email_service, dependent: :destroy has_one :pipelines_email_service, dependent: :destroy
has_one :irker_service, dependent: :destroy has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy has_one :pivotaltracker_service, dependent: :destroy
...@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base ...@@ -159,13 +158,13 @@ class Project < ActiveRecord::Base
has_one :project_feature, dependent: :destroy has_one :project_feature, dependent: :destroy
has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete has_one :statistics, class_name: 'ProjectStatistics', dependent: :delete
has_many :commit_statuses, dependent: :destroy, foreign_key: :gl_project_id has_many :commit_statuses, dependent: :destroy
has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline', foreign_key: :gl_project_id has_many :pipelines, dependent: :destroy, class_name: 'Ci::Pipeline'
has_many :builds, class_name: 'Ci::Build', foreign_key: :gl_project_id # the builds are created from the commit_statuses has_many :builds, class_name: 'Ci::Build' # the builds are created from the commit_statuses
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject', foreign_key: :gl_project_id has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner' has_many :runners, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
has_many :variables, dependent: :destroy, class_name: 'Ci::Variable', foreign_key: :gl_project_id has_many :variables, dependent: :destroy, class_name: 'Ci::Variable'
has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger', foreign_key: :gl_project_id has_many :triggers, dependent: :destroy, class_name: 'Ci::Trigger'
has_many :environments, dependent: :destroy has_many :environments, dependent: :destroy
has_many :deployments, dependent: :destroy has_many :deployments, dependent: :destroy
...@@ -197,6 +196,7 @@ class Project < ActiveRecord::Base ...@@ -197,6 +196,7 @@ class Project < ActiveRecord::Base
validates :name, uniqueness: { scope: :namespace_id } validates :name, uniqueness: { scope: :namespace_id }
validates :path, uniqueness: { scope: :namespace_id } validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import? validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 } validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create validate :check_limit, on: :create
validate :avatar_type, validate :avatar_type,
...@@ -881,13 +881,9 @@ class Project < ActiveRecord::Base ...@@ -881,13 +881,9 @@ class Project < ActiveRecord::Base
end end
def http_url_to_repo(user = nil) def http_url_to_repo(user = nil)
url = web_url credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
if user Gitlab::UrlSanitizer.new("#{web_url}.git", credentials: credentials).full_url
url.sub!(%r{\Ahttps?://}) { |protocol| "#{protocol}#{user.username}@" }
end
"#{url}.git"
end end
# Check if current branch name is marked as protected in the system # Check if current branch name is marked as protected in the system
......
# This class is to be removed with 9.1
# We should also by then remove BuildsEmailService from database
class BuildsEmailService < Service class BuildsEmailService < Service
prop_accessor :recipients
boolean_accessor :add_pusher
boolean_accessor :notify_only_broken_builds
validates :recipients, presence: true, if: ->(s) { s.activated? && !s.add_pusher? }
def initialize_properties
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
end
end
def title
'Builds emails'
end
def description
'Email the builds status to a list of recipients.'
end
def self.to_param def self.to_param
'builds_email' 'builds_email'
end end
def self.supported_events def self.supported_events
%w(build) %w[]
end
def execute(push_data)
return unless supported_events.include?(push_data[:object_kind])
return unless should_build_be_notified?(push_data)
recipients = all_recipients(push_data)
if recipients.any?
BuildEmailWorker.perform_async(
push_data[:build_id],
recipients,
push_data
)
end
end
def can_test?
project.builds.any?
end
def disabled_title
"Please setup a build on your repository."
end
def test_data(project = nil, user = nil)
Gitlab::DataBuilder::Build.build(project.builds.last)
end
def fields
[
{ type: 'textarea', name: 'recipients', placeholder: 'Emails separated by comma' },
{ type: 'checkbox', name: 'add_pusher', label: 'Add pusher to recipients list' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
]
end
def test(data)
begin
# bypass build status verification when testing
data[:build_status] = "failed"
data[:build_allow_failure] = false
result = execute(data)
rescue StandardError => error
return { success: false, result: error }
end
{ success: true, result: result }
end
def should_build_be_notified?(data)
case data[:build_status]
when 'success'
!notify_only_broken_builds?
when 'failed'
!allow_failure?(data)
else
false
end
end
def allow_failure?(data)
data[:build_allow_failure] == true
end
def all_recipients(data)
all_recipients = []
unless recipients.blank?
all_recipients += recipients.split(',').compact.reject(&:blank?)
end
if add_pusher? && data[:user][:email]
all_recipients << data[:user][:email]
end
all_recipients
end end
end end
module ChatMessage
class BuildMessage < BaseMessage
attr_reader :sha
attr_reader :ref_type
attr_reader :ref
attr_reader :status
attr_reader :project_name
attr_reader :project_url
attr_reader :user_name
attr_reader :user_url
attr_reader :duration
attr_reader :stage
attr_reader :build_id
attr_reader :build_name
def initialize(params)
@sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref]
@project_name = params[:project_name]
@project_url = params[:project_url]
@status = params[:commit][:status]
@user_name = params[:commit][:author_name]
@user_url = params[:commit][:author_url]
@duration = params[:commit][:duration]
@stage = params[:build_stage]
@build_name = params[:build_name]
@build_id = params[:build_id]
end
def pretext
''
end
def fallback
format(message)
end
def attachments
[{ text: format(message), color: attachment_color }]
end
private
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_link} #{humanized_status} on build #{build_link} of stage #{stage} in #{duration} #{'second'.pluralize(duration)}"
end
def build_url
"#{project_url}/builds/#{build_id}"
end
def build_link
link(build_name, build_url)
end
def user_link
link(user_name, user_url)
end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
end
def humanized_status
case status
when 'success'
'passed'
else
status
end
end
def attachment_color
if status == 'success'
'good'
else
'danger'
end
end
def branch_url
"#{project_url}/commits/#{ref}"
end
def branch_link
link(ref, branch_url)
end
def project_link
link(project_name, project_url)
end
def commit_url
"#{project_url}/commit/#{sha}/builds"
end
def commit_link
link(Commit.truncate_sha(sha), commit_url)
end
end
end
...@@ -6,7 +6,7 @@ class ChatNotificationService < Service ...@@ -6,7 +6,7 @@ class ChatNotificationService < Service
default_value_for :category, 'chat' default_value_for :category, 'chat'
prop_accessor :webhook, :username, :channel prop_accessor :webhook, :username, :channel
boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines boolean_accessor :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated? validates :webhook, presence: true, url: true, if: :activated?
...@@ -16,7 +16,6 @@ class ChatNotificationService < Service ...@@ -16,7 +16,6 @@ class ChatNotificationService < Service
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.notify_only_broken_builds = true
self.notify_only_broken_pipelines = true self.notify_only_broken_pipelines = true
end end
end end
...@@ -27,7 +26,7 @@ class ChatNotificationService < Service ...@@ -27,7 +26,7 @@ class ChatNotificationService < Service
def self.supported_events def self.supported_events
%w[push issue confidential_issue merge_request note tag_push %w[push issue confidential_issue merge_request note tag_push
build pipeline wiki_page] pipeline wiki_page]
end end
def execute(data) def execute(data)
...@@ -89,8 +88,6 @@ class ChatNotificationService < Service ...@@ -89,8 +88,6 @@ class ChatNotificationService < Service
ChatMessage::MergeMessage.new(data) unless is_update?(data) ChatMessage::MergeMessage.new(data) unless is_update?(data)
when "note" when "note"
ChatMessage::NoteMessage.new(data) ChatMessage::NoteMessage.new(data)
when "build"
ChatMessage::BuildMessage.new(data) if should_build_be_notified?(data)
when "pipeline" when "pipeline"
ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data) ChatMessage::PipelineMessage.new(data) if should_pipeline_be_notified?(data)
when "wiki_page" when "wiki_page"
...@@ -125,17 +122,6 @@ class ChatNotificationService < Service ...@@ -125,17 +122,6 @@ class ChatNotificationService < Service
data[:object_attributes][:action] == 'update' data[:object_attributes][:action] == 'update'
end end
def should_build_be_notified?(data)
case data[:commit][:status]
when 'success'
!notify_only_broken_builds?
when 'failed'
true
else
false
end
end
def should_pipeline_be_notified?(data) def should_pipeline_be_notified?(data)
case data[:object_attributes][:status] case data[:object_attributes][:status]
when 'success' when 'success'
......
...@@ -9,13 +9,13 @@ class HipchatService < Service ...@@ -9,13 +9,13 @@ class HipchatService < Service
].freeze ].freeze
prop_accessor :token, :room, :server, :color, :api_version prop_accessor :token, :room, :server, :color, :api_version
boolean_accessor :notify_only_broken_builds, :notify boolean_accessor :notify_only_broken_pipelines, :notify
validates :token, presence: true, if: :activated? validates :token, presence: true, if: :activated?
def initialize_properties def initialize_properties
if properties.nil? if properties.nil?
self.properties = {} self.properties = {}
self.notify_only_broken_builds = true self.notify_only_broken_pipelines = true
end end
end end
...@@ -41,12 +41,12 @@ class HipchatService < Service ...@@ -41,12 +41,12 @@ class HipchatService < Service
placeholder: 'Leave blank for default (v2)' }, placeholder: 'Leave blank for default (v2)' },
{ type: 'text', name: 'server', { type: 'text', name: 'server',
placeholder: 'Leave blank for default. https://hipchat.example.com' }, placeholder: 'Leave blank for default. https://hipchat.example.com' },
{ type: 'checkbox', name: 'notify_only_broken_builds' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
def self.supported_events def self.supported_events
%w(push issue confidential_issue merge_request note tag_push build) %w(push issue confidential_issue merge_request note tag_push pipeline)
end end
def execute(data) def execute(data)
...@@ -90,8 +90,8 @@ class HipchatService < Service ...@@ -90,8 +90,8 @@ class HipchatService < Service
create_merge_request_message(data) unless is_update?(data) create_merge_request_message(data) unless is_update?(data)
when "note" when "note"
create_note_message(data) create_note_message(data)
when "build" when "pipeline"
create_build_message(data) if should_build_be_notified?(data) create_pipeline_message(data) if should_pipeline_be_notified?(data)
end end
end end
...@@ -240,28 +240,29 @@ class HipchatService < Service ...@@ -240,28 +240,29 @@ class HipchatService < Service
message message
end end
def create_build_message(data) def create_pipeline_message(data)
ref_type = data[:tag] ? 'tag' : 'branch' pipeline_attributes = data[:object_attributes]
ref = data[:ref] pipeline_id = pipeline_attributes[:id]
sha = data[:sha] ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
user_name = data[:commit][:author_name] ref = pipeline_attributes[:ref]
status = data[:commit][:status] user_name = (data[:user] && data[:user][:name]) || 'API'
duration = data[:commit][:duration] status = pipeline_attributes[:status]
duration = pipeline_attributes[:duration]
branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>" branch_link = "<a href=\"#{project_url}/commits/#{CGI.escape(ref)}\">#{ref}</a>"
commit_link = "<a href=\"#{project_url}/commit/#{CGI.escape(sha)}/builds\">#{Commit.truncate_sha(sha)}</a>" pipeline_url = "<a href=\"#{project_url}/pipelines/#{pipeline_id}\">##{pipeline_id}</a>"
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)" "#{project_link}: Pipeline #{pipeline_url} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status(status)} in #{duration} second(s)"
end end
def message_color(data) def message_color(data)
build_status_color(data) || color || 'yellow' pipeline_status_color(data) || color || 'yellow'
end end
def build_status_color(data) def pipeline_status_color(data)
return unless data && data[:object_kind] == 'build' return unless data && data[:object_kind] == 'pipeline'
case data[:commit][:status] case data[:object_attributes][:status]
when 'success' when 'success'
'green' 'green'
else else
...@@ -294,10 +295,10 @@ class HipchatService < Service ...@@ -294,10 +295,10 @@ class HipchatService < Service
end end
end end
def should_build_be_notified?(data) def should_pipeline_be_notified?(data)
case data[:commit][:status] case data[:object_attributes][:status]
when 'success' when 'success'
!notify_only_broken_builds? !notify_only_broken_pipelines?
when 'failed' when 'failed'
true true
else else
......
...@@ -30,7 +30,6 @@ class MattermostService < ChatNotificationService ...@@ -30,7 +30,6 @@ class MattermostService < ChatNotificationService
[ [
{ type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' }, { type: 'text', name: 'webhook', placeholder: 'e.g. http://mattermost_host/hooks/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
......
...@@ -29,7 +29,6 @@ class SlackService < ChatNotificationService ...@@ -29,7 +29,6 @@ class SlackService < ChatNotificationService
[ [
{ type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' }, { type: 'text', name: 'webhook', placeholder: 'e.g. https://hooks.slack.com/services/…' },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' }, { type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' }, { type: 'checkbox', name: 'notify_only_broken_pipelines' },
] ]
end end
......
...@@ -42,8 +42,11 @@ class ProjectWiki ...@@ -42,8 +42,11 @@ class ProjectWiki
url_to_repo url_to_repo
end end
def http_url_to_repo def http_url_to_repo(user = nil)
[Gitlab.config.gitlab.url, "/", path_with_namespace, ".git"].join('') url = "#{Gitlab.config.gitlab.url}/#{path_with_namespace}.git"
credentials = Gitlab::UrlSanitizer.http_credentials_for_user(user)
Gitlab::UrlSanitizer.new(url, credentials: credentials).full_url
end end
def wiki_base_path def wiki_base_path
......
...@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base ...@@ -21,7 +21,7 @@ class Route < ActiveRecord::Base
attributes[:path] = route.path.sub(path_was, path) attributes[:path] = route.path.sub(path_was, path)
end end
if name_changed? && route.name.present? if name_changed? && name_was.present? && route.name.present?
attributes[:name] = route.name.sub(name_was, name) attributes[:name] = route.name.sub(name_was, name)
end end
......
...@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base ...@@ -215,7 +215,6 @@ class Service < ActiveRecord::Base
assembla assembla
bamboo bamboo
buildkite buildkite
builds_email
bugzilla bugzilla
campfire campfire
custom_issue_tracker custom_issue_tracker
......
...@@ -877,7 +877,7 @@ class User < ActiveRecord::Base ...@@ -877,7 +877,7 @@ class User < ActiveRecord::Base
def ci_authorized_runners def ci_authorized_runners
@ci_authorized_runners ||= begin @ci_authorized_runners ||= begin
runner_ids = Ci::RunnerProject. runner_ids = Ci::RunnerProject.
where("ci_runner_projects.gl_project_id IN (#{ci_projects_union.to_sql})"). where("ci_runner_projects.project_id IN (#{ci_projects_union.to_sql})").
select(:runner_id) select(:runner_id)
Ci::Runner.specific.where(id: runner_ids) Ci::Runner.specific.where(id: runner_ids)
end end
......
...@@ -155,7 +155,7 @@ class WikiPage ...@@ -155,7 +155,7 @@ class WikiPage
end end
# Returns boolean True or False if this instance # Returns boolean True or False if this instance
# has been fully saved to disk or not. # has been fully created on disk or not.
def persisted? def persisted?
@persisted == true @persisted == true
end end
...@@ -226,6 +226,8 @@ class WikiPage ...@@ -226,6 +226,8 @@ class WikiPage
end end
def save(method, *args) def save(method, *args)
saved = false
project_wiki = wiki project_wiki = wiki
if valid? && project_wiki.send(method, *args) if valid? && project_wiki.send(method, *args)
...@@ -243,10 +245,10 @@ class WikiPage ...@@ -243,10 +245,10 @@ class WikiPage
set_attributes set_attributes
@persisted = true @persisted = true
saved = true
else else
errors.add(:base, project_wiki.error_message) if project_wiki.error_message errors.add(:base, project_wiki.error_message) if project_wiki.error_message
@persisted = false
end end
@persisted saved
end end
end end
...@@ -55,13 +55,13 @@ module Ci ...@@ -55,13 +55,13 @@ module Ci
new_builds. new_builds.
# don't run projects which have not enabled shared runners and builds # don't run projects which have not enabled shared runners and builds
joins(:project).where(projects: { shared_runners_enabled: true }). joins(:project).where(projects: { shared_runners_enabled: true }).
joins('LEFT JOIN project_features ON ci_builds.gl_project_id = project_features.project_id'). joins('LEFT JOIN project_features ON ci_builds.project_id = project_features.project_id').
where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0'). where('project_features.builds_access_level IS NULL or project_features.builds_access_level > 0').
# Implement fair scheduling # Implement fair scheduling
# this returns builds that are ordered by number of running builds # this returns builds that are ordered by number of running builds
# we prefer projects that don't use shared runners at all # we prefer projects that don't use shared runners at all
joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.gl_project_id=project_builds.gl_project_id"). joins("LEFT JOIN (#{running_builds_for_shared_runners.to_sql}) AS project_builds ON ci_builds.project_id=project_builds.project_id").
order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC') order('COALESCE(project_builds.running_builds, 0) ASC', 'ci_builds.id ASC')
end end
...@@ -71,7 +71,7 @@ module Ci ...@@ -71,7 +71,7 @@ module Ci
def running_builds_for_shared_runners def running_builds_for_shared_runners
Ci::Build.running.where(runner: Ci::Runner.shared). Ci::Build.running.where(runner: Ci::Runner.shared).
group(:gl_project_id).select(:gl_project_id, 'count(*) AS running_builds') group(:project_id).select(:project_id, 'count(*) AS running_builds')
end end
def new_builds def new_builds
......
...@@ -6,7 +6,7 @@ module MergeRequests ...@@ -6,7 +6,7 @@ module MergeRequests
merge_request.source_project = find_source_project merge_request.source_project = find_source_project
merge_request.target_project = find_target_project merge_request.target_project = find_target_project
merge_request.target_branch = find_target_branch merge_request.target_branch = find_target_branch
merge_request.can_be_created = branches_valid? && source_branch_specified? && target_branch_specified? merge_request.can_be_created = branches_valid?
compare_branches if branches_present? compare_branches if branches_present?
assign_title_and_description if merge_request.can_be_created assign_title_and_description if merge_request.can_be_created
......
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
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