Merge branch 'master' into fix/gb/pipeline-retry-builds-started

* master: (313 commits)
  Allow slashes in slash command arguments
  Add API endpoint to get all milestone merge requests
  remove trailing comma
  Restore pagination to admin abuse reports
  replace deprecated NoErrorsPlugin with NoEmitOnErrorsPlugin
  only compress assets in production
  Reduce number of pipelines created to test pagination
  add CHANGELOG.md entry for !8761
  prevent diff unfolding link from appearing for deleted files
  fix build failures
  only show diff unfolding link if there are more lines to show
  fix typo in node section
  Only yield valid references in ReferenceFilter.references_in
  Cache js selectors; fix css
  move "Install node modules" step before "Migrate DB" within update process
  Renders pagination again for pipelines table
  update migration docs for 8.17 to include minimum node version
  Add CHANGELOG file
  Fix positioning of top scroll button
  Remove comments in migration
  ...
This commit is contained in:
Grzegorz Bizon 2017-02-17 14:31:00 +01:00
commit 5f271a9fa2
638 changed files with 13916 additions and 6910 deletions

View File

@ -12,12 +12,18 @@
"localStorage": false
},
"plugins": [
"filenames"
"filenames",
"import"
],
"settings": {
"import/resolver": {
"webpack": {
"config": "./config/webpack.config.js"
}
}
},
"rules": {
"filenames/match-regex": [2, "^[a-z0-9_]+(.js)?$"],
"no-multiple-empty-lines": ["error", { "max": 1 }],
"import/no-extraneous-dependencies": "off",
"import/no-unresolved": "off"
"no-multiple-empty-lines": ["error", { "max": 1 }]
}
}

View File

@ -107,7 +107,10 @@ setup-test-env:
<<: *dedicated-runner
stage: prepare
script:
- npm install
- node --version
- yarn --version
- yarn install --pure-lockfile
- yarn check # ensure that yarn.lock matches package.json
- bundle exec rake gitlab:assets:compile
- bundle exec ruby -Ispec -e 'require "spec_helper" ; TestEnv.init'
artifacts:
@ -246,13 +249,12 @@ karma:
<<: *use-db
<<: *dedicated-runner
script:
- npm link istanbul
- bundle exec rake karma
artifacts:
name: coverage-javascript
expire_in: 31d
paths:
- coverage-javascript/default/
- coverage-javascript/
lint-doc:
stage: test
@ -325,11 +327,9 @@ lint:javascript:
paths:
- node_modules/
stage: test
image: "node:7.1"
before_script:
- npm install
before_script: []
script:
- npm --silent run eslint
- yarn run eslint
lint:javascript:report:
<<: *dedicated-runner
@ -337,12 +337,10 @@ lint:javascript:report:
paths:
- node_modules/
stage: post-test
image: "node:7.1"
before_script:
- npm install
before_script: []
script:
- find app/ spec/ -name '*.js' -or -name '*.js.es6' -exec sed --in-place 's|/\* eslint-disable .*\*/||' {} \; # run report over all files
- npm --silent run eslint-report || true # ignore exit code
- yarn run eslint-report || true # ignore exit code
artifacts:
name: eslint-report
expire_in: 31d
@ -395,7 +393,7 @@ pages:
- mv public/ .public/
- mkdir public/
- mv coverage/ public/coverage-ruby/ || true
- mv coverage-javascript/default/ public/coverage-javascript/ || true
- mv coverage-javascript/ public/coverage-javascript/ || true
- mv eslint-report.html public/ || true
artifacts:
paths:

View File

@ -5,7 +5,7 @@ require:
inherit_from: .rubocop_todo.yml
AllCops:
TargetRubyVersion: 2.1
TargetRubyVersion: 2.3
# Cop names are not d§splayed in offense messages by default. Change behavior
# by overriding DisplayCopNames, or by giving the -D/--display-cop-names
# option.
@ -339,6 +339,10 @@ Style/OpMethod:
Style/ParenthesesAroundCondition:
Enabled: true
# Checks for an obsolete RuntimeException argument in raise/fail.
Style/RedundantException:
Enabled: true
# Checks for parentheses that seem not to serve any purpose.
Style/RedundantParentheses:
Enabled: true
@ -568,6 +572,10 @@ Lint/ElseLayout:
Lint/EmptyEnsure:
Enabled: true
# Checks for the presence of `when` branches without a body.
Lint/EmptyWhen:
Enabled: true
# Align ends correctly.
Lint/EndAlignment:
Enabled: true
@ -769,6 +777,10 @@ Rails/ScopeArgs:
RSpec/AnyInstance:
Enabled: false
# Check for expectations where `be(...)` can replace `eql(...)`.
RSpec/BeEql:
Enabled: false
# Check that the first argument to the top level describe is the tested class or
# module.
RSpec/DescribeClass:
@ -797,6 +809,10 @@ RSpec/ExampleWording:
not: does not
IgnoredWords: []
# Checks for `expect(...)` calls containing literal values.
RSpec/ExpectActual:
Enabled: true
# Checks the file and folder naming of the spec file.
RSpec/FilePath:
Enabled: false

View File

@ -21,10 +21,6 @@ Lint/AmbiguousRegexpLiteral:
Lint/AssignmentInCondition:
Enabled: false
# Offense count: 1
Lint/EmptyWhen:
Enabled: false
# Offense count: 20
Lint/HandleExceptions:
Enabled: false
@ -80,19 +76,11 @@ Performance/RedundantMatch:
Performance/RedundantMerge:
Enabled: false
# Offense count: 7
RSpec/BeEql:
Enabled: false
# Offense count: 15
# Configuration parameters: CustomIncludeMethods.
RSpec/EmptyExampleGroup:
Enabled: false
# Offense count: 24
RSpec/ExpectActual:
Enabled: false
# Offense count: 58
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: implicit, each, example
@ -424,11 +412,6 @@ Style/RaiseArgs:
Style/RedundantBegin:
Enabled: false
# Offense count: 1
# Cop supports --auto-correct.
Style/RedundantException:
Enabled: false
# Offense count: 29
# Cop supports --auto-correct.
Style/RedundantFreeze:

View File

@ -2,6 +2,13 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
## 8.16.5 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.16.4 (2017-02-02)
- Support non-ASCII characters in GFM autocomplete. !8729
@ -174,6 +181,13 @@ entry.
- Add margin to markdown math blocks.
- Add hover state to MR comment reply button.
## 8.15.6 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.15.4 (2017-01-09)
- Make successful pipeline emails off for watchers. !8176
@ -437,6 +451,13 @@ entry.
- Whitelist next project names: help, ci, admin, search. !8227
- Adds back CSS for progress-bars. !8237
## 8.14.9 (2017-02-14)
- Patch Asciidocs rendering to block XSS.
- Fix XSS vulnerability in SVG attachments.
- Prevent the GitHub importer from assigning labels and comments to merge requests or issues belonging to other projects.
- Patch XSS vulnerability in RDOC support.
## 8.14.8 (2017-01-25)
- Accept environment variables from the `pre-receive` script. !7967

View File

@ -29,6 +29,7 @@ gem 'omniauth-github', '~> 1.1.1'
gem 'omniauth-gitlab', '~> 1.0.2'
gem 'omniauth-google-oauth2', '~> 0.4.1'
gem 'omniauth-kerberos', '~> 0.3.0', group: :kerberos
gem 'omniauth-oauth2-generic', '~> 0.2.2'
gem 'omniauth-saml', '~> 1.7.0'
gem 'omniauth-shibboleth', '~> 1.2.0'
gem 'omniauth-twitter', '~> 1.2.0'

View File

@ -483,6 +483,8 @@ GEM
omniauth-oauth2 (1.3.1)
oauth2 (~> 1.0)
omniauth (~> 1.2)
omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
omniauth-saml (1.7.0)
omniauth (~> 1.3)
ruby-saml (~> 1.4)
@ -931,6 +933,7 @@ DEPENDENCIES
omniauth-gitlab (~> 1.0.2)
omniauth-google-oauth2 (~> 0.4.1)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
omniauth-saml (~> 1.7.0)
omniauth-shibboleth (~> 1.2.0)
omniauth-twitter (~> 1.2.0)

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -11,7 +11,7 @@
licensePath: "/api/:version/templates/licenses/:key",
gitignorePath: "/api/:version/templates/gitignores/:key",
gitlabCiYmlPath: "/api/:version/templates/gitlab_ci_ymls/:key",
dockerfilePath: "/api/:version/dockerfiles/:key",
dockerfilePath: "/api/:version/templates/dockerfiles/:key",
issuableTemplatePath: "/:namespace_path/:project_path/templates/:type/:key",
group: function(group_id, callback) {
var url = Api.buildUrl(Api.groupPath)

View File

@ -56,8 +56,7 @@ requireAll(require.context('./u2f', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('./droplab', false, /^\.\/.*\.(js|es6)$/));
requireAll(require.context('.', false, /^\.\/(?!application\.js).*\.(js|es6)$/));
require('vendor/fuzzaldrin-plus');
window.ES6Promise = require('vendor/es6-promise.auto');
window.ES6Promise.polyfill();
require('es6-promise').polyfill();
(function () {
document.addEventListener('beforeunload', function () {
@ -102,11 +101,6 @@ window.ES6Promise.polyfill();
}
});
$('.nav-sidebar').niceScroll({
cursoropacitymax: '0.4',
cursorcolor: '#FFF',
cursorborder: '1px solid #FFF'
});
$('.js-select-on-focus').on('focusin', function () {
return $(this).select().one('mouseup', function (e) {
return e.preventDefault();
@ -246,8 +240,6 @@ window.ES6Promise.polyfill();
});
gl.awardsHandler = new AwardsHandler();
new Aside();
// bind sidebar events
new gl.Sidebar();
gl.utils.initTimeagoTimeout();
});

View File

@ -95,7 +95,7 @@ $(() => {
},
computed: {
disabled() {
return Store.shouldAddBlankState();
return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
},
},
template: `

View File

@ -3,5 +3,5 @@
Vue.filter('due-date', (value) => {
const date = new Date(value);
return dateFormat(date, 'mmm d, yyyy');
return dateFormat(date, 'mmm d, yyyy', true);
});

View File

@ -20,7 +20,10 @@ $(() => {
gl.commits.PipelinesTableBundle.$destroy(true);
}
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView({
el: document.querySelector('#commit-pipeline-table-view'),
});
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle = new gl.commits.pipelines.PipelinesTableView();
if (pipelineTableViewEl && pipelineTableViewEl.dataset.disableInitialization === undefined) {
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
}
});

View File

@ -8,7 +8,22 @@
* Uses Vue.Resource
*/
class PipelinesService {
constructor(endpoint) {
/**
* 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);
}

View File

@ -4,6 +4,7 @@
*
* Used to store the Pipelines rendered in the commit view in the pipelines table.
*/
require('../../vue_realtime_listener');
class PipelinesStore {
constructor() {
@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed.
*
*/
startTimeAgoLoops() {
static startTimeAgoLoops() {
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
@ -44,7 +45,4 @@ class PipelinesStore {
}
}
window.gl = window.gl || {};
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
module.exports = PipelinesStore;

View File

@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service');
require('./pipelines_store');
const PipelineStore = require('./pipelines_store');
/**
*
@ -41,7 +40,7 @@ require('./pipelines_store');
data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset;
const store = new gl.commits.pipelines.PipelinesStore();
const store = new PipelineStore();
// Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
@ -56,15 +55,14 @@ require('./pipelines_store');
},
/**
* When the component is created the service to fetch the data will be
* initialized with the correct endpoint.
* When the component is about to be mounted, tell the service to fetch the data
*
* A request to fetch the pipelines will be made.
* In case of a successfull response we will store the data in the provided
* store, in case of a failed response we need to warn the user.
*
*/
created() {
beforeMount() {
const pipelinesService = new gl.commits.pipelines.PipelinesService(this.endpoint);
this.isLoading = true;
@ -72,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json())
.then((json) => {
this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false;
})
.catch(() => {
@ -81,9 +78,15 @@ require('./pipelines_store');
});
},
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
},
template: `
<div>
<div class="pipelines realtime-loading" v-if="isLoading">
<div class="pipelines">
<div class="realtime-loading" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>

View File

@ -13,6 +13,12 @@
<div>
<div class="events-description">
{{ stage.description }}
<span v-if="items.length === 50" class="events-info pull-right">
<i class="fa fa-warning has-tooltip"
title="Limited to showing 50 events at most"
data-placement="top"></i>
Showing 50 events
</span>
</div>
<ul class="stage-event-list">
<li v-for="commit in items" class="stage-event-item">

View File

@ -48,7 +48,7 @@
const calendar = new Pikaday({
field: $dueDateInput.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect: (dateText) => {
const formattedDate = dateFormat(new Date(dateText), 'yyyy-mm-dd');
@ -63,6 +63,7 @@
}
});
calendar.setDate(new Date($dueDateInput.val()));
this.$datePicker.append(calendar.el);
this.$datePicker.data('pikaday', calendar);
}
@ -169,11 +170,12 @@
const calendar = new Pikaday({
field: $datePicker.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect(dateText) {
$datePicker.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
calendar.setDate(new Date($datePicker.val()));
$datePicker.data('pikaday', calendar);
});

View File

@ -1,223 +1,192 @@
/* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
/* global Flash */
window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
require('../services/environments_service');
require('./environment_item');
const Vue = require('vue');
Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
(() => {
window.gl = window.gl || {};
module.exports = Vue.component('environment-component', {
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
type: Object,
required: true,
default: () => ({}),
},
components: {
'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
},
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
return {
store,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
canCreateEnvironment: environmentsData.canCreateEnvironment,
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return gl.utils.getParameterByName('scope');
},
components: {
'environment-item': gl.environmentsList.EnvironmentItem,
canReadEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
return {
state: this.store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
canCreateEnvironment: environmentsData.canCreateEnvironment,
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
};
canCreateDeploymentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
computed: {
scope() {
return this.$options.getQueryParameter('scope');
},
canCreateEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
},
canReadEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
},
},
canCreateDeploymentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
canCreateEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
},
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
gl.environmentsService = new EnvironmentsService(this.endpoint);
const scope = this.$options.getQueryParameter('scope');
if (scope) {
this.store.storeVisibility(scope);
}
this.isLoading = true;
return gl.environmentsService.all()
.then(resp => resp.json())
.then((json) => {
this.store.storeEnvironments(json);
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
/**
* Transforms the url parameter into an object and
* returns the one requested.
* Will change the page number and update the URL.
*
* @param {String} param
* @returns {String} The value of the requested parameter.
* @param {Number} pageNumber desired page to go to.
* @return {String}
*/
getQueryParameter(parameter) {
return window.location.search.substring(1).split('&').reduce((acc, param) => {
const paramSplited = param.split('=');
acc[paramSplited[0]] = paramSplited[1];
return acc;
}, {})[parameter];
},
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
/**
* Converts permission provided as strings to booleans.
* @param {String} string
* @returns {Boolean}
*/
convertPermissionToBoolean(string) {
return string === 'true';
gl.utils.visitUrl(param);
return param;
},
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area">
<ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === undefined }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li><li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
template: `
<div :class="cssContainerClass">
<div class="top-area">
<ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</div>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</div>
<div class="table-holder"
v-if="!isLoading && state.filteredEnvironments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0"
is="environment-item"
v-for="children in model.children"
:model="children"
:toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</tr>
</template>
</tbody>
</table>
</div>
</li>
</ul>
<div v-if="canCreateEnvironmentParsed && !isLoading" class="nav-controls">
<a :href="newEnvironmentPath" class="btn btn-create">
New environment
</a>
</div>
</div>
`,
});
})();
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="blank-state blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div>
</div>
</div>
`,
});

View File

@ -1,50 +1,43 @@
/* global Vue */
const Vue = require('vue');
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
playIconSvg: {
type: String,
required: false,
},
module.exports = Vue.component('actions-component', {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
template: `
<div class="inline">
<div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
</a>
playIconSvg: {
type: String,
required: false,
},
},
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<a :href="action.play_path"
data-method="post"
rel="nofollow"
class="js-manual-action-link">
template: `
<div class="inline">
<div class="dropdown">
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
<i class="fa fa-caret-down"></i>
</a>
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
<ul class="dropdown-menu dropdown-menu-align-right">
<li v-for="action in actions">
<a :href="action.play_path"
data-method="post"
rel="nofollow"
class="js-manual-action-link">
<span>
{{action.name}}
</span>
</a>
</li>
</ul>
</div>
<span class="js-action-play-icon-container" v-html="playIconSvg"></span>
<span>
{{action.name}}
</span>
</a>
</li>
</ul>
</div>
`,
});
})();
</div>
`,
});

View File

@ -1,23 +1,19 @@
/* global Vue */
/**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
default: '',
},
module.exports = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
default: '',
},
},
template: `
<a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i>
</a>
`,
});
})();
template: `
<a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i>
</a>
`,
});

View File

@ -1,33 +1,30 @@
/* global Vue */
/**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`
*/
const Vue = require('vue');
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
default: '',
},
isLastDeployment: {
type: Boolean,
default: true,
},
module.exports = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
default: '',
},
template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
</a>
`,
});
})();
isLastDeployment: {
type: Boolean,
default: true,
},
},
template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="isLastDeployment">
Re-deploy
</span>
<span v-else>
Rollback
</span>
</a>
`,
});

View File

@ -1,27 +1,24 @@
/* global Vue */
/**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
default: '',
},
module.exports = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
default: '',
},
},
template: `
<a class="btn stop-env-link"
:href="stopUrl"
data-confirm="Are you sure you want to stop this environment?"
data-method="post"
rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i>
</a>
`,
});
})();
template: `
<a class="btn stop-env-link"
:href="stopUrl"
data-confirm="Are you sure you want to stop this environment?"
data-method="post"
rel="nofollow">
<i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a>
`,
});

View File

@ -1,28 +1,25 @@
/* global Vue */
/**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: {
terminalPath: {
type: String,
default: '',
},
terminalIconSvg: {
type: String,
default: '',
},
module.exports = Vue.component('terminal-button-component', {
props: {
terminalPath: {
type: String,
default: '',
},
terminalIconSvg: {
type: String,
default: '',
},
},
template: `
<a class="btn terminal-button"
:href="terminalPath">
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a>
`,
});
})();
template: `
<a class="btn terminal-button"
:href="terminalPath">
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a>
`,
});

View File

@ -0,0 +1,74 @@
/**
* Render environments table.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
},
props: {
environments: {
type: Array,
required: true,
default: () => ([]),
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
template: `
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
</template>
</tbody>
</table>
`,
});

View File

@ -1,6 +1,4 @@
window.Vue = require('vue');
require('./stores/environments_store');
require('./components/environment');
const EnvironmentsComponent = require('./components/environment');
require('../vue_shared/vue_resource_interceptor');
$(() => {
@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true);
}
const Store = gl.environmentsList.EnvironmentsStore;
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({
gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
});
});

View File

@ -0,0 +1,14 @@
const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListFolderApp) {
gl.EnvironmentsListFolderApp.$destroy(true);
}
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});

View File

@ -0,0 +1,181 @@
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
const Vue = require('vue');
Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
module.exports = Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
},
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
const pathname = window.location.pathname;
const endpoint = `${pathname}.json`;
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
return {
store,
folderName,
endpoint,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return gl.utils.getParameterByName('scope');
},
canReadEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* URL to link in the stopped tab.
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
},
/**
* URL to link in the available tab.
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area" v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div>
</div>
</div>
`,
});

View File

@ -1,20 +1,8 @@
/* globals Vue */
/* eslint-disable no-unused-vars, no-param-reassign */
const Vue = require('vue');
class EnvironmentsService {
constructor(root) {
Vue.http.options.root = root;
this.environments = Vue.resource(root);
Vue.http.interceptors.push((request, next) => {
// needed in order to not break the tests.
if ($.rails) {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
});
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
}
all() {
@ -22,4 +10,4 @@ class EnvironmentsService {
}
}
window.EnvironmentsService = EnvironmentsService;
module.exports = EnvironmentsService;

View File

@ -1,190 +1,90 @@
/* eslint-disable no-param-reassign */
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
require('~/lib/utils/common_utils');
/**
* Environments Store.
*
* Stores received environments, count of stopped environments and count of
* available environments.
*/
class EnvironmentsStore {
constructor() {
this.state = {};
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
this.state.paginationInformation = {};
gl.environmentsList.EnvironmentsStore = {
state: {},
return this;
}
create() {
this.state.environments = [];
this.state.stoppedCounter = 0;
this.state.availableCounter = 0;
this.state.visibility = 'available';
this.state.filteredEnvironments = [];
/**
*
* Stores the received environments.
*
* In the main environments endpoint, each environment has the following schema
* { name: String, size: Number, latest: Object }
* In the endpoint to retrieve environments from each folder, the environment does
* not have the `latest` key and the data is all in the root level.
* To avoid doing this check in the view, we store both cases the same by extracting
* what is inside the `latest` key.
*
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
*
* @param {Array} environments
* @returns {Array}
*/
storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => {
let filtered = {};
return this;
},
if (env.size > 1) {
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
}
/**
* In order to display a tree view we need to modify the received
* data in to a tree structure based on `environment_type`
* sorted alphabetically.
* In each children a `vue-` property will be added. This property will be
* used to know if an item is a children mostly for css purposes. This is
* needed because the children row is a fragment instance and therfore does
* not accept non-prop attributes.
*
*
* @example
* it will transform this:
* [
* { name: "environment", environment_type: "review" },
* { name: "environment_1", environment_type: null }
* { name: "environment_2, environment_type: "review" }
* ]
* into this:
* [
* { name: "review", children:
* [
* { name: "environment", environment_type: "review", vue-isChildren: true},
* { name: "environment_2", environment_type: "review", vue-isChildren: true}
* ]
* },
* {name: "environment_1", environment_type: null}
* ]
*
*
* @param {Array} environments List of environments.
* @returns {Array} Tree structured array with the received environments.
*/
storeEnvironments(environments = []) {
this.state.stoppedCounter = this.countByState(environments, 'stopped');
this.state.availableCounter = this.countByState(environments, 'available');
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
delete filtered.latest;
} else {
filtered = Object.assign(filtered, env);
}
const environmentsTree = environments.reduce((acc, environment) => {
if (environment.environment_type !== null) {
const occurs = acc.filter(element => element.children &&
element.name === environment.environment_type);
return filtered;
});
environment['vue-isChildren'] = true;
this.state.environments = filteredEnvironments;
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment);
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
children: [environment],
isOpen: false,
'vue-isChildren': environment['vue-isChildren'],
});
}
} else {
acc.push(environment);
}
return filteredEnvironments;
}
return acc;
}, []).slice().sort(this.sortByName);
setPagination(pagination = {}) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
this.state.environments = environmentsTree;
this.state.paginationInformation = paginationInformation;
return paginationInformation;
}
this.filterEnvironmentsByVisibility(this.state.environments);
/**
* Stores the number of available environments.
*
* @param {Number} count = 0
* @return {Number}
*/
storeAvailableCount(count = 0) {
this.state.availableCounter = count;
return count;
}
return environmentsTree;
},
/**
* Stores the number of closed environments.
*
* @param {Number} count = 0
* @return {Number}
*/
storeStoppedCount(count = 0) {
this.state.stoppedCounter = count;
return count;
}
}
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item);
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/**
* Toggles folder open property given the environment type.
*
* @param {String} envType
* @return {Array}
*/
toggleFolder(envType) {
const environments = this.state.environments;
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
}
return env;
});
this.state.environments = environmentsCopy;
return environmentsCopy;
},
/**
* Given an array of environments, returns the number of environments
* that have the given state.
*
* @param {Array} environments
* @param {String} state
* @returns {Number}
*/
countByState(environments, state) {
return environments.filter(env => env.state === state).length;
},
/**
* Sorts the two objects provided by their name.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
sortByName(a, b) {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
},
};
})();
module.exports = EnvironmentsStore;

View File

@ -103,6 +103,9 @@
this.input.each((i, input) => {
const $input = $(input);
$input.off('focus.setupAtWho').on('focus.setupAtWho', this.setupAtWho.bind(this, $input));
// This triggers at.js again
// Needed for slash commands with suffixes (ex: /label ~)
$input.on('inserted-commands.atwho', $input.trigger.bind($input, 'keyup'));
});
},
setupAtWho: function($input) {

View File

@ -47,9 +47,11 @@
}
// Only filter asynchronously only if option remote is set
if (this.options.remote) {
$inputContainer.parent().addClass('is-loading');
clearTimeout(timeout);
return timeout = setTimeout(function() {
return this.options.query(this.input.val(), function(data) {
$inputContainer.parent().removeClass('is-loading');
return this.options.callback(data);
}.bind(this));
}.bind(this), 250);

View File

@ -40,11 +40,12 @@
calendar = new Pikaday({
field: $issuableDueDate.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
onSelect: function(dateText) {
$issuableDueDate.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
}
});
calendar.setDate(new Date($issuableDueDate.val()));
}
}

View File

@ -54,16 +54,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) {
if ('id' in data) {
$(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text());
if (isClose) {
$('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1);
} else {
$('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
}
} else {
new Flash(issueFailMessage, 'alert');

View File

@ -231,6 +231,21 @@
return upperCaseHeaders;
};
/**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
* @returns {Object}
*/
w.gl.utils.parseIntPagination = paginationInformation => ({
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
page: parseInt(paginationInformation['X-PAGE'], 10),
total: parseInt(paginationInformation['X-TOTAL'], 10),
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
/**
* Transforms a DOMStringMap into a plain object.
*
@ -241,5 +256,45 @@
acc[element] = DOMStringMapObject[element];
return acc;
}, {});
/**
* Updates the search parameter of a URL given the parameter and values provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
* If there are params but not for the given one, we'll add it at the end.
* Returns the new search parameters.
*
* @param {String} param
* @param {Number|String|Undefined|Null} value
* @return {String}
*/
w.gl.utils.setParamInURL = (param, value) => {
let search;
const locationSearch = window.location.search;
if (locationSearch.length === 0) {
search = `?${param}=${value}`;
}
if (locationSearch.indexOf(param) !== -1) {
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
search = `${locationSearch}&${param}=${value}`;
}
return search;
};
/**
* Converts permission provided as strings to booleans.
*
* @param {String} string
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
})(window);
}).call(this);

View File

@ -19,7 +19,7 @@
const calendar = new Pikaday({
field: $input.get(0),
theme: 'gitlab-theme',
format: 'YYYY-MM-DD',
format: 'yyyy-mm-dd',
minDate: new Date(),
onSelect(dateText) {
$input.val(dateFormat(new Date(dateText), 'yyyy-mm-dd'));
@ -30,6 +30,7 @@
},
});
calendar.setDate(new Date($input.val()));
$input.data('pikaday', calendar);
});

View File

@ -61,6 +61,7 @@ require('./flash');
constructor({ action, setUrl, stubLocation } = {}) {
this.diffsLoaded = false;
this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
@ -102,9 +103,10 @@ require('./flash');
}
clickTab(e) {
if (e.target && gl.utils.isMetaClick(e)) {
const targetLink = e.target.getAttribute('href');
if (e.currentTarget && gl.utils.isMetaClick(e)) {
const targetLink = e.currentTarget.getAttribute('href');
e.stopImmediatePropagation();
e.preventDefault();
window.open(targetLink, '_blank');
}
}
@ -128,6 +130,13 @@ require('./flash');
$.scrollTo('.merge-request-details .merge-request-tabs', {
offset: 0,
});
} else if (action === 'pipelines') {
if (this.pipelinesLoaded) {
return;
}
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
gl.commits.pipelines.PipelinesTableBundle.$mount(pipelineTableViewEl);
this.pipelinesLoaded = true;
} else {
this.expandView();
this.resetViewContainer();

View File

@ -110,7 +110,7 @@ require('./smart_interval');
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) {
return _this.$widgetBody.html("<h4>" + data.merge_error + "</h4>");
return $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>");
} else {
callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch);

View File

@ -29,7 +29,7 @@
if (selected.id == null) {
return selected.text;
} else {
return selected.kind + ": " + selected.path;
return selected.kind + ": " + selected.full_path;
}
},
data: function(term, dataCallback) {
@ -50,7 +50,7 @@
if (namespace.id == null) {
return namespace.text;
} else {
return namespace.kind + ": " + namespace.path;
return namespace.kind + ": " + namespace.full_path;
}
},
renderRow: this.renderRow,

View File

@ -923,9 +923,10 @@ require('vendor/task_list');
};
Notes.prototype.toggleCommitList = function(e) {
const $element = $(e.target);
const $element = $(e.currentTarget);
const $closestSystemCommitList = $element.siblings('.system-note-commit-list');
$element.find('.fa').toggleClass('fa-angle-down').toggleClass('fa-angle-up');
$closestSystemCommitList.toggleClass('hide-shade');
};

View File

@ -25,6 +25,7 @@
bindEvents() {
$('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
$('#user_notification_email').on('change', this.submitForm);
$('#user_notified_of_own_activity').on('change', this.submitForm);
$('.update-username').on('ajax:before', this.beforeUpdateUsername);
$('.update-username').on('ajax:complete', this.afterUpdateUsername);
$('.update-notifications').on('ajax:success', this.onUpdateNotifs);

View File

@ -38,13 +38,15 @@
this.$buttons.attr('data-status', newStatus);
this.$buttons.find('> span').text(newAction);
for (const button of this.$buttons) {
this.$buttons.map((button) => {
const $button = $(button);
if ($button.attr('data-original-title')) {
$button.tooltip('hide').attr('data-original-title', newAction).tooltip('fixTitle');
}
}
return button;
});
});
}
}

View File

@ -21,11 +21,16 @@
};
Sidebar.prototype.addEventListeners = function() {
const $document = $(document);
const throttledSetSidebarHeight = _.throttle(this.setSidebarHeight, 10);
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
$(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
$(window).on('resize', () => throttledSetSidebarHeight());
$document.on('scroll', () => throttledSetSidebarHeight());
$document.on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
@ -191,6 +196,17 @@
}
};
Sidebar.prototype.setSidebarHeight = function() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const $rightSidebar = $('.js-right-sidebar');
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$rightSidebar.outerHeight($(window).height() - diff);
} else {
$rightSidebar.outerHeight('100%');
}
};
Sidebar.prototype.isOpen = function() {
return this.sidebar.is('.right-sidebar-expanded');
};

View File

@ -1,111 +0,0 @@
/* eslint-disable arrow-parens, class-methods-use-this, no-param-reassign */
/* global Cookies */
(() => {
const pinnedStateCookie = 'pin_nav';
const sidebarBreakpoint = 1024;
const pageSelector = '.page-with-sidebar';
const navbarSelector = '.navbar-gitlab';
const sidebarWrapperSelector = '.sidebar-wrapper';
const sidebarContentSelector = '.nav-sidebar';
const pinnedToggleSelector = '.js-nav-pin';
const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
const pinnedPageClass = 'page-sidebar-pinned';
const expandedPageClass = 'page-sidebar-expanded';
const pinnedNavbarClass = 'header-sidebar-pinned';
const expandedNavbarClass = 'header-sidebar-expanded';
class Sidebar {
constructor() {
if (!Sidebar.singleton) {
Sidebar.singleton = this;
Sidebar.singleton.init();
}
return Sidebar.singleton;
}
init() {
this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
this.isExpanded = (
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
);
$(window).on('resize', () => this.setSidebarHeight());
$(document)
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body, a, button', (e) => this.handleClickEvent(e))
.on('DOMContentLoaded', () => this.renderState())
.on('scroll', () => this.setSidebarHeight())
.on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
this.setSidebarHeight();
}
handleClickEvent(e) {
if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
const $target = $(e.target);
const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
this.toggleSidebar();
}
}
}
updateTodoCount(count) {
$('.js-todos-count').text(gl.text.addDelimiter(count));
}
toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.renderState();
}
setSidebarHeight() {
const $navHeight = $('.navbar-gitlab').outerHeight() + $('.layout-nav').outerHeight();
const diff = $navHeight - $('body').scrollTop();
if (diff > 0) {
$('.js-right-sidebar').outerHeight($(window).height() - diff);
} else {
$('.js-right-sidebar').outerHeight('100%');
}
}
togglePinnedState() {
this.isPinned = !this.isPinned;
if (!this.isPinned) {
this.isExpanded = false;
}
Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
this.renderState();
}
renderState() {
$(pageSelector)
.toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
.toggleClass(expandedPageClass, this.isExpanded);
$(navbarSelector)
.toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
.toggleClass(expandedNavbarClass, this.isExpanded);
const $pinnedToggle = $(pinnedToggleSelector);
const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
$pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
if (this.isExpanded) {
const sidebarContent = $(sidebarContentSelector);
setTimeout(() => { sidebarContent.niceScroll().updateScrollBar(); }, 200);
}
}
}
window.gl = window.gl || {};
gl.Sidebar = Sidebar;
})();

View File

@ -147,24 +147,21 @@
goToTodoUrl(e) {
const todoLink = this.dataset.url;
let targetLink = e.target.getAttribute('href');
if (e.target.tagName === 'IMG') { // See if clicked target was Avatar
targetLink = e.target.parentElement.getAttribute('href'); // Parent of Avatar is link
}
if (!todoLink) {
return;
}
if (gl.utils.isMetaClick(e)) {
const windowTarget = '_blank';
const selected = e.target;
e.preventDefault();
// Meta-Click on username leads to different URL than todoLink.
// Turbolinks can resolve that URL, but window.open requires URL manually.
if (targetLink !== todoLink) {
return window.open(targetLink, '_blank');
if (selected.tagName === 'IMG') {
const avatarUrl = selected.parentElement.getAttribute('href');
return window.open(avatarUrl, windowTarget);
} else {
return window.open(todoLink, '_blank');
return window.open(todoLink, windowTarget);
}
} else {
return gl.utils.visitUrl(todoLink);

View File

@ -62,6 +62,7 @@
<li v-for='artifact in pipeline.details.artifacts'>
<a
rel="nofollow"
download
:href='artifact.path'
>
<i class="fa fa-download" aria-hidden="true"></i>

View File

@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination');
require('./store');
require('../vue_shared/components/pipelines_table');
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => {
gl.VuePipelines = Vue.extend({
@ -28,15 +29,34 @@ require('../vue_shared/components/pipelines_table');
},
props: ['scope', 'store', 'svgs'],
created() {
const pagenum = gl.utils.getParameterByName('p');
const pagenum = gl.utils.getParameterByName('page');
const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
},
beforeUpdate() {
if (this.pipelines.length && this.$children) {
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
}
},
methods: {
/**
* Changes the URL according to the pagination component.
*
* If no scope is provided, 'all' is assumed.
*
* Pagination component sends "null" when no scope is provided.
*
* @param {Number} pagenum
* @param {String} apiScope = 'all'
*/
change(pagenum, apiScope) {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
if (!apiScope) apiScope = 'all';
gl.utils.visitUrl(`?scope=${apiScope}&page=${pagenum}`);
},
},
template: `

View File

@ -1,68 +1,31 @@
/* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */
require('../vue_realtime_listener');
/* eslint-disable no-param-reassign */
((gl) => {
const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = {
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
const paginationInfo = gl.utils.parseIntPagination(normalized);
return paginationInfo;
};
gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () =>
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
goFetch();
const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => {
const timeAgoComponent = component.$children.filter(el => el.$options._componentTag === 'time-ago')[0];
acc.push(timeAgoComponent);
return acc;
}, []).forEach(e => e.changeTime());
}, 10000);
};
startTimeLoops();
const removeIntervals = () => clearInterval(this.timeLoopInterval);
const startIntervals = () => startTimeLoops();
gl.VueRealtimeListener(removeIntervals, startIntervals);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('An error occurred while fetching the pipelines, please reload the page again.');
});
}
};
})(window.gl || (window.gl = {}));

View File

@ -1,4 +1,5 @@
/* global Vue */
window.Vue = require('vue');
(() => {
window.gl = window.gl || {};

View File

@ -57,9 +57,7 @@ window.Vue = require('vue');
},
methods: {
changePage(e) {
let apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const apiScope = gl.utils.getParameterByName('scope');
const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo;

View File

@ -19,7 +19,6 @@
@import "framework/flash.scss";
@import "framework/forms.scss";
@import "framework/gfm.scss";
@import "framework/gitlab-theme.scss";
@import "framework/header.scss";
@import "framework/highlight.scss";
@import "framework/issue_box.scss";

View File

@ -116,7 +116,7 @@
}
.btn,
.side-nav-toggle {
.global-dropdown-toggle {
@include transition(background-color, border-color, color, box-shadow);
}
@ -128,8 +128,7 @@
.note-action-button .link-highlight,
.toolbar-btn,
.dropdown-toggle-caret,
.fa:not(.fa-bell) {
.dropdown-toggle-caret {
@include transition(color);
}
@ -141,7 +140,6 @@ a {
@include transition(background-color, box-shadow);
}
.nav-sidebar a,
.dropdown-menu a,
.dropdown-menu button,
.dropdown-menu-nav a {

View File

@ -28,6 +28,8 @@
.avatar {
@extend .avatar-circle;
@include transition-property(none);
width: 40px;
height: 40px;
padding: 0;

View File

@ -1,144 +0,0 @@
/**
* Styles the GitLab application with a specific color theme
*
* $color-light -
* $color -
* $color-darker -
* $color-dark -
*/
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.toggle-nav-collapse,
.pin-nav-btn {
color: $color-light;
&:hover {
color: $white-light;
}
}
.sidebar-wrapper {
background: $color-darker;
}
.sidebar-action-buttons {
color: $color-light;
background-color: lighten($color-darker, 5%);
}
.nav-sidebar {
li {
a {
color: $color-light;
&:hover,
&:focus,
&:active {
background: $color-dark;
}
i {
color: $color-light;
}
path,
polygon {
fill: $color-light;
}
.count {
color: $color-light;
background: $color-dark;
}
svg {
position: relative;
top: 3px;
}
}
&.separate-item {
border-top: 1px solid $color;
}
&.active a {
color: $white-light;
background: $color-dark;
&.no-highlight {
border: none;
}
i {
color: $white-light;
}
path,
polygon {
fill: $white-light;
}
}
}
.about-gitlab {
color: $color-light;
}
}
}
}
$theme-charcoal-light: #b9bbbe;
$theme-charcoal: #485157;
$theme-charcoal-dark: #3d454d;
$theme-charcoal-darker: #383f45;
$theme-blue-light: #becde9;
$theme-blue: #2980b9;
$theme-blue-dark: #1970a9;
$theme-blue-darker: #096099;
$theme-graphite-light: #ccc;
$theme-graphite: #777;
$theme-graphite-dark: #666;
$theme-graphite-darker: #555;
$theme-black-light: #979797;
$theme-black: #373737;
$theme-black-dark: #272727;
$theme-black-darker: #222;
$theme-green-light: #adc;
$theme-green: #019875;
$theme-green-dark: #018865;
$theme-green-darker: #017855;
$theme-violet-light: #98c;
$theme-violet: #548;
$theme-violet-dark: #436;
$theme-violet-darker: #325;
body {
&.ui_blue {
@include gitlab-theme($theme-blue-light, $theme-blue, $theme-blue-dark, $theme-blue-darker);
}
&.ui_charcoal {
@include gitlab-theme($theme-charcoal-light, $theme-charcoal, $theme-charcoal-dark, $theme-charcoal-darker);
}
&.ui_graphite {
@include gitlab-theme($theme-graphite-light, $theme-graphite, $theme-graphite-dark, $theme-graphite-darker);
}
&.ui_black {
@include gitlab-theme($theme-black-light, $theme-black, $theme-black-dark, $theme-black-darker);
}
&.ui_green {
@include gitlab-theme($theme-green-light, $theme-green, $theme-green-dark, $theme-green-darker);
}
&.ui_violet {
@include gitlab-theme($theme-violet-light, $theme-violet, $theme-violet-dark, $theme-violet-darker);
}
}

View File

@ -100,23 +100,42 @@ header {
}
}
}
}
.side-nav-toggle {
position: absolute;
left: -10px;
margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $gray-light;
.global-dropdown {
position: absolute;
left: -10px;
.badge {
font-size: 11px;
}
li {
.active a {
font-weight: bold;
}
&:hover {
background-color: $white-normal;
color: $gl-header-nav-hover-color;
.badge {
background-color: $white-light;
}
}
}
}
.global-dropdown-toggle {
margin: 7px 0;
font-size: 18px;
padding: 6px 10px;
border: none;
background-color: $gray-light;
&:hover {
background-color: $white-normal;
color: $gl-header-nav-hover-color;
}
}
.header-content {
position: relative;
height: $header-height;

View File

@ -1,36 +1,3 @@
.page-with-sidebar {
padding-bottom: 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
.sidebar-wrapper {
box-shadow: none;
}
}
.sidebar-wrapper {
position: fixed;
top: 0;
bottom: 0;
left: 0;
height: 100%;
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
box-shadow: 2px 0 16px 0 $black-transparent;
}
}
.sidebar-wrapper {
z-index: 1000;
background: $gray-light;
.nicescroll-rails-hr {
// TODO: Figure out why nicescroll doesn't hide horizontal bar
display: none!important;
}
}
.content-wrapper {
width: 100%;
transition: padding $sidebar-transition-duration;
@ -47,105 +14,6 @@
}
}
.nav-sidebar {
position: absolute;
top: 50px;
bottom: 0;
width: $sidebar_width;
overflow-y: auto;
overflow-x: hidden;
&.navbar-collapse {
padding: 0 !important;
}
li {
&.separate-item {
padding-top: 10px;
margin-top: 10px;
}
.icon-container {
width: 34px;
display: inline-block;
text-align: center;
}
a {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
&:hover,
&:active,
&:focus {
text-decoration: none;
}
i {
font-size: 16px;
}
i,
svg {
margin-right: 13px;
}
}
}
.count {
float: right;
padding: 0 8px;
border-radius: 6px;
}
.about-gitlab {
padding: 7px $gl-sidebar-padding;
font-size: $gl-font-size;
line-height: 24px;
display: block;
text-decoration: none;
font-weight: normal;
position: absolute;
bottom: 10px;
}
}
.sidebar-action-buttons {
width: $sidebar_width;
position: absolute;
top: 0;
left: 0;
min-height: 50px;
padding: 5px 0;
font-size: 18px;
line-height: 30px;
.toggle-nav-collapse {
left: 0;
}
.pin-nav-btn {
right: 0;
display: none;
@media (min-width: $sidebar-breakpoint) {
display: block;
}
.fa {
transition: transform .15s;
.page-sidebar-pinned & {
transform: rotate(90deg);
}
}
}
}
.nav-header-btn {
padding: 10px $gl-sidebar-padding;
color: inherit;
@ -161,59 +29,16 @@
}
}
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
}
}
.page-sidebar-pinned {
.content-wrapper,
.layout-nav {
@media (min-width: $sidebar-breakpoint) {
padding-left: $sidebar_width;
}
}
.merge-request-tabs-holder.affix {
@media (min-width: $sidebar-breakpoint) {
left: $sidebar_width;
}
}
&.right-sidebar-expanded {
.line-resolve-all-container {
@media (min-width: $sidebar-breakpoint) {
display: none;
}
}
}
}
header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
.side-nav-toggle {
display: none;
}
.header-content {
padding-left: 0;
}
}
}
.right-sidebar-collapsed {
padding-right: 0;
@media (min-width: $screen-sm-min) {
.content-wrapper {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
.merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
right: $gutter_collapsed_width;
}
}
@ -231,7 +56,7 @@ header.header-sidebar-pinned {
@media (min-width: $screen-sm-min) and (max-width: $screen-sm-max) {
&:not(.build-sidebar):not(.wiki-sidebar) {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
}
@ -245,12 +70,12 @@ header.header-sidebar-pinned {
}
&.with-overlay .merge-request-tabs-holder.affix {
right: $sidebar_collapsed_width;
right: $gutter_collapsed_width;
}
}
&.with-overlay {
padding-right: $sidebar_collapsed_width;
padding-right: $gutter_collapsed_width;
}
}

View File

@ -1,8 +1,6 @@
/*
* Layout
*/
$sidebar_collapsed_width: 62px;
$sidebar_width: 220px;
$gutter_collapsed_width: 62px;
$gutter_width: 290px;
$gutter_inner_width: 250px;
@ -541,4 +539,4 @@ Pipeline Graph
*/
$stage-hover-bg: #eaf3fc;
$stage-hover-border: #d1e7fc;
$action-icon-color: #d6d6d6;
$action-icon-color: #d6d6d6;

View File

@ -91,7 +91,7 @@
}
&.scroll-top {
top: 110px;
top: 10px;
}
&.scroll-bottom {

View File

@ -284,7 +284,11 @@
.events-description {
line-height: 65px;
padding-left: $gl-padding;
padding: 0 $gl-padding;
}
.events-info {
color: $gl-text-color-secondary;
}
}

View File

@ -10,6 +10,11 @@
font-size: 34px;
}
.environments-folder-name {
font-weight: normal;
padding-top: 20px;
}
@media (max-width: $screen-xs-max) {
.environments-container {
width: 100%;
@ -110,17 +115,20 @@
}
}
.children-row .environment-name {
margin-left: 17px;
margin-right: -17px;
}
.folder-icon {
padding: 0 5px 0 0;
margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
.fa:nth-child(1) {
margin-right: 3px;
}
}
.folder-name {
cursor: pointer;
color: $gl-text-color-secondary;
display: inline-block;
}
}
@ -135,4 +143,4 @@
margin-right: 0;
}
}
}
}

View File

@ -41,7 +41,6 @@
word-wrap: break-word;
.md {
color: $gl-grayish-blue;
font-size: $gl-font-size;
.label {

View File

@ -193,7 +193,6 @@
top: $header-height;
bottom: 0;
right: 0;
z-index: 8;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@ -254,11 +253,11 @@
display: block;
}
width: $sidebar_collapsed_width;
width: $gutter_collapsed_width;
padding-top: 0;
.block {
width: $sidebar_collapsed_width - 2px;
width: $gutter_collapsed_width - 2px;
margin-left: -19px;
padding: 15px 0 0;
border-bottom: none;

View File

@ -85,14 +85,18 @@
-webkit-align-items: center;
align-items: center;
i,
svg {
margin-right: 8px;
}
svg {
margin-right: 4px;
position: relative;
top: 1px;
overflow: visible;
}
&> span {
& > span {
padding-right: 4px;
}

View File

@ -72,6 +72,7 @@ ul.notes {
overflow: hidden;
.system-note-commit-list-toggler {
color: $gl-link-color;
display: none;
padding: 10px 0 0;
cursor: pointer;
@ -107,16 +108,6 @@ ul.notes {
display: none;
}
p:last-child {
a {
color: $gl-text-color;
&:hover {
color: $gl-link-color;
}
}
}
&::after {
content: '';
width: 100%;

View File

@ -864,7 +864,7 @@
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
width: 90px;
max-width: 70%;
color: $gl-text-color-secondary;
margin-left: 2px;
display: inline-block;

View File

@ -1,42 +1,3 @@
.application-theme {
label {
margin-right: 20px;
text-align: center;
.preview {
border-radius: 4px;
height: 80px;
margin-bottom: 10px;
width: 160px;
&.ui_blue {
background: $theme-blue;
}
&.ui_charcoal {
background: $theme-charcoal;
}
&.ui_graphite {
background: $theme-graphite;
}
&.ui_black {
background: $theme-black;
}
&.ui_green {
background: $theme-green;
}
&.ui_violet {
background: $theme-violet;
}
}
}
}
.syntax-theme {
label {
margin-right: 20px;

View File

@ -35,12 +35,8 @@
margin-bottom: 10px;
}
.project-path {
padding-right: 0;
.form-control {
border-radius: $border-radius-base;
}
.project-path .form-control {
border-radius: $border-radius-base;
}
.input-group > div {
@ -106,6 +102,7 @@
font-size: 24px;
font-weight: 400;
line-height: 1;
word-wrap: break-word;
.fa {
margin-left: 2px;

View File

@ -171,6 +171,8 @@
.tree-controls {
float: right;
margin-top: 11px;
position: relative;
z-index: 2;
.project-action-button {
margin-left: $btn-side-margin;

View File

@ -1,3 +1,11 @@
.new-wiki-page {
.new-wiki-page-slug-tip {
display: inline-block;
max-width: 100%;
margin-top: 5px;
}
}
.title .edit-wiki-header {
width: 780px;
margin-left: auto;
@ -9,12 +17,18 @@
@extend .top-area;
position: relative;
.wiki-breadcrumb {
border-bottom: 1px solid $white-normal;
padding: 11px 0;
}
.wiki-page-title {
margin: 0;
font-size: 22px;
}
.wiki-last-edit-by {
display: block;
color: $gl-text-color-secondary;
strong {
@ -121,6 +135,10 @@
margin: 5px 0 10px;
}
ul.wiki-pages ul {
padding-left: 15px;
}
.wiki-sidebar-header {
padding: 0 $gl-padding $gl-padding;
@ -129,3 +147,15 @@
}
}
}
ul.wiki-pages-list.content-list {
& ul {
list-style: none;
margin-left: 0;
padding-left: 15px;
}
& ul li {
padding: 5px 0;
}
}

View File

@ -31,7 +31,6 @@ nav.navbar-collapse.collapse,
.blob-commit-info,
.file-title,
.file-holder,
.sidebar-wrapper,
.nav,
.btn,
ul.notes-form,

View File

@ -13,7 +13,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def update
if @runner.update_attributes(runner_params)
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
respond_to do |format|
format.js
format.html { redirect_to admin_runner_path(@runner) }
@ -31,7 +31,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def resume
if @runner.update_attributes(active: true)
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'
@ -39,7 +39,7 @@ class Admin::RunnersController < Admin::ApplicationController
end
def pause
if @runner.update_attributes(active: false)
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to admin_runners_path, notice: 'Runner was successfully updated.'
else
redirect_to admin_runners_path, alert: 'Runner was not updated.'

View File

@ -194,7 +194,6 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
:theme_id,
:twitter,
:username,
:website_url

View File

@ -9,6 +9,28 @@ module IssuableCollections
private
def issuable_meta_data(issuable_collection)
# map has to be used here since using pluck or select will
# throw an error when ordering issuables by priority which inserts
# a new order into the collection.
# We cannot use reorder to not mess up the paginated collection.
issuable_ids = issuable_collection.map(&:id)
issuable_note_count = Note.count_for_collection(issuable_ids, @collection_type)
issuable_votes_count = AwardEmoji.votes_for_collection(issuable_ids, @collection_type)
issuable_ids.each_with_object({}) do |id, issuable_meta|
downvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.downvote? }
upvotes = issuable_votes_count.find { |votes| votes.awardable_id == id && votes.upvote? }
notes = issuable_note_count.find { |notes| notes.noteable_id == id }
issuable_meta[id] = Issuable::IssuableMeta.new(
upvotes.try(:count).to_i,
downvotes.try(:count).to_i,
notes.try(:count).to_i
)
end
end
def issues_collection
issues_finder.execute.preload(:project, :author, :assignee, :labels, :milestone, project: :namespace)
end

View File

@ -9,6 +9,9 @@ module IssuesAction
.non_archived
.page(params[:page])
@collection_type = "Issue"
@issuable_meta_data = issuable_meta_data(@issues)
respond_to do |format|
format.html
format.atom { render layout: false }

View File

@ -7,6 +7,9 @@ module MergeRequestsAction
@merge_requests = merge_requests_collection
.page(params[:page])
@collection_type = "MergeRequest"
@issuable_meta_data = issuable_meta_data(@merge_requests)
end
private

View File

@ -1,19 +1,14 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
include FilterProjects
before_action :event_filter
def index
@projects = current_user.authorized_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace)
@projects = load_projects(current_user.authorized_projects)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
respond_to do |format|
format.html { @last_push = current_user.recent_push }
format.atom do
event_filter
load_events
render layout: false
end
@ -26,9 +21,8 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
end
def starred
@projects = current_user.viewable_starred_projects.sorted_by_activity
@projects = filter_projects(@projects)
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = load_projects(current_user.viewable_starred_projects)
@projects = @projects.includes(:forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
@projects = @projects.page(params[:page])
@ -37,7 +31,6 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
respond_to do |format|
format.html
format.json do
render json: {
html: view_to_html_string("dashboard/projects/_projects", locals: { projects: @projects })
@ -48,9 +41,15 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
private
def load_projects(base_scope)
projects = base_scope.sorted_by_activity.includes(:namespace)
filter_projects(projects)
end
def load_events
@events = Event.in_projects(@projects)
@events = @event_filter.apply_filter(@events).with_associations
@events = Event.in_projects(load_projects(current_user.authorized_projects))
@events = event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
end

View File

@ -17,6 +17,6 @@ class Profiles::NotificationsController < Profiles::ApplicationController
end
def user_params
params.require(:user).permit(:notification_email)
params.require(:user).permit(:notification_email, :notified_of_own_activity)
end
end

View File

@ -34,7 +34,6 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:layout,
:dashboard,
:project_view,
:theme_id
)
end
end

View File

@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize
def index
@scope = params[:scope]
@environments = project.environments.includes(:last_deployment)
@environments = project.environments
.with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
render json: EnvironmentSerializer
.new(project: @project, user: current_user)
.represent(@environments)
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.within_folders
.represent(@environments),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
end
end
end
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@environments),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
end
end
end

View File

@ -23,8 +23,11 @@ class Projects::IssuesController < Projects::ApplicationController
respond_to :html
def index
@issues = issues_collection
@issues = @issues.page(params[:page])
@collection_type = "Issue"
@issues = issues_collection
@issues = @issues.page(params[:page])
@issuable_meta_data = issuable_meta_data(@issues)
if @issues.out_of_range? && @issues.total_pages != 0
return redirect_to url_for(params.merge(page: @issues.total_pages))
end

View File

@ -36,8 +36,11 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :authorize_can_resolve_conflicts!, only: [:conflicts, :conflict_for_path, :resolve_conflicts]
def index
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@collection_type = "MergeRequest"
@merge_requests = merge_requests_collection
@merge_requests = @merge_requests.page(params[:page])
@issuable_meta_data = issuable_meta_data(@merge_requests)
if @merge_requests.out_of_range? && @merge_requests.total_pages != 0
return redirect_to url_for(params.merge(page: @merge_requests.total_pages))
end
@ -366,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged'
@status = :success
elsif merge_request.merge_when_build_succeeds
if merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds
else
# Only MRs that can be merged end in this action
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
# in last case it does not have any special status. Possible error is handled inside widget js function
@status = :success
end
render 'merge'

View File

@ -12,7 +12,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def update
if @runner.update_attributes(runner_params)
if Ci::UpdateRunnerService.new(@runner).update(runner_params)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
render 'edit'
@ -28,7 +28,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def resume
if @runner.update_attributes(active: true)
if Ci::UpdateRunnerService.new(@runner).update(active: true)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'
@ -36,7 +36,7 @@ class Projects::RunnersController < Projects::ApplicationController
end
def pause
if @runner.update_attributes(active: false)
if Ci::UpdateRunnerService.new(@runner).update(active: false)
redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
else
redirect_to runner_path(@runner), alert: 'Runner was not updated.'

View File

@ -27,7 +27,7 @@ class Projects::TagsController < Projects::ApplicationController
end
def create
result = CreateTagService.new(@project, current_user).
result = Tags::CreateService.new(@project, current_user).
execute(params[:tag_name], params[:ref], params[:message], params[:release_description])
if result[:status] == :success
@ -41,7 +41,7 @@ class Projects::TagsController < Projects::ApplicationController
end
def destroy
DeleteTagService.new(project, current_user).execute(params[:id])
Tags::DestroyService.new(project, current_user).execute(params[:id])
respond_to do |format|
format.html do

View File

@ -8,6 +8,7 @@ class Projects::WikisController < Projects::ApplicationController
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page])
@wiki_entries = WikiPage.group_by_directory(@wiki_pages)
end
def show
@ -83,7 +84,7 @@ class Projects::WikisController < Projects::ApplicationController
def destroy
@page = @project_wiki.find_page(params[:id])
@page.delete if @page
WikiPages::DestroyService.new(@project, current_user).execute(@page)
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, :home),
@ -116,7 +117,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
@sidebar_wiki_pages = @project_wiki.pages.first(15)
@sidebar_wiki_entries = WikiPage.group_by_directory(@project_wiki.pages.first(15))
rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)

View File

@ -296,4 +296,13 @@ module ApplicationHelper
def page_class
"issue-boards-page" if current_controller?(:boards)
end
# Returns active css class when condition returns true
# otherwise returns nil.
#
# Example:
# %li{ class: active_when(params[:filter] == '1') }
def active_when(condition)
'active' if condition
end
end

View File

@ -1,7 +1,7 @@
module BuildsHelper
def sidebar_build_class(build, current_build)
build_class = ''
build_class += ' active' if build == current_build
build_class += ' active' if build.id === current_build.id
build_class += ' retried' if build.retried?
build_class
end

View File

@ -75,10 +75,10 @@ module MergeRequestsHelper
new_namespace_project_merge_request_path(
@project.namespace, @project,
merge_request: {
source_project_id: @merge_request.source_project_id,
target_project_id: @merge_request.target_project_id,
source_branch: @merge_request.source_branch,
target_branch: @merge_request.target_branch,
source_project_id: merge_request.source_project_id,
target_project_id: merge_request.target_project_id,
source_branch: merge_request.source_branch,
target_branch: merge_request.target_branch,
},
change_branches: true
)

View File

@ -10,7 +10,7 @@ module NamespacesHelper
data_attr_users = { 'data-options-parent' => 'users' }
group_opts = [
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.path : g.human_name, g.id, data_attr_group] }
"Groups", groups.sort_by(&:human_name).map { |g| [display_path ? g.full_path : g.human_name, g.id, data_attr_group] }
]
users_opts = [

View File

@ -1,10 +1,4 @@
module NavHelper
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
end
end
def page_gutter_class
if current_path?('merge_requests#show') ||
current_path?('merge_requests#diffs') ||
@ -32,10 +26,6 @@ module NavHelper
class_name = ''
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name
end
@ -46,8 +36,4 @@ module NavHelper
def nav_control_class
"nav-control" if current_user
end
def pinned_nav?
cookies[:pin_nav] == 'true'
end
end

View File

@ -34,6 +34,10 @@ module PageLayoutHelper
end
end
def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
end
def page_image
default = image_url('gitlab_logo.png')

View File

@ -23,7 +23,7 @@ module PreferencesHelper
if defined.size != DASHBOARD_CHOICES.size
# Ensure that anyone adding new options updates this method too
raise RuntimeError, "`User` defines #{defined.size} dashboard choices," +
raise "`User` defines #{defined.size} dashboard choices," \
" but `DASHBOARD_CHOICES` defined #{DASHBOARD_CHOICES.size}."
else
defined.map do |key, _|
@ -41,10 +41,6 @@ module PreferencesHelper
]
end
def user_application_theme
Gitlab::Themes.for_user(current_user).css_class
end
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end

View File

@ -63,7 +63,7 @@ module SubmoduleHelper
namespace = components.pop.gsub(/^\.\.$/, '')
if namespace.empty?
namespace = @project.namespace.path
namespace = @project.namespace.full_path
end
[

View File

@ -15,6 +15,7 @@ module TodosHelper
when Todo::MARKED then 'added a todo for'
when Todo::APPROVAL_REQUIRED then 'set you as an approver for'
when Todo::UNMERGEABLE then 'Could not merge'
when Todo::DIRECTLY_ADDRESSED then 'directly addressed you on'
end
end
@ -88,7 +89,8 @@ module TodosHelper
{ id: Todo::ASSIGNED, text: 'Assigned' },
{ id: Todo::MENTIONED, text: 'Mentioned' },
{ id: Todo::MARKED, text: 'Added' },
{ id: Todo::BUILD_FAILED, text: 'Pipelines' }
{ id: Todo::BUILD_FAILED, text: 'Pipelines' },
{ id: Todo::DIRECTLY_ADDRESSED, text: 'Directly addressed' }
]
end

View File

@ -0,0 +1,13 @@
module WikiHelper
# Produces a pure text breadcrumb for a given page.
#
# page_slug - The slug of a WikiPage object.
#
# Returns a String composed of the capitalized name of each directory and the
# capitalized name of the page itself.
def breadcrumb(page_slug)
page_slug.split('/').
map { |dir_or_page| WikiPage.unhyphenize(dir_or_page).capitalize }.
join(' / ')
end
end

View File

@ -151,7 +151,7 @@ class Notify < BaseMailer
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
headers[:subject].prepend('Re: ') if headers[:subject]
headers[:subject]&.prepend('Re: ')
mail_thread(model, headers)
end

View File

@ -116,31 +116,25 @@ class ApplicationSetting < ActiveRecord::Base
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates_each :restricted_visibility_levels do |record, attr, value|
unless value.nil?
value.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
value&.each do |level|
unless Gitlab::VisibilityLevel.options.has_value?(level)
record.errors.add(attr, "'#{level}' is not a valid visibility level")
end
end
end
validates_each :import_sources do |record, attr, value|
unless value.nil?
value.each do |source|
unless Gitlab::ImportSources.options.has_value?(source)
record.errors.add(attr, "'#{source}' is not a import source")
end
value&.each do |source|
unless Gitlab::ImportSources.options.has_value?(source)
record.errors.add(attr, "'#{source}' is not a import source")
end
end
end
validates_each :disabled_oauth_sign_in_sources do |record, attr, value|
unless value.nil?
value.each do |source|
unless Devise.omniauth_providers.include?(source.to_sym)
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
value&.each do |source|
unless Devise.omniauth_providers.include?(source.to_sym)
record.errors.add(attr, "'#{source}' is not an OAuth sign-in source")
end
end
end
@ -230,11 +224,11 @@ class ApplicationSetting < ActiveRecord::Base
end
def domain_whitelist_raw
self.domain_whitelist.join("\n") unless self.domain_whitelist.nil?
self.domain_whitelist&.join("\n")
end
def domain_blacklist_raw
self.domain_blacklist.join("\n") unless self.domain_blacklist.nil?
self.domain_blacklist&.join("\n")
end
def domain_whitelist_raw=(values)

View File

@ -16,6 +16,14 @@ class AwardEmoji < ActiveRecord::Base
scope :downvotes, -> { where(name: DOWNVOTE_NAME) }
scope :upvotes, -> { where(name: UPVOTE_NAME) }
class << self
def votes_for_collection(ids, type)
select('name', 'awardable_id', 'COUNT(*) as count').
where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids).
group('name', 'awardable_id')
end
end
def downvote?
self.name == DOWNVOTE_NAME
end

View File

@ -22,8 +22,6 @@ module Ci
scope :online, ->() { where('contacted_at > ?', LAST_CONTACT_TIME) }
scope :ordered, ->() { order(id: :desc) }
after_save :tick_runner_queue, if: :form_editable_changed?
scope :owned_or_shared, ->(project_id) do
joins('LEFT JOIN ci_runner_projects ON ci_runner_projects.runner_id = ci_runners.id')
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
@ -40,6 +38,8 @@ module Ci
acts_as_taggable
after_destroy :cleanup_runner_queue
# Searches for runners matching the given query.
#
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@ -147,14 +147,14 @@ module Ci
private
def runner_queue_key
"runner:build_queue:#{self.token}"
def cleanup_runner_queue
Gitlab::Redis.with do |redis|
redis.del(runner_queue_key)
end
end
def form_editable_changed?
FORM_EDITABLE.any? do |editable|
public_send("#{editable}_changed?")
end
def runner_queue_key
"runner:build_queue:#{self.token}"
end
def tag_constraints

View File

@ -15,6 +15,11 @@ module Issuable
include Taskable
include TimeTrackable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes and notes count for issues and merge requests
# lists avoiding n+1 queries and improving performance.
IssuableMeta = Struct.new(:upvotes, :downvotes, :notes_count)
included do
cache_markdown_field :title, pipeline: :single_line
cache_markdown_field :description
@ -95,8 +100,8 @@ module Issuable
def update_assignee_cache_counts
# make sure we flush the cache for both the old *and* new assignees(if they exist)
previous_assignee = User.find_by_id(assignee_id_was) if assignee_id_was
previous_assignee.update_cache_counts if previous_assignee
assignee.update_cache_counts if assignee
previous_assignee&.update_cache_counts
assignee&.update_cache_counts
end
# We want to use optimistic lock for cases when only title or description are involved

Some files were not shown because too many files have changed in this diff Show More