Commit 744580fd authored by Mike Greiling's avatar Mike Greiling

Merge branch 'master' into add-yarn-documentation

* master: (73 commits)
  fix typo in node section
  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
  add space between ci text and commit sha in Merge Request widget
  Do not use single quote in headings as it breaks docs.gitlab.com
  Fix broken test
  Update services templates docs
  Simplify Pages admin source docs
  Simplify Pages admin Omnibus docs
  Fix error in MR widget after /merge slash command
  Remove arrow icon from folders
  Create util to handle pagination transformation
  Wrap long Project and Group titles
  Changes after review
  Changes after review
  Rename storePagination to setPagination
  Transforms startTimeAgoLoops into a static method so we can reuse it instead of have 2
  ...
parents 9b86fba5 cee957f5
...@@ -95,7 +95,7 @@ $(() => { ...@@ -95,7 +95,7 @@ $(() => {
}, },
computed: { computed: {
disabled() { disabled() {
return Store.shouldAddBlankState(); return !this.store.lists.filter(list => list.type !== 'blank' && list.type !== 'done').length;
}, },
}, },
template: ` template: `
......
...@@ -4,6 +4,7 @@ ...@@ -4,6 +4,7 @@
* *
* Used to store the Pipelines rendered in the commit view in the pipelines table. * Used to store the Pipelines rendered in the commit view in the pipelines table.
*/ */
require('../../vue_realtime_listener');
class PipelinesStore { class PipelinesStore {
constructor() { constructor() {
...@@ -24,7 +25,7 @@ class PipelinesStore { ...@@ -24,7 +25,7 @@ class PipelinesStore {
* update the time to show how long as passed. * update the time to show how long as passed.
* *
*/ */
startTimeAgoLoops() { static startTimeAgoLoops() {
const startTimeLoops = () => { const startTimeLoops = () => {
this.timeLoopInterval = setInterval(() => { this.timeLoopInterval = setInterval(() => {
this.$children[0].$children.reduce((acc, component) => { this.$children[0].$children.reduce((acc, component) => {
...@@ -44,7 +45,4 @@ class PipelinesStore { ...@@ -44,7 +45,4 @@ class PipelinesStore {
} }
} }
window.gl = window.gl || {}; module.exports = PipelinesStore;
gl.commits = gl.commits || {};
gl.commits.pipelines = gl.commits.pipelines || {};
gl.commits.pipelines.PipelinesStore = PipelinesStore;
...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource')); ...@@ -6,9 +6,8 @@ window.Vue.use(require('vue-resource'));
require('../../lib/utils/common_utils'); require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor'); require('../../vue_shared/vue_resource_interceptor');
require('../../vue_shared/components/pipelines_table'); require('../../vue_shared/components/pipelines_table');
require('../../vue_realtime_listener/index');
require('./pipelines_service'); require('./pipelines_service');
require('./pipelines_store'); const PipelineStore = require('./pipelines_store');
/** /**
* *
...@@ -41,7 +40,7 @@ require('./pipelines_store'); ...@@ -41,7 +40,7 @@ require('./pipelines_store');
data() { data() {
const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset; const pipelinesTableData = document.querySelector('#commit-pipeline-table-view').dataset;
const svgsData = document.querySelector('.pipeline-svgs').dataset; const svgsData = document.querySelector('.pipeline-svgs').dataset;
const store = new gl.commits.pipelines.PipelinesStore(); const store = new PipelineStore();
// Transform svgs DOMStringMap to a plain Object. // Transform svgs DOMStringMap to a plain Object.
const svgsObject = gl.utils.DOMStringMapToObject(svgsData); const svgsObject = gl.utils.DOMStringMapToObject(svgsData);
...@@ -71,7 +70,6 @@ require('./pipelines_store'); ...@@ -71,7 +70,6 @@ require('./pipelines_store');
.then(response => response.json()) .then(response => response.json())
.then((json) => { .then((json) => {
this.store.storePipelines(json); this.store.storePipelines(json);
this.store.startTimeAgoLoops.call(this, Vue);
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
...@@ -80,6 +78,12 @@ require('./pipelines_store'); ...@@ -80,6 +78,12 @@ require('./pipelines_store');
}); });
}, },
beforeUpdate() {
if (this.state.pipelines.length && this.$children) {
PipelineStore.startTimeAgoLoops.call(this, Vue);
}
},
template: ` template: `
<div class="pipelines"> <div class="pipelines">
<div class="realtime-loading" v-if="isLoading"> <div class="realtime-loading" v-if="isLoading">
......
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
/* global Flash */ /* global Flash */
window.Vue = require('vue'); const Vue = require('vue');
window.Vue.use(require('vue-resource')); Vue.use(require('vue-resource'));
require('../services/environments_service'); const EnvironmentsService = require('../services/environments_service');
require('./environment_item'); const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
(() => { require('../../vue_shared/components/table_pagination');
window.gl = window.gl || {}; require('../../lib/utils/common_utils');
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', { module.exports = Vue.component('environment-component', {
props: {
store: { components: {
type: Object, 'environment-table': EnvironmentTable,
required: true, 'table-pagination': gl.VueGlPagination,
default: () => ({}), },
},
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: { canReadEnvironmentParsed() {
'environment-item': gl.environmentsList.EnvironmentItem, return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
}, },
data() { canCreateDeploymentParsed() {
const environmentsData = document.querySelector('#environments-list-view').dataset; return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
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,
};
}, },
computed: { canCreateEnvironmentParsed() {
scope() { return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
return this.$options.getQueryParameter('scope');
},
canReadEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateDeployment);
},
canCreateEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateEnvironment);
},
}, },
/** },
* Fetches all the environments and stores them.
* Toggles loading property. /**
*/ * Fetches all the environments and stores them.
created() { * Toggles loading property.
gl.environmentsService = new EnvironmentsService(this.endpoint); */
created() {
const scope = this.$options.getQueryParameter('scope'); const scope = gl.utils.getParameterByName('scope') || this.visibility;
if (scope) { const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
this.store.storeVisibility(scope);
} const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
this.isLoading = true; const service = new EnvironmentsService(endpoint);
return gl.environmentsService.all() this.isLoading = true;
.then(resp => resp.json())
.then((json) => { return service.all()
this.store.storeEnvironments(json); .then(resp => ({
this.isLoading = false; headers: resp.headers,
}) body: resp.json(),
.catch(() => { }))
this.isLoading = false; .then((response) => {
new Flash('An error occurred while fetching the environments.', 'alert'); 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);
}, },
/** /**
* Transforms the url parameter into an object and * Will change the page number and update the URL.
* returns the one requested.
* *
* @param {String} param * @param {Number} pageNumber desired page to go to.
* @returns {String} The value of the requested parameter. * @return {String}
*/ */
getQueryParameter(parameter) { changePage(pageNumber) {
return window.location.search.substring(1).split('&').reduce((acc, param) => { const param = gl.utils.setParamInURL('page', pageNumber);
const paramSplited = param.split('=');
acc[paramSplited[0]] = paramSplited[1];
return acc;
}, {})[parameter];
},
/** gl.utils.visitUrl(param);
* Converts permission provided as strings to booleans. return param;
* @param {String} string
* @returns {Boolean}
*/
convertPermissionToBoolean(string) {
return string === 'true';
}, },
},
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>
</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
</a>
</div>
</div>
methods: { <div class="environments-container">
toggleRow(model) { <div class="environments-list-loading text-center" v-if="isLoading">
return this.store.toggleFolder(model.name); <i class="fa fa-spinner fa-spin"></i>
}, </div>
},
template: ` <div class="blank-state blank-state-no-icon"
<div :class="cssContainerClass"> v-if="!isLoading && state.environments.length === 0">
<div class="top-area"> <h2 class="blank-state-title js-blank-state-title">
<ul v-if="!isLoading" class="nav-links"> You don't have any environments right now.
<li v-bind:class="{ 'active': scope === undefined }"> </h2>
<a :href="projectEnvironmentsPath"> <p class="blank-state-text">
Available Environments are places where code gets deployed, such as staging or production.
<span class="badge js-available-environments-count"> <br />
{{state.availableCounter}} <a :href="helpPagePath">
</span> Read more about environments
</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
</a> </a>
</div> </p>
<a v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New Environment
</a>
</div> </div>
<div class="environments-container"> <div class="table-holder"
<div class="environments-list-loading text-center" v-if="isLoading"> v-if="!isLoading && state.environments.length > 0">
<i class="fa fa-spinner fa-spin"></i>
</div> <environment-table
:environments="state.environments"
<div class="blank-state blank-state-no-icon" :can-create-deployment="canCreateDeploymentParsed"
v-if="!isLoading && state.environments.length === 0"> :can-read-environment="canReadEnvironmentParsed"
<h2 class="blank-state-title js-blank-state-title"> :play-icon-svg="playIconSvg"
You don't have any environments right now. :terminal-icon-svg="terminalIconSvg"
</h2> :commit-icon-svg="commitIconSvg">
<p class="blank-state-text"> </environment-table>
Environments are places where code gets deployed, such as staging or production.
<br /> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
<a :href="helpPagePath"> :change="changePage"
Read more about environments :pageInfo="state.paginationInformation">
</a> </table-pagination>
</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.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>
</div> </div>
</div> </div>
`, </div>
}); `,
})(); });
/* global Vue */ const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('actions-component', {
props: {
(() => { actions: {
window.gl = window.gl || {}; type: Array,
window.gl.environmentsList = window.gl.environmentsList || {}; required: false,
default: () => [],
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
playIconSvg: {
type: String,
required: false,
},
}, },
template: ` playIconSvg: {
<div class="inline"> type: String,
<div class="dropdown"> required: false,
<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> template: `
<div class="inline">
<ul class="dropdown-menu dropdown-menu-align-right"> <div class="dropdown">
<li v-for="action in actions"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<a :href="action.play_path" <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
data-method="post" <i class="fa fa-caret-down"></i>
rel="nofollow" </a>
class="js-manual-action-link">
<ul class="dropdown-menu dropdown-menu-align-right">
<span class="js-action-play-icon-container" v-html="playIconSvg"></span> <li v-for="action in actions">
<a :href="action.play_path"
<span> data-method="post"
{{action.name}} rel="nofollow"
</span> class="js-manual-action-link">
</a>
</li> <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
</ul>
</div> <span>
{{action.name}}
</span>
</a>
</li>
</ul>
</div> </div>
`, </div>
}); `,
})(); });
/* global Vue */ /**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('external-url-component', {
props: {
(() => { externalUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn external_url" :href="externalUrl" target="_blank"> <a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ const Vue = require('vue');
/* global timeago */ const Timeago = require('timeago.js');
window.Vue = require('vue');
window.timeago = require('timeago.js');
require('../../lib/utils/text_utility'); require('../../lib/utils/text_utility');
require('../../vue_shared/components/commit'); require('../../vue_shared/components/commit');
require('./environment_actions'); const ActionsComponent = require('./environment_actions');
require('./environment_external_url'); const ExternalUrlComponent = require('./environment_external_url');
require('./environment_stop'); const StopComponent = require('./environment_stop');
require('./environment_rollback'); const RollbackComponent = require('./environment_rollback');
require('./environment_terminal_button'); const TerminalButtonComponent = require('./environment_terminal_button');
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
module.exports = Vue.component('environment-item', {
components: {
'commit-component': gl.CommitComponent,
'actions-component': ActionsComponent,
'external-url-component': ExternalUrlComponent,
'stop-component': StopComponent,
'rollback-component': RollbackComponent,
'terminal-button-component': TerminalButtonComponent,
},
props: {
model: {
type: Object,
required: true,
default: () => ({}),
},
(() => { canCreateDeployment: {
/** type: Boolean,
* Envrionment Item Component required: false,
* default: false,
* Used in a hierarchical structure to show folders with children },
* in a table.
* Recursive component based on [Tree View](https://vuejs.org/examples/tree-view.html)
*
* See this [issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/22539)
* for more information.15
*/
window.gl = window.gl || {}; canReadEnvironment: {
window.gl.environmentsList = window.gl.environmentsList || {}; type: Boolean,
window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line required: false,
default: false,
gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { },
components: { commitIconSvg: {
'commit-component': gl.CommitComponent, type: String,
'actions-component': gl.environmentsList.ActionsComponent, required: false,
'external-url-component': gl.environmentsList.ExternalUrlComponent, },
'stop-component': gl.environmentsList.StopComponent,
'rollback-component': gl.environmentsList.RollbackComponent, playIconSvg: {
'terminal-button-component': gl.environmentsList.TerminalButtonComponent, type: String,
}, required: false,
props: {
model: {
type: Object,
required: true,
default: () => ({}),
},
toggleRow: {
type: Function,
required: false,
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
data() {
return {
rowClass: {
'children-row': this.model['vue-isChildren'],
},
};
},
computed: {
/**
* If an item has a `children` entry it means it is a folder.
* Folder items have different behaviours - it is possible to toggle
* them and show their children.
*
* @returns {Boolean|Undefined}
*/
isFolder() {
return this.model.children && this.model.children.length > 0;
},
/**
* If an item is inside a folder structure will return true.
* Used for css purposes.
*
* @returns {Boolean|undefined}
*/
isChildren() {
return this.model['vue-isChildren'];
},
/**
* Counts the number of environments in each folder.
* Used to show a badge with the counter.
*
* @returns {Number|Undefined} The number of environments for the current folder.
*/
childrenCounter() {
return this.model.children && this.model.children.length;
},
/**
* Verifies if `last_deployment` key exists in the current Envrionment.
* This key is required to render most of the html - this method works has
* an helper.
*
* @returns {Boolean}
*/
hasLastDeploymentKey() {
if (this.model.last_deployment &&
!this.$options.isObjectEmpty(this.model.last_deployment)) {
return true;
}
return false;
},
/**
* Verifies is the given environment has manual actions.
* Used to verify if we should render them or nor.
*
* @returns {Boolean|Undefined}
*/
hasManualActions() {
return this.model.last_deployment && this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0;
},
/**
* Returns the value of the `stop_action?` key provided in the response.
*
* @returns {Boolean}
*/
hasStopAction() {
return this.model['stop_action?'];
},
/**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable;
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined;
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
return gl.environmentsList.timeagoInstance.format(
this.model.last_deployment.deployable.created_at,
);
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: gl.text.humanize(action.name),
play_path: action.play_path,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model.last_deployment &&
this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model.last_deployment && this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author) {
return this.model.last_deployment.commit.author;
}
return undefined;
},
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model.last_deployment && this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model.last_deployment &&
this.model.last_deployment.deployable) {
return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model.last_deployment &&
this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
},
/**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
*/
deploymentHasUser() {
return !this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user);
},
/**
* Returns the user object nested with the last_deployment object.
* Used to render the template.
*
* @returns {Object}
*/
deploymentUser() {
if (!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return !this.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.deployable);
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return !this.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
}, },
terminalIconSvg: {
type: String,
required: false,
},
},
computed: {
/** /**
* Helper to verify if certain given object are empty. * Verifies if `last_deployment` key exists in the current Envrionment.
* Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty * This key is required to render most of the html - this method works has
* @param {Object} object * an helper.
* @returns {Bollean} *
* @returns {Boolean}
*/ */
isObjectEmpty(object) { hasLastDeploymentKey() {
for (const key in object) { // eslint-disable-line if (this.model &&
if (hasOwnProperty.call(object, key)) { this.model.last_deployment &&
return false; !this.$options.isObjectEmpty(this.model.last_deployment)) {
} return true;
} }
return true; return false;
}, },
template: ` /**
<tr> * Verifies is the given environment has manual actions.
<td v-bind:class="{ 'children-row': isChildren}"> * Used to verify if we should render them or nor.
<a v-if="!isFolder" *
class="environment-name" * @returns {Boolean|Undefined}
:href="model.environment_path"> */
{{model.name}} hasManualActions() {
</a> return this.model &&
<span v-else v-on:click="toggleRow(model)" class="folder-name"> this.model.last_deployment &&
<span class="folder-icon"> this.model.last_deployment.manual_actions &&
<i v-show="model.isOpen" class="fa fa-caret-down"></i> this.model.last_deployment.manual_actions.length > 0;
<i v-show="!model.isOpen" class="fa fa-caret-right"></i> },
</span>
/**
<span> * Returns the value of the `stop_action?` key provided in the response.
{{model.name}} *
</span> * @returns {Boolean}
*/
<span class="badge"> hasStopAction() {
{{childrenCounter}} return this.model && this.model['stop_action?'];
</span> },
</span>
</td> /**
* Verifies if the `deployable` key is present in `last_deployment` key.
* Used to verify whether we should or not render the rollback partial.
*
* @returns {Boolean|Undefined}
*/
canRetry() {
return this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment &&
this.model.last_deployment.deployable;
},
/**
* Verifies if the date to be shown is present.
*
* @returns {Boolean|Undefined}
*/
canShowDate() {
return this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined;
},
/**
* Human readable date.
*
* @returns {String}
*/
createdDate() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
},
/**
* Returns the manual actions with the name parsed.
*
* @returns {Array.<Object>|Undefined}
*/
manualActions() {
if (this.hasManualActions) {
return this.model.last_deployment.manual_actions.map((action) => {
const parsedAction = {
name: gl.text.humanize(action.name),
play_path: action.play_path,
};
return parsedAction;
});
}
return [];
},
/**
* Builds the string used in the user image alt attribute.
*
* @returns {String}
*/
userImageAltDescription() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.user &&
this.model.last_deployment.user.username) {
return `${this.model.last_deployment.user.username}'s avatar'`;
}
return '';
},
/**
* If provided, returns the commit tag.
*
* @returns {String|Undefined}
*/
commitTag() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.tag) {
return this.model.last_deployment.tag;
}
return undefined;
},
/**
* If provided, returns the commit ref.
*
* @returns {Object|Undefined}
*/
commitRef() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.ref) {
return this.model.last_deployment.ref;
}
return undefined;
},
/**
* If provided, returns the commit url.
*
* @returns {String|Undefined}
*/
commitUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path;
}
return undefined;
},
/**
* If provided, returns the commit short sha.
*
* @returns {String|Undefined}
*/
commitShortSha() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id) {
return this.model.last_deployment.commit.short_id;
}
return undefined;
},
/**
* If provided, returns the commit title.
*
* @returns {String|Undefined}
*/
commitTitle() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.title) {
return this.model.last_deployment.commit.title;
}
return undefined;
},
/**
* If provided, returns the commit tag.
*
* @returns {Object|Undefined}
*/
commitAuthor() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit &&
this.model.last_deployment.commit.author) {
return this.model.last_deployment.commit.author;
}
<td class="deployment-column"> return undefined;
<span v-if="shouldRenderDeploymentID"> },
{{deploymentInternalId}}
/**
* Verifies if the `retry_path` key is present and returns its value.
*
* @returns {String|Undefined}
*/
retryUrl() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path;
}
return undefined;
},
/**
* Verifies if the `last?` key is present and returns its value.
*
* @returns {Boolean|Undefined}
*/
isLastDeployment() {
return this.model && this.model.last_deployment &&
this.model.last_deployment['last?'];
},
/**
* Builds the name of the builds needed to display both the name and the id.
*
* @returns {String}
*/
buildName() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) {
return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
}
return '';
},
/**
* Builds the needed string to show the internal id.
*
* @returns {String}
*/
deploymentInternalId() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`;
}
return '';
},
/**
* Verifies if the user object is present under last_deployment object.
*
* @returns {Boolean}
*/
deploymentHasUser() {
return this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user);
},
/**
* Returns the user object nested with the last_deployment object.
* Used to render the template.
*
* @returns {Object}
*/
deploymentUser() {
if (this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user;
}
return {};
},
/**
* Verifies if the build name column should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderBuildName() {
return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.deployable);
},
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/**
* Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present
* and if the environment is not a folder.
*
* @returns {Boolean}
*/
shouldRenderDeploymentID() {
return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined;
},
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
/**
* Constructs folder URL based on the current location and the folder id.
*
* @return {String}
*/
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
},
/**
* Helper to verify if certain given object are empty.
* Should be replaced by lodash _.isEmpty - https://lodash.com/docs/4.17.2#isEmpty
* @param {Object} object
* @returns {Bollean}
*/
isObjectEmpty(object) {
for (const key in object) { // eslint-disable-line
if (hasOwnProperty.call(object, key)) {
return false;
}
}
return true;
},
template: `
<tr>
<td>
<a v-if="!model.isFolder"
class="environment-name"
:href="environmentPath">
{{model.name}}
</a>
<a v-else class="folder-name" :href="folderUrl">
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
</span> </span>
<span v-if="!isFolder && deploymentHasUser"> <span>
by {{model.folderName}}
<a :href="deploymentUser.web_url" class="js-deploy-user-container">
<img class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a>
</span> </span>
</td>
<td class="environments-build-cell"> <span class="badge">
<a v-if="shouldRenderBuildName" {{model.size}}
class="build-link" </span>
:href="model.last_deployment.deployable.build_path"> </a>
{{buildName}} </td>
<td class="deployment-column">
<span v-if="shouldRenderDeploymentID">
{{deploymentInternalId}}
</span>
<span v-if="!model.isFolder && deploymentHasUser">
by
<a :href="deploymentUser.web_url" class="js-deploy-user-container">
<img class="avatar has-tooltip s20"
:src="deploymentUser.avatar_url"
:alt="userImageAltDescription"
:title="deploymentUser.username" />
</a> </a>
</td> </span>
</td>
<td>
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <td class="environments-build-cell">
<commit-component <a v-if="shouldRenderBuildName"
:tag="commitTag" class="build-link"
:commit-ref="commitRef" :href="buildPath">
:commit-url="commitUrl" {{buildName}}
:short-sha="commitShortSha" </a>
:title="commitTitle" </td>
:author="commitAuthor"
:commit-icon-svg="commitIconSvg"> <td>
</commit-component> <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component
:tag="commitTag"
:commit-ref="commitRef"
:commit-url="commitUrl"
:short-sha="commitShortSha"
:title="commitTitle"
:author="commitAuthor"
:commit-icon-svg="commitIconSvg">
</commit-component>
</div>
<p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
No deployments yet
</p>
</td>
<td>
<span v-if="!model.isFolder && canShowDate"
class="environment-created-date-timeago">
{{createdDate}}
</span>
</td>
<td class="hidden-xs">
<div v-if="!model.isFolder">
<div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container">
<actions-component
:play-icon-svg="playIconSvg"
:actions="manualActions">
</actions-component>
</div> </div>
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
No deployments yet <div v-if="externalURL && canReadEnvironment"
</p> class="inline js-external-url-container">
</td> <external-url-component
:external-url="externalURL">
<td> </external-url-component>
<span </div>
v-if="!isFolder && canShowDate"
class="environment-created-date-timeago"> <div v-if="hasStopAction && canCreateDeployment"
{{createdDate}} class="inline js-stop-component-container">
</span> <stop-component
</td> :stop-url="model.stop_path">
</stop-component>
<td class="hidden-xs"> </div>
<div v-if="!isFolder">
<div v-if="hasManualActions && canCreateDeployment" <div v-if="model && model.terminal_path"
class="inline js-manual-actions-container"> class="inline js-terminal-button-container">
<actions-component <terminal-button-component
:play-icon-svg="playIconSvg" :terminal-icon-svg="terminalIconSvg"
:actions="manualActions"> :terminal-path="model.terminal_path">
</actions-component> </terminal-button-component>
</div> </div>
<div v-if="model.external_url && canReadEnvironment" <div v-if="canRetry && canCreateDeployment"
class="inline js-external-url-container"> class="inline js-rollback-component-container">
<external-url-component <rollback-component
:external-url="model.external_url"> :is-last-deployment="isLastDeployment"
</external-url-component> :retry-url="retryUrl">
</div> </rollback-component>
<div v-if="hasStopAction && canCreateDeployment"
class="inline js-stop-component-container">
<stop-component
:stop-url="model.stop_path">
</stop-component>
</div>
<div v-if="model.terminal_path"
class="inline js-terminal-button-container">
<terminal-button-component
:terminal-icon-svg="terminalIconSvg"
:terminal-path="model.terminal_path">
</terminal-button-component>
</div>
<div v-if="canRetry && canCreateDeployment"
class="inline js-rollback-component-container">
<rollback-component
:is-last-deployment="isLastDeployment"
:retry-url="retryUrl">
</rollback-component>
</div>
</div> </div>
</td> </div>
</tr> </td>
`, </tr>
}); `,
})(); });
/* 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'); module.exports = Vue.component('rollback-component', {
props: {
(() => { retryUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
},
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
default: '',
},
isLastDeployment: { isLastDeployment: {
type: Boolean, type: Boolean,
default: true, default: true,
},
}, },
},
template: ` template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
Re-deploy Re-deploy
</span> </span>
<span v-else> <span v-else>
Rollback Rollback
</span> </span>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('stop-component', {
props: {
(() => { stopUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn stop-env-link" <a class="btn stop-env-link"
:href="stopUrl" :href="stopUrl"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('terminal-button-component', {
props: {
(() => { terminalPath: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
},
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { terminalIconSvg: {
props: { type: String,
terminalPath: { default: '',
type: String,
default: '',
},
terminalIconSvg: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn terminal-button" <a class="btn terminal-button"
:href="terminalPath"> :href="terminalPath">
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a> </a>
`, `,
}); });
})();
/**
* 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>
`,
});
window.Vue = require('vue'); const EnvironmentsComponent = require('./components/environment');
require('./stores/environments_store');
require('./components/environment');
require('../vue_shared/vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
...@@ -9,14 +7,8 @@ $(() => { ...@@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) { if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true); 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'), el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
}); });
}); });
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'),
});
});
/* 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>
`,
});
/* globals Vue */ const Vue = require('vue');
/* eslint-disable no-unused-vars, no-param-reassign */
class EnvironmentsService { class EnvironmentsService {
constructor(endpoint) {
constructor(root) { this.environments = Vue.resource(endpoint);
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();
});
} }
all() { all() {
...@@ -22,4 +10,4 @@ class EnvironmentsService { ...@@ -22,4 +10,4 @@ class EnvironmentsService {
} }
} }
window.EnvironmentsService = EnvironmentsService; module.exports = EnvironmentsService;
/* eslint-disable no-param-reassign */ require('~/lib/utils/common_utils');
(() => { /**
window.gl = window.gl || {}; * Environments Store.
window.gl.environmentsList = window.gl.environmentsList || {}; *
* Stores received environments, count of stopped environments and count of
gl.environmentsList.EnvironmentsStore = { * available environments.
state: {}, */
class EnvironmentsStore {
create() { constructor() {
this.state.environments = []; this.state = {};
this.state.stoppedCounter = 0; this.state.environments = [];
this.state.availableCounter = 0; this.state.stoppedCounter = 0;
this.state.visibility = 'available'; this.state.availableCounter = 0;
this.state.filteredEnvironments = []; this.state.paginationInformation = {};
return this; return this;
}, }
/** /**
* In order to display a tree view we need to modify the received *
* data in to a tree structure based on `environment_type` * Stores the received environments.
* sorted alphabetically. *
* In each children a `vue-` property will be added. This property will be * In the main environments endpoint, each environment has the following schema
* used to know if an item is a children mostly for css purposes. This is * { name: String, size: Number, latest: Object }
* needed because the children row is a fragment instance and therfore does * In the endpoint to retrieve environments from each folder, the environment does
* not accept non-prop attributes. * 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.
* @example *
* it will transform this: * 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.
* { name: "environment", environment_type: "review" }, *
* { name: "environment_1", environment_type: null } * @param {Array} environments
* { name: "environment_2, environment_type: "review" } * @returns {Array}
* ] */
* into this: storeEnvironments(environments = []) {
* [ const filteredEnvironments = environments.map((env) => {
* { name: "review", children: let filtered = {};
* [
* { name: "environment", environment_type: "review", vue-isChildren: true}, if (env.size > 1) {
* { name: "environment_2", environment_type: "review", vue-isChildren: true} filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
* ] }
* },
* {name: "environment_1", environment_type: null} if (env.latest) {
* ] filtered = Object.assign(filtered, env, env.latest);
* delete filtered.latest;
* } else {
* @param {Array} environments List of environments. filtered = Object.assign(filtered, env);
* @returns {Array} Tree structured array with the received environments. }
*/
storeEnvironments(environments = []) { return filtered;
this.state.stoppedCounter = this.countByState(environments, 'stopped'); });
this.state.availableCounter = this.countByState(environments, 'available');
this.state.environments = filteredEnvironments;
const environmentsTree = environments.reduce((acc, environment) => {
if (environment.environment_type !== null) { return filteredEnvironments;
const occurs = acc.filter(element => element.children && }
element.name === environment.environment_type);
setPagination(pagination = {}) {
environment['vue-isChildren'] = true; const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment); this.state.paginationInformation = paginationInformation;
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); return paginationInformation;
} else { }
acc.push({
name: environment.environment_type, /**
children: [environment], * Stores the number of available environments.
isOpen: false, *
'vue-isChildren': environment['vue-isChildren'], * @param {Number} count = 0
}); * @return {Number}
} */
} else { storeAvailableCount(count = 0) {
acc.push(environment); this.state.availableCounter = count;
} return count;
}
return acc;
}, []).slice().sort(this.sortByName); /**
* Stores the number of closed environments.
this.state.environments = environmentsTree; *
* @param {Number} count = 0
this.filterEnvironmentsByVisibility(this.state.environments); * @return {Number}
*/
return environmentsTree; storeStoppedCount(count = 0) {
}, this.state.stoppedCounter = count;
return count;
storeVisibility(visibility) { }
this.state.visibility = visibility; }
},
/** module.exports = EnvironmentsStore;
* 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
},
};
})();
...@@ -54,16 +54,19 @@ require('vendor/task_list'); ...@@ -54,16 +54,19 @@ require('vendor/task_list');
success: function(data, textStatus, jqXHR) { success: function(data, textStatus, jqXHR) {
if ('id' in data) { if ('id' in data) {
$(document).trigger('issuable:change'); $(document).trigger('issuable:change');
const currentTotal = Number($('.issue_counter').text());
if (isClose) { if (isClose) {
$('a.btn-close').addClass('hidden'); $('a.btn-close').addClass('hidden');
$('a.btn-reopen').removeClass('hidden'); $('a.btn-reopen').removeClass('hidden');
$('div.status-box-closed').removeClass('hidden'); $('div.status-box-closed').removeClass('hidden');
$('div.status-box-open').addClass('hidden'); $('div.status-box-open').addClass('hidden');
$('.issue_counter').text(currentTotal - 1);
} else { } else {
$('a.btn-reopen').addClass('hidden'); $('a.btn-reopen').addClass('hidden');
$('a.btn-close').removeClass('hidden'); $('a.btn-close').removeClass('hidden');
$('div.status-box-closed').addClass('hidden'); $('div.status-box-closed').addClass('hidden');
$('div.status-box-open').removeClass('hidden'); $('div.status-box-open').removeClass('hidden');
$('.issue_counter').text(currentTotal + 1);
} }
} else { } else {
new Flash(issueFailMessage, 'alert'); new Flash(issueFailMessage, 'alert');
......
...@@ -231,6 +231,21 @@ ...@@ -231,6 +231,21 @@
return upperCaseHeaders; 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. * Transforms a DOMStringMap into a plain object.
* *
...@@ -241,5 +256,45 @@ ...@@ -241,5 +256,45 @@
acc[element] = DOMStringMapObject[element]; acc[element] = DOMStringMapObject[element];
return acc; 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); })(window);
}).call(this); }).call(this);
...@@ -110,7 +110,7 @@ require('./smart_interval'); ...@@ -110,7 +110,7 @@ require('./smart_interval');
urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : ''; urlSuffix = deleteSourceBranch ? '?deleted_source_branch=true' : '';
return window.location.href = window.location.pathname + urlSuffix; return window.location.href = window.location.pathname + urlSuffix;
} else if (data.merge_error) { } 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 { } else {
callback = function() { callback = function() {
return merge_request_widget.mergeInProgress(deleteSourceBranch); return merge_request_widget.mergeInProgress(deleteSourceBranch);
......
...@@ -62,6 +62,7 @@ ...@@ -62,6 +62,7 @@
<li v-for='artifact in pipeline.details.artifacts'> <li v-for='artifact in pipeline.details.artifacts'>
<a <a
rel="nofollow" rel="nofollow"
download
:href='artifact.path' :href='artifact.path'
> >
<i class="fa fa-download" aria-hidden="true"></i> <i class="fa fa-download" aria-hidden="true"></i>
......
...@@ -5,6 +5,7 @@ window.Vue = require('vue'); ...@@ -5,6 +5,7 @@ window.Vue = require('vue');
require('../vue_shared/components/table_pagination'); require('../vue_shared/components/table_pagination');
require('./store'); require('./store');
require('../vue_shared/components/pipelines_table'); require('../vue_shared/components/pipelines_table');
const CommitPipelinesStoreWithTimeAgo = require('../commit/pipelines/pipelines_store');
((gl) => { ((gl) => {
gl.VuePipelines = Vue.extend({ gl.VuePipelines = Vue.extend({
...@@ -32,10 +33,30 @@ require('../vue_shared/components/pipelines_table'); ...@@ -32,10 +33,30 @@ require('../vue_shared/components/pipelines_table');
const scope = gl.utils.getParameterByName('scope'); const scope = gl.utils.getParameterByName('scope');
if (pagenum) this.pagenum = pagenum; if (pagenum) this.pagenum = pagenum;
if (scope) this.apiScope = scope; if (scope) this.apiScope = scope;
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
}, },
beforeUpdate() {
if (this.pipelines.length && this.$children) {
CommitPipelinesStoreWithTimeAgo.startTimeAgoLoops.call(this, Vue);
}
},
methods: { 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) { change(pagenum, apiScope) {
if (!apiScope) apiScope = 'all';
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
}, },
}, },
......
/* global gl, Flash */ /* global gl, Flash */
/* eslint-disable no-param-reassign, no-underscore-dangle */ /* eslint-disable no-param-reassign */
require('../vue_realtime_listener');
((gl) => { ((gl) => {
const pageValues = (headers) => { const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers); const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.parseIntPagination(normalized);
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'],
};
return paginationInfo; return paginationInfo;
}; };
gl.PipelineStore = class { gl.PipelineStore = class {
fetchDataLoop(Vue, pageNum, url, apiScope) { fetchDataLoop(Vue, pageNum, url, apiScope) {
this.pageRequest = true; this.pageRequest = true;
const updatePipelineNums = (count) => {
const { all } = count;
const running = count.running_or_pending;
document.querySelector('.js-totalbuilds-count').innerHTML = all;
document.querySelector('.js-running-count').innerHTML = running;
};
const goFetch = () =>
this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const res = JSON.parse(response.body);
this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
updatePipelineNums(this.count);
this.pageRequest = false;
}, () => {
this.pageRequest = false;
return new Flash('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(); return this.$http.get(`${url}?scope=${apiScope}&page=${pageNum}`)
.then((response) => {
const pageInfo = pageValues(response.headers);
this.pageInfo = Object.assign({}, this.pageInfo, pageInfo);
const removeIntervals = () => clearInterval(this.timeLoopInterval); const res = JSON.parse(response.body);
const startIntervals = () => startTimeLoops(); this.count = Object.assign({}, this.count, res.count);
this.pipelines = Object.assign([], this.pipelines, res.pipelines);
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 = {})); })(window.gl || (window.gl = {}));
/* global Vue */ /* global Vue */
window.Vue = require('vue');
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -57,9 +57,7 @@ window.Vue = require('vue'); ...@@ -57,9 +57,7 @@ window.Vue = require('vue');
}, },
methods: { methods: {
changePage(e) { changePage(e) {
let apiScope = gl.utils.getParameterByName('scope'); const apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText; const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo; const { totalPages, nextPage, previousPage } = this.pageInfo;
......
...@@ -91,7 +91,7 @@ ...@@ -91,7 +91,7 @@
} }
&.scroll-top { &.scroll-top {
top: 110px; top: 10px;
} }
&.scroll-bottom { &.scroll-bottom {
......
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
font-size: 34px; font-size: 34px;
} }
.environments-folder-name {
font-weight: normal;
padding-top: 20px;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.environments-container { .environments-container {
width: 100%; width: 100%;
...@@ -110,17 +115,20 @@ ...@@ -110,17 +115,20 @@
} }
} }
.children-row .environment-name {
margin-left: 17px;
margin-right: -17px;
}
.folder-icon { .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 { .folder-name {
cursor: pointer; cursor: pointer;
color: $gl-text-color-secondary;
display: inline-block;
} }
} }
...@@ -135,4 +143,4 @@ ...@@ -135,4 +143,4 @@
margin-right: 0; margin-right: 0;
} }
} }
} }
\ No newline at end of file
...@@ -102,6 +102,7 @@ ...@@ -102,6 +102,7 @@
font-size: 24px; font-size: 24px;
font-weight: 400; font-weight: 400;
line-height: 1; line-height: 1;
word-wrap: break-word;
.fa { .fa {
margin-left: 2px; margin-left: 2px;
......
...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
def index def index
@scope = params[:scope] @environments = project.environments
@environments = project.environments.includes(:last_deployment) .with_state(params[:scope] || :available)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: EnvironmentSerializer render json: {
.new(project: @project, user: current_user) environments: EnvironmentSerializer
.represent(@environments) .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 end
end end
......
...@@ -369,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController ...@@ -369,10 +369,13 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end end
def merge_widget_refresh def merge_widget_refresh
if merge_request.in_progress_merge_commit_sha || merge_request.state == 'merged' if merge_request.merge_when_build_succeeds
@status = :success
elsif merge_request.merge_when_build_succeeds
@status = :merge_when_build_succeeds @status = :merge_when_build_succeeds
else
# Only MRs that can be merged end in this action
# MR can be already picked up for merge / merged already or can be waiting for worker to be picked up
# in last case it does not have any special status. Possible error is handled inside widget js function
@status = :success
end end
render 'merge' render 'merge'
......
...@@ -296,4 +296,13 @@ module ApplicationHelper ...@@ -296,4 +296,13 @@ module ApplicationHelper
def page_class def page_class
"issue-boards-page" if current_controller?(:boards) "issue-boards-page" if current_controller?(:boards)
end end
# Returns active css class when condition returns true
# otherwise returns nil.
#
# Example:
# %li{ class: active_when(params[:filter] == '1') }
def active_when(condition)
'active' if condition
end
end end
...@@ -34,6 +34,10 @@ module PageLayoutHelper ...@@ -34,6 +34,10 @@ module PageLayoutHelper
end end
end end
def favicon
Rails.env.development? ? 'favicon-blue.ico' : 'favicon.ico'
end
def page_image def page_image
default = image_url('gitlab_logo.png') default = image_url('gitlab_logo.png')
......
...@@ -42,7 +42,7 @@ class Namespace < ActiveRecord::Base ...@@ -42,7 +42,7 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') } after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
# Save the storage paths before the projects are destroyed to use them on after destroy # Save the storage paths before the projects are destroyed to use them on after destroy
before_destroy(prepend: true) { @old_repository_storage_paths = repository_storage_paths } before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir after_destroy :rm_dir
scope :root, -> { where('type IS NULL') } scope :root, -> { where('type IS NULL') }
...@@ -211,6 +211,14 @@ class Namespace < ActiveRecord::Base ...@@ -211,6 +211,14 @@ class Namespace < ActiveRecord::Base
parent_id_changed? parent_id_changed?
end end
def prepare_for_destroy
old_repository_storage_paths
end
def old_repository_storage_paths
@old_repository_storage_paths ||= repository_storage_paths
end
private private
def repository_storage_paths def repository_storage_paths
...@@ -224,7 +232,7 @@ class Namespace < ActiveRecord::Base ...@@ -224,7 +232,7 @@ class Namespace < ActiveRecord::Base
def rm_dir def rm_dir
# Remove the namespace directory in all storages paths used by member projects # Remove the namespace directory in all storages paths used by member projects
@old_repository_storage_paths.each do |repository_storage_path| old_repository_storage_paths.each do |repository_storage_path|
# Move namespace directory into trash. # Move namespace directory into trash.
# We will remove it later async # We will remove it later async
new_path = "#{path}+#{id}+deleted" new_path = "#{path}+#{id}+deleted"
......
...@@ -214,6 +214,8 @@ class Project < ActiveRecord::Base ...@@ -214,6 +214,8 @@ class Project < ActiveRecord::Base
# Scopes # Scopes
default_scope { where(pending_delete: false) } default_scope { where(pending_delete: false) }
scope :with_deleted, -> { unscope(where: :pending_delete) }
scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) } scope :sorted_by_activity, -> { reorder(last_activity_at: :desc) }
scope :sorted_by_stars, -> { reorder('projects.star_count DESC') } scope :sorted_by_stars, -> { reorder('projects.star_count DESC') }
......
...@@ -160,6 +160,10 @@ class ProjectWiki ...@@ -160,6 +160,10 @@ class ProjectWiki
} }
end end
def repository_storage_path
project.repository_storage_path
end
private private
def init_repo(path_with_namespace) def init_repo(path_with_namespace)
......
...@@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer ...@@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer
end end
def represent(resource, opts = {}) def represent(resource, opts = {})
resource = @paginator.paginate(resource) if paginated?
if itemized? if itemized?
itemize(resource).map do |item| itemize(resource).map do |item|
{ name: item.name, { name: item.name,
...@@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer ...@@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) } latest: super(item.latest, opts) }
end end
else else
resource = @paginator.paginate(resource) if paginated?
super(resource, opts) super(resource, opts)
end end
end end
...@@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer ...@@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer
private private
def itemize(resource) def itemize(resource)
items = resource.group(:item_name).order('item_name ASC') items = resource.order('folder_name ASC')
.pluck('COALESCE(environment_type, name) AS item_name', .group('COALESCE(environment_type, name)')
'COUNT(*) AS environments_count', .select('COALESCE(environment_type, name) AS folder_name',
'MAX(id) AS last_environment_id') 'COUNT(*) AS size', 'MAX(id) AS last_id')
# It makes a difference when you call `paginate` method, because
# although `page` is effective at the end, it calls counting methods
# immediately.
items = @paginator.paginate(items) if paginated?
environments = resource.where(id: items.map(&:last)).index_by(&:id) environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
items.map do |name, size, id| items.map do |item|
Item.new(name, size, environments[id]) Item.new(item.folder_name, item.size, environments[item.last_id])
end end
end end
end end
...@@ -8,7 +8,9 @@ module Groups ...@@ -8,7 +8,9 @@ module Groups
end end
def execute def execute
group.projects.each do |project| group.prepare_for_destroy
group.projects.with_deleted.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup. # Execute the destruction of the models immediately to ensure atomic cleanup.
# Skip repository removal because we remove directory with namespace # Skip repository removal because we remove directory with namespace
# that contain all these repositories # that contain all these repositories
......
...@@ -11,18 +11,20 @@ module MergeRequests ...@@ -11,18 +11,20 @@ module MergeRequests
def execute(merge_request) def execute(merge_request)
@merge_request = merge_request @merge_request = merge_request
return log_merge_error('Merge request is not mergeable', true) unless @merge_request.mergeable? unless @merge_request.mergeable?
return log_merge_error('Merge request is not mergeable', save_message_on_model: true)
end
@source = find_merge_source @source = find_merge_source
return log_merge_error('No source for merge', true) unless @source unless @source
log_merge_error('No source for merge', save_message_on_model: true)
end
merge_request.in_locked_state do merge_request.in_locked_state do
if commit if commit
after_merge after_merge
success success
else
log_merge_error('Can not merge changes', true)
end end
end end
end end
...@@ -43,11 +45,11 @@ module MergeRequests ...@@ -43,11 +45,11 @@ module MergeRequests
if commit_id if commit_id
merge_request.update(merge_commit_sha: commit_id) merge_request.update(merge_commit_sha: commit_id)
else else
merge_request.update(merge_error: 'Conflicts detected during merge') log_merge_error('Conflicts detected during merge', save_message_on_model: true)
false false
end end
rescue GitHooksService::PreReceiveError => e rescue GitHooksService::PreReceiveError => e
merge_request.update(merge_error: e.message) log_merge_error(e.message, save_message_on_model: true)
false false
rescue StandardError => e rescue StandardError => e
merge_request.update(merge_error: "Something went wrong during merge: #{e.message}") merge_request.update(merge_error: "Something went wrong during merge: #{e.message}")
...@@ -70,10 +72,10 @@ module MergeRequests ...@@ -70,10 +72,10 @@ module MergeRequests
@merge_request.force_remove_source_branch? ? @merge_request.author : current_user @merge_request.force_remove_source_branch? ? @merge_request.author : current_user
end end
def log_merge_error(message, http_error = false) def log_merge_error(message, save_message_on_model: false)
Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}") Rails.logger.error("MergeService ERROR: #{merge_request_info} - #{message}")
error(message) if http_error @merge_request.update(merge_error: message) if save_message_on_model
end end
def merge_request_info def merge_request_info
......
...@@ -8,15 +8,14 @@ ...@@ -8,15 +8,14 @@
%div{ class: container_class } %div{ class: container_class }
%ul.nav-links.log-tabs %ul.nav-links.log-tabs
- loggers.each do |klass| - loggers.each do |klass|
%li{ class: (klass == Gitlab::GitLogger ? 'active' : '') }> %li{ class: active_when(klass == Gitlab::GitLogger) }>
= link_to klass::file_name, "##{klass::file_name_noext}", = link_to klass::file_name, "##{klass::file_name_noext}",
'data-toggle' => 'tab' 'data-toggle' => 'tab'
.row-content-block .row-content-block
To prevent performance issues admin logs output the last 2000 lines To prevent performance issues admin logs output the last 2000 lines
.tab-content .tab-content
- loggers.each do |klass| - loggers.each do |klass|
.tab-pane{ class: (klass == Gitlab::GitLogger ? 'active' : ''), .tab-pane{ class: active_when(klass == Gitlab::GitLogger), id: klass::file_name_noext }
id: klass::file_name_noext }
.file-holder#README .file-holder#README
.js-file-title.file-title .js-file-title.file-title
%i.fa.fa-file %i.fa.fa-file
......
...@@ -48,13 +48,13 @@ ...@@ -48,13 +48,13 @@
= link_to admin_projects_path do = link_to admin_projects_path do
All All
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE) do
Private Private
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL) do
Internal Internal
= nav_link(html_options: { class: params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s ? 'active' : '' }) do = nav_link(html_options: { class: active_when(params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s) }) do
= link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do = link_to admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC) do
Public Public
......
...@@ -38,31 +38,31 @@ ...@@ -38,31 +38,31 @@
.nav-block .nav-block
%ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs %ul.nav-links.wide.scrolling-tabs.white.scrolling-tabs
.fade-left .fade-left
= nav_link(html_options: { class: ('active' unless params[:filter]) }) do = nav_link(html_options: { class: active_when(params[:filter].nil?) }) do
= link_to admin_users_path do = link_to admin_users_path do
Active Active
%small.badge= number_with_delimiter(User.active.count) %small.badge= number_with_delimiter(User.active.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'admins') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'admins') }) do
= link_to admin_users_path(filter: "admins") do = link_to admin_users_path(filter: "admins") do
Admins Admins
%small.badge= number_with_delimiter(User.admins.count) %small.badge= number_with_delimiter(User.admins.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_enabled'} filter-two-factor-enabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_enabled')} filter-two-factor-enabled" }) do
= link_to admin_users_path(filter: 'two_factor_enabled') do = link_to admin_users_path(filter: 'two_factor_enabled') do
2FA Enabled 2FA Enabled
%small.badge= number_with_delimiter(User.with_two_factor.count) %small.badge= number_with_delimiter(User.with_two_factor.count)
= nav_link(html_options: { class: "#{'active' if params[:filter] == 'two_factor_disabled'} filter-two-factor-disabled" }) do = nav_link(html_options: { class: "#{active_when(params[:filter] == 'two_factor_disabled')} filter-two-factor-disabled" }) do
= link_to admin_users_path(filter: 'two_factor_disabled') do = link_to admin_users_path(filter: 'two_factor_disabled') do
2FA Disabled 2FA Disabled
%small.badge= number_with_delimiter(User.without_two_factor.count) %small.badge= number_with_delimiter(User.without_two_factor.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'external') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'external') }) do
= link_to admin_users_path(filter: 'external') do = link_to admin_users_path(filter: 'external') do
External External
%small.badge= number_with_delimiter(User.external.count) %small.badge= number_with_delimiter(User.external.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'blocked') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'blocked') }) do
= link_to admin_users_path(filter: "blocked") do = link_to admin_users_path(filter: "blocked") do
Blocked Blocked
%small.badge= number_with_delimiter(User.blocked.count) %small.badge= number_with_delimiter(User.blocked.count)
= nav_link(html_options: { class: ('active' if params[:filter] == 'wop') }) do = nav_link(html_options: { class: active_when(params[:filter] == 'wop') }) do
= link_to admin_users_path(filter: "wop") do = link_to admin_users_path(filter: "wop") do
Without projects Without projects
%small.badge= number_with_delimiter(User.without_projects.count) %small.badge= number_with_delimiter(User.without_projects.count)
......
.top-area .top-area
%ul.nav-links %ul.nav-links
%li{ class: ("active" unless params[:filter]) }> %li{ class: active_when(params[:filter].nil?) }>
= link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects Your Projects
%li{ class: ("active" if params[:filter] == 'starred') }> %li{ class: active_when(params[:filter] == 'starred') }>
= link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
Starred Projects Starred Projects
...@@ -4,15 +4,13 @@ ...@@ -4,15 +4,13 @@
- if current_user.todos.any? - if current_user.todos.any?
.top-area .top-area
%ul.nav-links %ul.nav-links
- todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending') %li.todos-pending{ class: active_when(params[:state].blank? || params[:state] == 'pending') }>
%li{ class: "todos-pending #{todo_pending_active}" }>
= link_to todos_filter_path(state: 'pending') do = link_to todos_filter_path(state: 'pending') do
%span %span
To do To do
%span.badge %span.badge
= number_with_delimiter(todos_pending_count) = number_with_delimiter(todos_pending_count)
- todo_done_active = ('active' if params[:state] == 'done') %li.todos-done{ class: active_when(params[:state] == 'done') }>
%li{ class: "todos-done #{todo_done_active}" }>
= link_to todos_filter_path(state: 'done') do = link_to todos_filter_path(state: 'done') do
%span %span
Done Done
......
...@@ -4,7 +4,7 @@ ...@@ -4,7 +4,7 @@
.login-body .login-body
= render 'devise/sessions/new_crowd' = render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i| - @ldap_servers.each_with_index do |server, i|
.login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: (:active if i.zero? && !crowd_enabled?) } .login-box.tab-pane{ id: "#{server['provider_name']}", role: 'tabpanel', class: active_when(i.zero? && !crowd_enabled?) }
.login-body .login-body
= render 'devise/sessions/new_ldap', server: server = render 'devise/sessions/new_ldap', server: server
- if signin_enabled? - if signin_enabled?
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
%li.active %li.active
= link_to "Crowd", "#crowd", 'data-toggle' => 'tab' = link_to "Crowd", "#crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i| - @ldap_servers.each_with_index do |server, i|
%li{ class: (:active if i.zero? && !crowd_enabled?) } %li{ class: active_when(i.zero? && !crowd_enabled?) }
= link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab' = link_to server['label'], "##{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled? - if signin_enabled?
%li %li
......
...@@ -13,7 +13,7 @@ ...@@ -13,7 +13,7 @@
= link_to filter_projects_path(visibility_level: nil) do = link_to filter_projects_path(visibility_level: nil) do
Any Any
- Gitlab::VisibilityLevel.values.each do |level| - Gitlab::VisibilityLevel.values.each do |level|
%li{ class: (level.to_s == params[:visibility_level]) ? 'active' : 'light' } %li{ class: active_when(level.to_s == params[:visibility_level]) || 'light' }
= link_to filter_projects_path(visibility_level: level) do = link_to filter_projects_path(visibility_level: level) do
= visibility_level_icon(level) = visibility_level_icon(level)
= visibility_level_label(level) = visibility_level_label(level)
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
Any Any
- @tags.each do |tag| - @tags.each do |tag|
%li{ class: (tag.name == params[:tag]) ? 'active' : 'light' } %li{ class: active_when(tag.name == params[:tag]) || 'light' }
= link_to filter_projects_path(tag: tag.name) do = link_to filter_projects_path(tag: tag.name) do
= icon('tag') = icon('tag')
= tag.name = tag.name
...@@ -6,5 +6,5 @@ ...@@ -6,5 +6,5 @@
-# total_pages: total number of pages -# total_pages: total number of pages
-# per_page: number of items to fetch per page -# per_page: number of items to fetch per page
-# remote: data-remote -# remote: data-remote
%li{ class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}" } %li.page{ class: [active_when(page.current?), ('sibling' if page.next? || page.prev?)] }
= link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil } = link_to page, url, { remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil }
...@@ -23,7 +23,7 @@ ...@@ -23,7 +23,7 @@
%title= page_title(site_name) %title= page_title(site_name)
%meta{ name: "description", content: page_description } %meta{ name: "description", content: page_description }
= favicon_link_tag 'favicon.ico' = favicon_link_tag favicon
= stylesheet_link_tag "application", media: "all" = stylesheet_link_tag "application", media: "all"
= stylesheet_link_tag "print", media: "print" = stylesheet_link_tag "print", media: "print"
......
...@@ -8,19 +8,19 @@ ...@@ -8,19 +8,19 @@
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' } %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
%li.dropdown-header Source code %li.dropdown-header Source code
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow', download: '' do
%i.fa.fa-download %i.fa.fa-download
%span Download zip %span Download zip
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow', download: '' do
%i.fa.fa-download %i.fa.fa-download
%span Download tar.gz %span Download tar.gz
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow', download: '' do
%i.fa.fa-download %i.fa.fa-download
%span Download tar.bz2 %span Download tar.bz2
%li %li
= link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow', download: '' do
%i.fa.fa-download %i.fa.fa-download
%span Download tar %span Download tar
...@@ -36,6 +36,6 @@ ...@@ -36,6 +36,6 @@
%li.dropdown-header Previous Artifacts %li.dropdown-header Previous Artifacts
- artifacts.each do |job| - artifacts.each do |job|
%li %li
= link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow' do = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, "#{ref}/download", job: job.name), rel: 'nofollow', download: '' do
%i.fa.fa-download %i.fa.fa-download
%span Download '#{job.name}' %span Download '#{job.name}'
...@@ -78,7 +78,7 @@ ...@@ -78,7 +78,7 @@
%ul.dropdown-menu.dropdown-menu-align-right %ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build| - artifacts.each do |build|
%li %li
= link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow', download: '' do
= icon("download") = icon("download")
%span Download '#{build.name}' artifacts %span Download '#{build.name}' artifacts
......
- @no_container = true
- page_title "Environments"
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play") } }
...@@ -28,7 +28,7 @@ ...@@ -28,7 +28,7 @@
%span %span
CI job CI job
= ci_label_for_status(status) = ci_label_for_status(status)
for for
- commit = @merge_request.diff_head_commit - commit = @merge_request.diff_head_commit
= succeed "." do = succeed "." do
= link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace" = link_to commit.short_id, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, commit), class: "monospace"
......
...@@ -5,23 +5,23 @@ ...@@ -5,23 +5,23 @@
%div{ class: container_class } %div{ class: container_class }
.top-area .top-area
%ul.nav-links %ul.nav-links
%li{ class: ('active' if @scope.nil?) }> %li{ class: active_when(@scope.nil?) }>
= link_to project_pipelines_path(@project) do = link_to project_pipelines_path(@project) do
All All
%span.badge.js-totalbuilds-count %span.badge.js-totalbuilds-count
= number_with_delimiter(@pipelines_count) = number_with_delimiter(@pipelines_count)
%li{ class: ('active' if @scope == 'running') }> %li{ class: active_when(@scope == 'running') }>
= link_to project_pipelines_path(@project, scope: :running) do = link_to project_pipelines_path(@project, scope: :running) do
Running Running
%span.badge.js-running-count %span.badge.js-running-count
= number_with_delimiter(@running_or_pending_count) = number_with_delimiter(@running_or_pending_count)
%li{ class: ('active' if @scope == 'branches') }> %li{ class: active_when(@scope == 'branches') }>
= link_to project_pipelines_path(@project, scope: :branches) do = link_to project_pipelines_path(@project, scope: :branches) do
Branches Branches
%li{ class: ('active' if @scope == 'tags') }> %li{ class: active_when(@scope == 'tags') }>
= link_to project_pipelines_path(@project, scope: :tags) do = link_to project_pipelines_path(@project, scope: :tags) do
Tags Tags
......
%li{ class: params[:id] == wiki_page.slug ? 'active' : '' } %li{ class: active_when(params[:id] == wiki_page.slug) }
= link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do = link_to namespace_project_wiki_path(@project.namespace, @project, wiki_page) do
= wiki_page.title.capitalize = wiki_page.title.capitalize
%ul.nav-links.search-filter %ul.nav-links.search-filter
- if @project - if @project
%li{ class: ("active" if @scope == 'blobs') } %li{ class: active_when(@scope == 'blobs') }
= link_to search_filter_path(scope: 'blobs') do = link_to search_filter_path(scope: 'blobs') do
Code Code
%span.badge %span.badge
= @search_results.blobs_count = @search_results.blobs_count
%li{ class: ("active" if @scope == 'issues') } %li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do = link_to search_filter_path(scope: 'issues') do
Issues Issues
%span.badge %span.badge
= @search_results.issues_count = @search_results.issues_count
%li{ class: ("active" if @scope == 'merge_requests') } %li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do = link_to search_filter_path(scope: 'merge_requests') do
Merge requests Merge requests
%span.badge %span.badge
= @search_results.merge_requests_count = @search_results.merge_requests_count
%li{ class: ("active" if @scope == 'milestones') } %li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do = link_to search_filter_path(scope: 'milestones') do
Milestones Milestones
%span.badge %span.badge
= @search_results.milestones_count = @search_results.milestones_count
%li{ class: ("active" if @scope == 'notes') } %li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do = link_to search_filter_path(scope: 'notes') do
Comments Comments
%span.badge %span.badge
= @search_results.notes_count = @search_results.notes_count
%li{ class: ("active" if @scope == 'wiki_blobs') } %li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do = link_to search_filter_path(scope: 'wiki_blobs') do
Wiki Wiki
%span.badge %span.badge
= @search_results.wiki_blobs_count = @search_results.wiki_blobs_count
%li{ class: ("active" if @scope == 'commits') } %li{ class: active_when(@scope == 'commits') }
= link_to search_filter_path(scope: 'commits') do = link_to search_filter_path(scope: 'commits') do
Commits Commits
%span.badge %span.badge
= @search_results.commits_count = @search_results.commits_count
- elsif @show_snippets - elsif @show_snippets
%li{ class: ("active" if @scope == 'snippet_blobs') } %li{ class: active_when(@scope == 'snippet_blobs') }
= link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do = link_to search_filter_path(scope: 'snippet_blobs', snippets: true, group_id: nil, project_id: nil) do
Snippet Contents Snippet Contents
%span.badge %span.badge
= @search_results.snippet_blobs_count = @search_results.snippet_blobs_count
%li{ class: ("active" if @scope == 'snippet_titles') } %li{ class: active_when(@scope == 'snippet_titles') }
= link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do = link_to search_filter_path(scope: 'snippet_titles', snippets: true, group_id: nil, project_id: nil) do
Titles and Filenames Titles and Filenames
%span.badge %span.badge
= @search_results.snippet_titles_count = @search_results.snippet_titles_count
- else - else
%li{ class: ("active" if @scope == 'projects') } %li{ class: active_when(@scope == 'projects') }
= link_to search_filter_path(scope: 'projects') do = link_to search_filter_path(scope: 'projects') do
Projects Projects
%span.badge %span.badge
= @search_results.projects_count = @search_results.projects_count
%li{ class: ("active" if @scope == 'issues') } %li{ class: active_when(@scope == 'issues') }
= link_to search_filter_path(scope: 'issues') do = link_to search_filter_path(scope: 'issues') do
Issues Issues
%span.badge %span.badge
= @search_results.issues_count = @search_results.issues_count
%li{ class: ("active" if @scope == 'merge_requests') } %li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do = link_to search_filter_path(scope: 'merge_requests') do
Merge requests Merge requests
%span.badge %span.badge
= @search_results.merge_requests_count = @search_results.merge_requests_count
%li{ class: ("active" if @scope == 'milestones') } %li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do = link_to search_filter_path(scope: 'milestones') do
Milestones Milestones
%span.badge %span.badge
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= snippet.title = snippet.title
by by
= link_to user_snippets_path(snippet.author) do = link_to user_snippets_path(snippet.author) do
= image_tag avatar_icon(snippet.author_email), class: "avatar avatar-inline s16", alt: '' = image_tag avatar_icon(snippet.author), class: "avatar avatar-inline s16", alt: ''
= snippet.author_name = snippet.author_name
%span.light= time_ago_with_tooltip(snippet.created_at) %span.light= time_ago_with_tooltip(snippet.created_at)
%h4.snippet-title %h4.snippet-title
......
...@@ -18,6 +18,6 @@ ...@@ -18,6 +18,6 @@
%span %span
by by
= link_to user_snippets_path(snippet_title.author) do = link_to user_snippets_path(snippet_title.author) do
= image_tag avatar_icon(snippet_title.author_email), class: "avatar avatar-inline s16", alt: '' = image_tag avatar_icon(snippet_title.author), class: "avatar avatar-inline s16", alt: ''
= snippet_title.author_name = snippet_title.author_name
%span.light= time_ago_with_tooltip(snippet_title.created_at) %span.light= time_ago_with_tooltip(snippet_title.created_at)
%ul.nav-links %ul.nav-links
%li{ class: ('active' if scope.nil?) }> %li{ class: active_when(scope.nil?) }>
= link_to build_path_proc.call(nil) do = link_to build_path_proc.call(nil) do
All All
%span.badge.js-totalbuilds-count %span.badge.js-totalbuilds-count
= number_with_delimiter(all_builds.count(:id)) = number_with_delimiter(all_builds.count(:id))
%li{ class: ('active' if scope == 'pending') }> %li{ class: active_when(scope == 'pending') }>
= link_to build_path_proc.call('pending') do = link_to build_path_proc.call('pending') do
Pending Pending
%span.badge %span.badge
= number_with_delimiter(all_builds.pending.count(:id)) = number_with_delimiter(all_builds.pending.count(:id))
%li{ class: ('active' if scope == 'running') }> %li{ class: active_when(scope == 'running') }>
= link_to build_path_proc.call('running') do = link_to build_path_proc.call('running') do
Running Running
%span.badge %span.badge
= number_with_delimiter(all_builds.running.count(:id)) = number_with_delimiter(all_builds.running.count(:id))
%li{ class: ('active' if scope == 'finished') }> %li{ class: active_when(scope == 'finished') }>
= link_to build_path_proc.call('finished') do = link_to build_path_proc.call('finished') do
Finished Finished
%span.badge %span.badge
......
...@@ -3,23 +3,23 @@ ...@@ -3,23 +3,23 @@
- issuables = @issues || @merge_requests - issuables = @issues || @merge_requests
%ul.nav-links.issues-state-filters %ul.nav-links.issues-state-filters
%li{ class: ("active" if params[:state] == 'opened') }> %li{ class: active_when(params[:state] == 'opened') }>
= link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do = link_to page_filter_path(state: 'opened', label: true), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened." do
#{issuables_state_counter_text(type, :opened)} #{issuables_state_counter_text(type, :opened)}
- if type == :merge_requests - if type == :merge_requests
%li{ class: ("active" if params[:state] == 'merged') }> %li{ class: active_when(params[:state] == 'merged') }>
= link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do = link_to page_filter_path(state: 'merged', label: true), id: 'state-merged', title: 'Filter by merge requests that are currently merged.' do
#{issuables_state_counter_text(type, :merged)} #{issuables_state_counter_text(type, :merged)}
%li{ class: ("active" if params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do = link_to page_filter_path(state: 'closed', label: true), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.' do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed)}
- else - else
%li{ class: ("active" if params[:state] == 'closed') }> %li{ class: active_when(params[:state] == 'closed') }>
= link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do = link_to page_filter_path(state: 'closed', label: true), id: 'state-all', title: 'Filter by issues that are currently closed.' do
#{issuables_state_counter_text(type, :closed)} #{issuables_state_counter_text(type, :closed)}
%li{ class: ("active" if params[:state] == 'all') }> %li{ class: active_when(params[:state] == 'all') }>
= link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do = link_to page_filter_path(state: 'all', label: true), id: 'state-all', title: "Show all #{page_context_word}." do
#{issuables_state_counter_text(type, :all)} #{issuables_state_counter_text(type, :all)}
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
- include_private = local_assigns.fetch(:include_private, false) - include_private = local_assigns.fetch(:include_private, false)
.nav-links.snippet-scope-menu .nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) } %li{ class: active_when(params[:scope].nil?) }
= link_to subject_snippets_path(subject) do = link_to subject_snippets_path(subject) do
All All
%span.badge %span.badge
...@@ -12,19 +12,19 @@ ...@@ -12,19 +12,19 @@
= subject.snippets.public_and_internal.count = subject.snippets.public_and_internal.count
- if include_private - if include_private
%li{ class: ("active" if params[:scope] == "are_private") } %li{ class: active_when(params[:scope] == "are_private") }
= link_to subject_snippets_path(subject, scope: 'are_private') do = link_to subject_snippets_path(subject, scope: 'are_private') do
Private Private
%span.badge %span.badge
= subject.snippets.are_private.count = subject.snippets.are_private.count
%li{ class: ("active" if params[:scope] == "are_internal") } %li{ class: active_when(params[:scope] == "are_internal") }
= link_to subject_snippets_path(subject, scope: 'are_internal') do = link_to subject_snippets_path(subject, scope: 'are_internal') do
Internal Internal
%span.badge %span.badge
= subject.snippets.are_internal.count = subject.snippets.are_internal.count
%li{ class: ("active" if params[:scope] == "are_public") } %li{ class: active_when(params[:scope] == "are_public") }
= link_to subject_snippets_path(subject, scope: 'are_public') do = link_to subject_snippets_path(subject, scope: 'are_public') do
Public Public
%span.badge %span.badge
......
---
title: don't animate logo when downloading files
merge_request:
author:
---
title: update issue count when closing/reopening an issue
merge_request:
author:
---
title: Only return target project's comments for a commit
merge_request:
author:
---
title: Show merge errors in merge request widget
merge_request: 9229
author:
---
title: Fix error in MR widget after /merge slash command
merge_request: 9259
author:
---
title: Only run timeago loops after rendering timeago components
merge_request:
author:
---
title: Fix positioning of `Scroll to top` button
merge_request:
author:
---
title: Wrap long Project and Group titles
merge_request: 9301
author:
---
title: Set Auto-Submitted header to mails
merge_request:
author: Semyon Pupkov
---
title: Make Karma output look nicer for CI
merge_request: 9165
author: winniehell
---
title: Adds paginationd and folders view to environments table
merge_request:
author:
---
title: Move babel config for instanbul to karma config
merge_request: 9286
author: winniehell
---
title: Seed abuse reports for development
merge_request:
author:
---
title: Reduced query count for snippet search
merge_request:
author:
ActionMailer::Base.register_interceptor(AdditionalEmailHeadersInterceptor)
...@@ -2,8 +2,20 @@ var path = require('path'); ...@@ -2,8 +2,20 @@ var path = require('path');
var webpackConfig = require('./webpack.config.js'); var webpackConfig = require('./webpack.config.js');
var ROOT_PATH = path.resolve(__dirname, '..'); var ROOT_PATH = path.resolve(__dirname, '..');
// add coverage instrumentation to babel config
if (webpackConfig && webpackConfig.module && webpackConfig.module.rules) {
var babelConfig = webpackConfig.module.rules.find(function (rule) {
return rule.loader === 'babel-loader';
});
babelConfig.options = babelConfig.options || {};
babelConfig.options.plugins = babelConfig.options.plugins || [];
babelConfig.options.plugins.push('istanbul');
}
// Karma configuration // Karma configuration
module.exports = function(config) { module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
config.set({ config.set({
basePath: ROOT_PATH, basePath: ROOT_PATH,
browsers: ['PhantomJS'], browsers: ['PhantomJS'],
...@@ -15,7 +27,7 @@ module.exports = function(config) { ...@@ -15,7 +27,7 @@ module.exports = function(config) {
preprocessors: { preprocessors: {
'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'], 'spec/javascripts/**/*.js?(.es6)': ['webpack', 'sourcemap'],
}, },
reporters: ['progress', 'coverage-istanbul'], reporters: [progressReporter, 'coverage-istanbul'],
coverageIstanbulReporter: { coverageIstanbulReporter: {
reports: ['html', 'text-summary'], reports: ['html', 'text-summary'],
dir: 'coverage-javascript/', dir: 'coverage-javascript/',
......
...@@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do
get :terminal get :terminal
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end end
collection do
get :folder, path: 'folders/:id'
end
end end
resource :cycle_analytics, only: [:show] resource :cycle_analytics, only: [:show]
......
...@@ -22,6 +22,7 @@ var config = { ...@@ -22,6 +22,7 @@ var config = {
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
issuable: './issuable/issuable_bundle.js', issuable: './issuable/issuable_bundle.js',
...@@ -54,7 +55,6 @@ var config = { ...@@ -54,7 +55,6 @@ var config = {
exclude: /(node_modules|vendor\/assets)/, exclude: /(node_modules|vendor\/assets)/,
loader: 'babel-loader', loader: 'babel-loader',
options: { options: {
plugins: IS_PRODUCTION ? [] : ['istanbul'],
presets: [ presets: [
["es2015", {"modules": false}], ["es2015", {"modules": false}],
'stage-2' 'stage-2'
......
require 'factory_girl_rails'
(AbuseReport.default_per_page + 3).times do
FactoryGirl.create(:abuse_report)
end
# GitLab Pages Administration # GitLab Pages administration
> **Notes:** > **Notes:**
- [Introduced][ee-80] in GitLab EE 8.3. - [Introduced][ee-80] in GitLab EE 8.3.
...@@ -6,6 +6,7 @@ ...@@ -6,6 +6,7 @@
- GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17. - GitLab Pages [were ported][ce-14605] to Community Edition in GitLab 8.17.
- This guide is for Omnibus GitLab installations. If you have installed - This guide is for Omnibus GitLab installations. If you have installed
GitLab from source, follow the [Pages source installation document](source.md). GitLab from source, follow the [Pages source installation document](source.md).
- To learn how to use GitLab Pages, read the [user documentation][pages-userguide].
--- ---
...@@ -14,9 +15,6 @@ sure to read the [changelog](#changelog) if you are upgrading to a new GitLab ...@@ -14,9 +15,6 @@ sure to read the [changelog](#changelog) if you are upgrading to a new GitLab
version as it may include new features and changes needed to be made in your version as it may include new features and changes needed to be made in your
configuration. configuration.
If you are looking for ways to upload your static content in GitLab Pages, you
probably want to read the [user documentation][pages-userguide].
## Overview ## Overview
GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server GitLab Pages makes use of the [GitLab Pages daemon], a simple HTTP server
...@@ -32,7 +30,7 @@ In the case of custom domains, the Pages daemon needs to listen on ports `80` ...@@ -32,7 +30,7 @@ In the case of custom domains, the Pages daemon needs to listen on ports `80`
and/or `443`. For that reason, there is some flexibility in the way which you and/or `443`. For that reason, there is some flexibility in the way which you
can set it up: can set it up:
1. Run the pages daemon in the same server as GitLab, listening on a secondary IP 1. Run the pages daemon in the same server as GitLab, listening on a secondary IP.
1. Run the pages daemon in a separate server. In that case, the 1. Run the pages daemon in a separate server. In that case, the
[Pages path](#change-storage-path) must also be present in the server that [Pages path](#change-storage-path) must also be present in the server that
the pages daemon is installed, so you will have to share it via network. the pages daemon is installed, so you will have to share it via network.
...@@ -64,11 +62,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the ...@@ -64,11 +62,11 @@ you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this: host that GitLab runs. For example, an entry would look like this:
``` ```
*.example.io. 1800 IN A 1.2.3.4 *.example.io. 1800 IN A 1.1.1.1
``` ```
where `example.io` is the domain under which GitLab Pages will be served where `example.io` is the domain under which GitLab Pages will be served
and `1.2.3.4` is the IP address of your GitLab instance. and `1.1.1.1` is the IP address of your GitLab instance.
> **Note:** > **Note:**
You should not use the GitLab domain to serve user pages. For more information You should not use the GitLab domain to serve user pages. For more information
...@@ -78,101 +76,126 @@ see the [security section](#security). ...@@ -78,101 +76,126 @@ see the [security section](#security).
## Configuration ## Configuration
Depending on your needs, you can install GitLab Pages in four different ways. Depending on your needs, you can set up GitLab Pages in 4 different ways.
The following options are listed from the easiest setup to the most
advanced one. The absolute minimum requirement is to set up the wildcard DNS
since that is needed in all configurations.
### Option 1. Custom domains with HTTPS support ### Wildcard domains
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | >
>---
>
URL scheme: `http://page.example.io`
Pages enabled, daemon is enabled AND pages has external IP support enabled. This is the minimum setup that you can use Pages with. It is the base for all
In that case, the pages daemon is running, NGINX still proxies requests to other setups as described below. Nginx will proxy all requests to the daemon.
the daemon but the daemon is also able to receive requests from the outside The Pages daemon doesn't listen to the outside world.
world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`: 1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`:
```ruby ```ruby
pages_external_url "https://example.io" pages_external_url 'http://example.io'
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
gitlab_pages['external_http'] = '1.1.1.2:80'
gitlab_pages['external_https'] = '1.1.1.2:443'
``` ```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
`1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
### Option 2. Custom domains without HTTPS support ### Wildcard domains with TLS support
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `http://page.example.io` and `http://page.com` | no | yes | no | yes | - Wildcard TLS certificate
>
>---
>
URL scheme: `https://page.example.io`
Pages enabled, daemon is enabled AND pages has external IP support enabled. Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
In that case, the pages daemon is running, NGINX still proxies requests to outside world.
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
1. Edit `/etc/gitlab/gitlab.rb`: 1. Place the certificate and key inside `/etc/gitlab/ssl`
1. In `/etc/gitlab/gitlab.rb` specify the following configuration:
```ruby ```ruby
pages_external_url "http://example.io" pages_external_url 'https://example.io'
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false pages_nginx['redirect_http_to_https'] = true
gitlab_pages['external_http'] = '1.1.1.2:80' pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key"
``` ```
where `1.1.1.1` is the primary IP address that GitLab is listening to and where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key,
`1.1.1.2` the secondary IP where the GitLab Pages daemon listens to. respectively.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
### Option 3. Wildcard HTTPS domain without custom domains ## Advanced configuration
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | In addition to the wildcard domains, you can also have the option to configure
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| GitLab Pages to work with custom domains. Again, there are two options here:
| `https://page.example.io` | yes | no | no | no | support custom domains with and without TLS certificates. The easiest setup is
that without TLS certificates.
Pages enabled, daemon is enabled and NGINX will proxy all requests to the ### Custom domains
daemon. Pages daemon doesn't listen to the outside world.
1. Place the certificate and key inside `/etc/gitlab/ssl` >**Requirements:**
1. In `/etc/gitlab/gitlab.rb` specify the following configuration: - [Wildcard DNS setup](#dns-configuration)
- Secondary IP
>
---
>
URL scheme: `http://page.example.io` and `http://domain.com`
```ruby In that case, the pages daemon is running, Nginx still proxies requests to
pages_external_url 'https://example.io' the daemon but the daemon is also able to receive requests from the outside
world. Custom domains are supported, but no TLS.
pages_nginx['redirect_http_to_https'] = true 1. Edit `/etc/gitlab/gitlab.rb`:
pages_nginx['ssl_certificate'] = "/etc/gitlab/ssl/pages-nginx.crt"
pages_nginx['ssl_certificate_key'] = "/etc/gitlab/ssl/pages-nginx.key" ```ruby
pages_external_url "http://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
gitlab_pages['external_http'] = '1.1.1.2:80'
``` ```
where `pages-nginx.crt` and `pages-nginx.key` are the SSL cert and key, where `1.1.1.1` is the primary IP address that GitLab is listening to and
respectively. `1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
### Option 4. Wildcard HTTP domain without custom domains ### Custom domains with TLS support
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `http://page.example.io` | no | no | no | no | - Wildcard TLS certificate
- Secondary IP
>
---
>
URL scheme: `https://page.example.io` and `https://domain.com`
Pages enabled, daemon is enabled and NGINX will proxy all requests to the In that case, the pages daemon is running, Nginx still proxies requests to
daemon. Pages daemon doesn't listen to the outside world. the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
1. Set the external URL for GitLab Pages in `/etc/gitlab/gitlab.rb`: 1. Edit `/etc/gitlab/gitlab.rb`:
```ruby ```ruby
pages_external_url 'http://example.io' pages_external_url "https://example.io"
nginx['listen_addresses'] = ['1.1.1.1']
pages_nginx['enable'] = false
gitlab_pages['cert'] = "/etc/gitlab/ssl/example.io.crt"
gitlab_pages['cert_key'] = "/etc/gitlab/ssl/example.io.key"
gitlab_pages['external_http'] = '1.1.1.2:80'
gitlab_pages['external_https'] = '1.1.1.2:443'
``` ```
where `1.1.1.1` is the primary IP address that GitLab is listening to and
`1.1.1.2` the secondary IP where the GitLab Pages daemon listens to.
1. [Reconfigure GitLab][reconfigure] 1. [Reconfigure GitLab][reconfigure]
## Change storage path ## Change storage path
......
...@@ -17,22 +17,54 @@ Pages to the latest supported version. ...@@ -17,22 +17,54 @@ Pages to the latest supported version.
## Prerequisites ## Prerequisites
[Read the Omnibus prerequisites section.](index.md#prerequisites) Before proceeding with the Pages configuration, you will need to:
1. Have a separate domain under which the GitLab Pages will be served. In this
document we assume that to be `example.io`.
1. Configure a **wildcard DNS record**.
1. (Optional) Have a **wildcard certificate** for that domain if you decide to
serve Pages under HTTPS.
1. (Optional but recommended) Enable [Shared runners](../../ci/runners/README.md)
so that your users don't have to bring their own.
### DNS configuration
GitLab Pages expect to run on their own virtual host. In your DNS server/provider
you need to add a [wildcard DNS A record][wiki-wildcard-dns] pointing to the
host that GitLab runs. For example, an entry would look like this:
```
*.example.io. 1800 IN A 1.1.1.1
```
where `example.io` is the domain under which GitLab Pages will be served
and `1.1.1.1` is the IP address of your GitLab instance.
> **Note:**
You should not use the GitLab domain to serve user pages. For more information
see the [security section](#security).
[wiki-wildcard-dns]: https://en.wikipedia.org/wiki/Wildcard_DNS_record
## Configuration ## Configuration
Depending on your needs, you can install GitLab Pages in four different ways. Depending on your needs, you can set up GitLab Pages in 4 different ways.
The following options are listed from the easiest setup to the most
advanced one. The absolute minimum requirement is to set up the wildcard DNS
since that is needed in all configurations.
### Option 1. Custom domains with HTTPS support ### Wildcard domains
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `https://page.example.io` and `https://page.com` | yes | redirects to HTTPS | yes | yes | >
>---
>
URL scheme: `http://page.example.io`
Pages enabled, daemon is enabled AND pages has external IP support enabled. This is the minimum setup that you can use Pages with. It is the base for all
In that case, the pages daemon is running, NGINX still proxies requests to other setups as described below. Nginx will proxy all requests to the daemon.
the daemon but the daemon is also able to receive requests from the outside The Pages daemon doesn't listen to the outside world.
world. Custom domains and TLS are supported.
1. Install the Pages daemon: 1. Install the Pages daemon:
...@@ -44,10 +76,14 @@ world. Custom domains and TLS are supported. ...@@ -44,10 +76,14 @@ world. Custom domains and TLS are supported.
sudo -u git -H make sudo -u git -H make
``` ```
1. Edit `gitlab.yml` to look like the example below. You need to change the 1. Go to the GitLab installation directory:
`host` to the FQDN under which GitLab Pages will be served. Set
`external_http` and `external_https` to the secondary IP on which the pages ```bash
daemon will listen for connections: cd /home/git/gitlab
```
1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
the `host` to the FQDN under which GitLab Pages will be served:
```yaml ```yaml
## GitLab Pages ## GitLab Pages
...@@ -57,25 +93,10 @@ world. Custom domains and TLS are supported. ...@@ -57,25 +93,10 @@ world. Custom domains and TLS are supported.
# path: shared/pages # path: shared/pages
host: example.io host: example.io
port: 443 port: 80
https: true https: false
external_http: 1.1.1.2:80
external_https: 1.1.1.2:443
``` ```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
order to enable the pages daemon. In `gitlab_pages_options` the
`-pages-domain`, `-listen-http` and `-listen-https` must match the `host`,
`external_http` and `external_https` settings that you set above respectively.
The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
of the `example.io` domain:
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file: 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash ```bash
...@@ -85,22 +106,21 @@ world. Custom domains and TLS are supported. ...@@ -85,22 +106,21 @@ world. Custom domains and TLS are supported.
Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
1. Restart NGINX 1. Restart NGINX
1. [Restart GitLab][restart] 1. [Restart GitLab][restart]
### Option 2. Custom domains without HTTPS support ### Wildcard domains with TLS support
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `http://page.example.io` and `http://page.com` | no | yes | no | yes | - Wildcard TLS certificate
>
>---
>
URL scheme: `https://page.example.io`
Pages enabled, daemon is enabled AND pages has external IP support enabled. Nginx will proxy all requests to the daemon. Pages daemon doesn't listen to the
In that case, the pages daemon is running, NGINX still proxies requests to outside world.
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
1. Install the Pages daemon: 1. Install the Pages daemon:
...@@ -112,34 +132,20 @@ world. Custom domains and TLS are supported. ...@@ -112,34 +132,20 @@ world. Custom domains and TLS are supported.
sudo -u git -H make sudo -u git -H make
``` ```
1. Edit `gitlab.yml` to look like the example below. You need to change the 1. In `gitlab.yml`, set the port to `443` and https to `true`:
`host` to the FQDN under which GitLab Pages will be served. Set
`external_http` to the secondary IP on which the pages daemon will listen
for connections:
```yaml ```bash
## GitLab Pages
pages: pages:
enabled: true enabled: true
# The location where pages are stored (default: shared/pages). # The location where pages are stored (default: shared/pages).
# path: shared/pages # path: shared/pages
host: example.io host: example.io
port: 80 port: 443
https: false https: true
external_http: 1.1.1.2:80
``` ```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
order to enable the pages daemon. In `gitlab_pages_options` the
`-pages-domain` and `-listen-http` must match the `host` and `external_http`
settings that you set above respectively:
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file: 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash ```bash
...@@ -149,20 +155,30 @@ world. Custom domains and TLS are supported. ...@@ -149,20 +155,30 @@ world. Custom domains and TLS are supported.
Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
1. Restart NGINX 1. Restart NGINX
1. [Restart GitLab][restart] 1. [Restart GitLab][restart]
### Option 3. Wildcard HTTPS domain without custom domains
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | ## Advanced configuration
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:|
| `https://page.example.io` | yes | no | no | no | In addition to the wildcard domains, you can also have the option to configure
GitLab Pages to work with custom domains. Again, there are two options here:
support custom domains with and without TLS certificates. The easiest setup is
that without TLS certificates.
### Custom domains
Pages enabled, daemon is enabled and NGINX will proxy all requests to the >**Requirements:**
daemon. Pages daemon doesn't listen to the outside world. - [Wildcard DNS setup](#dns-configuration)
- Secondary IP
>
---
>
URL scheme: `http://page.example.io` and `http://domain.com`
In that case, the pages daemon is running, Nginx still proxies requests to
the daemon but the daemon is also able to receive requests from the outside
world. Custom domains are supported, but no TLS.
1. Install the Pages daemon: 1. Install the Pages daemon:
...@@ -173,20 +189,35 @@ daemon. Pages daemon doesn't listen to the outside world. ...@@ -173,20 +189,35 @@ daemon. Pages daemon doesn't listen to the outside world.
sudo -u git -H git checkout v0.2.4 sudo -u git -H git checkout v0.2.4
sudo -u git -H make sudo -u git -H make
``` ```
1. In `gitlab.yml`, set the port to `443` and https to `true`:
```bash 1. Edit `gitlab.yml` to look like the example below. You need to change the
## GitLab Pages `host` to the FQDN under which GitLab Pages will be served. Set
`external_http` to the secondary IP on which the pages daemon will listen
for connections:
```yaml
pages: pages:
enabled: true enabled: true
# The location where pages are stored (default: shared/pages). # The location where pages are stored (default: shared/pages).
# path: shared/pages # path: shared/pages
host: example.io host: example.io
port: 443 port: 80
https: true https: false
external_http: 1.1.1.2:80
``` ```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
order to enable the pages daemon. In `gitlab_pages_options` the
`-pages-domain` and `-listen-http` must match the `host` and `external_http`
settings that you set above respectively:
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80"
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file: 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash ```bash
...@@ -196,17 +227,26 @@ daemon. Pages daemon doesn't listen to the outside world. ...@@ -196,17 +227,26 @@ daemon. Pages daemon doesn't listen to the outside world.
Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
1. Restart NGINX 1. Restart NGINX
1. [Restart GitLab][restart] 1. [Restart GitLab][restart]
### Option 4. Wildcard HTTP domain without custom domains ### Custom domains with TLS support
| URL scheme | Wildcard certificate | Custom domain with HTTP support | Custom domain with HTTPS support | Secondary IP | >**Requirements:**
| --- |:---:|:---:|:---:|:---:|:---:|:---:|:---:| - [Wildcard DNS setup](#dns-configuration)
| `http://page.example.io` | no | no | no | no | - Wildcard TLS certificate
- Secondary IP
>
---
>
URL scheme: `https://page.example.io` and `https://domain.com`
Pages enabled, daemon is enabled and NGINX will proxy all requests to the In that case, the pages daemon is running, Nginx still proxies requests to
daemon. Pages daemon doesn't listen to the outside world. the daemon but the daemon is also able to receive requests from the outside
world. Custom domains and TLS are supported.
1. Install the Pages daemon: 1. Install the Pages daemon:
...@@ -218,14 +258,10 @@ daemon. Pages daemon doesn't listen to the outside world. ...@@ -218,14 +258,10 @@ daemon. Pages daemon doesn't listen to the outside world.
sudo -u git -H make sudo -u git -H make
``` ```
1. Go to the GitLab installation directory: 1. Edit `gitlab.yml` to look like the example below. You need to change the
`host` to the FQDN under which GitLab Pages will be served. Set
```bash `external_http` and `external_https` to the secondary IP on which the pages
cd /home/git/gitlab daemon will listen for connections:
```
1. Edit `gitlab.yml` and under the `pages` setting, set `enabled` to `true` and
the `host` to the FQDN under which GitLab Pages will be served:
```yaml ```yaml
## GitLab Pages ## GitLab Pages
...@@ -235,10 +271,25 @@ daemon. Pages daemon doesn't listen to the outside world. ...@@ -235,10 +271,25 @@ daemon. Pages daemon doesn't listen to the outside world.
# path: shared/pages # path: shared/pages
host: example.io host: example.io
port: 80 port: 443
https: false https: true
external_http: 1.1.1.2:80
external_https: 1.1.1.2:443
``` ```
1. Edit `/etc/default/gitlab` and set `gitlab_pages_enabled` to `true` in
order to enable the pages daemon. In `gitlab_pages_options` the
`-pages-domain`, `-listen-http` and `-listen-https` must match the `host`,
`external_http` and `external_https` settings that you set above respectively.
The `-root-cert` and `-root-key` settings are the wildcard TLS certificates
of the `example.io` domain:
```
gitlab_pages_enabled=true
gitlab_pages_options="-pages-domain example.io -pages-root $app_root/shared/pages -listen-proxy 127.0.0.1:8090 -listen-http 1.1.1.2:80 -listen-https 1.1.1.2:443 -root-cert /path/to/example.io.crt -root-key /path/to/example.io.key
```
1. Copy the `gitlab-pages-ssl` Nginx configuration file: 1. Copy the `gitlab-pages-ssl` Nginx configuration file:
```bash ```bash
...@@ -248,9 +299,27 @@ daemon. Pages daemon doesn't listen to the outside world. ...@@ -248,9 +299,27 @@ daemon. Pages daemon doesn't listen to the outside world.
Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL. Replace `gitlab-pages-ssl` with `gitlab-pages` if you are not using SSL.
1. Edit all GitLab related configs in `/etc/nginx/site-available/` and replace
`0.0.0.0` with `1.1.1.1`, where `1.1.1.1` the primary IP where GitLab
listens to.
1. Restart NGINX 1. Restart NGINX
1. [Restart GitLab][restart] 1. [Restart GitLab][restart]
## Change storage path
Follow the steps below to change the default path where GitLab Pages' contents
are stored.
1. Pages are stored by default in `/var/opt/gitlab/gitlab-rails/shared/pages`.
If you wish to store them in another location you must set it up in
`/etc/gitlab/gitlab.rb`:
```ruby
gitlab_rails['pages_path'] = "/mnt/storage/pages"
```
1. [Reconfigure GitLab][reconfigure]
## NGINX caveats ## NGINX caveats
>**Note:** >**Note:**
......
...@@ -535,6 +535,7 @@ deploy_review: ...@@ -535,6 +535,7 @@ deploy_review:
- master - master
stop_review: stop_review:
stage: deploy
variables: variables:
GIT_STRATEGY: none GIT_STRATEGY: none
script: script:
...@@ -555,7 +556,9 @@ when their associated branch is deleted. ...@@ -555,7 +556,9 @@ when their associated branch is deleted.
When you have an environment that has a stop action defined (typically when When you have an environment that has a stop action defined (typically when
the environment describes a review app), GitLab will automatically trigger a the environment describes a review app), GitLab will automatically trigger a
stop action when the associated branch is deleted. stop action when the associated branch is deleted. The `stop_review` job must
be in the same `stage` as the `deploy_review` one in order for the environment
to automatically stop.
You can read more in the [`.gitlab-ci.yml` reference][onstop]. You can read more in the [`.gitlab-ci.yml` reference][onstop].
......
...@@ -690,6 +690,8 @@ The `stop_review_app` job is **required** to have the following keywords defined ...@@ -690,6 +690,8 @@ The `stop_review_app` job is **required** to have the following keywords defined
- `when` - [reference](#when) - `when` - [reference](#when)
- `environment:name` - `environment:name`
- `environment:action` - `environment:action`
- `stage` should be the same as the `review_app` in order for the environment
to stop automatically when the branch is deleted
#### dynamic environments #### dynamic environments
......
...@@ -3,7 +3,7 @@ ...@@ -3,7 +3,7 @@
The purpose of this guide is to document potential "gotchas" that contributors The purpose of this guide is to document potential "gotchas" that contributors
might encounter or should avoid during development of GitLab CE and EE. might encounter or should avoid during development of GitLab CE and EE.
## Don't `describe` symbols ## Do not `describe` symbols
Consider the following model spec: Consider the following model spec:
...@@ -32,7 +32,7 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth ...@@ -32,7 +32,7 @@ spec/models/user_spec.rb|6 error| Failure/Error: u = described_class.new NoMeth
Except for the top-level `describe` block, always provide a String argument to Except for the top-level `describe` block, always provide a String argument to
`describe`. `describe`.
## Don't assert against the absolute value of a sequence-generated attribute ## Do not assert against the absolute value of a sequence-generated attribute
Consider the following factory: Consider the following factory:
...@@ -121,7 +121,7 @@ describe API::Labels do ...@@ -121,7 +121,7 @@ describe API::Labels do
end end
``` ```
## Don't `rescue Exception` ## Do not `rescue Exception`
See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception]. See ["Why is it bad style to `rescue Exception => e` in Ruby?"][Exception].
...@@ -130,7 +130,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9 ...@@ -130,7 +130,7 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897 [Exception]: http://stackoverflow.com/q/10048173/223897
## Don't use inline JavaScript in views ## Do not use inline JavaScript in views
Using the inline `:javascript` Haml filters comes with a Using the inline `:javascript` Haml filters comes with a
performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided. performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
......
...@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an ...@@ -5,7 +5,7 @@ GitLab University is the best place to learn about **Version Control with Git an
It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com) It doesn't replace, but accompanies our great [Documentation](https://docs.gitlab.com)
and [Blog Articles](https://about.gitlab.com/blog/). and [Blog Articles](https://about.gitlab.com/blog/).
Would you like to contribute to GitLab University? Then please take a look at our contribution [process](process) for more information. Would you like to contribute to GitLab University? Then please take a look at our contribution [process](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/PROCESS.md) for more information.
## Gitlab University Curriculum ## Gitlab University Curriculum
......
...@@ -49,7 +49,19 @@ Install Bundler: ...@@ -49,7 +49,19 @@ Install Bundler:
sudo gem install bundler --no-ri --no-rdoc sudo gem install bundler --no-ri --no-rdoc
``` ```
### 4. Get latest code ### 4. Update Node
GitLab now runs [webpack](http://webpack.js.org) to compile frontend assets and
it has a minimum requirement of node v4.3.0.
You can check which version you are running with `node -v`. If you are running
a version older than `v4.3.0` you will need to update to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the nodejs.org website.
<https://nodejs.org/en/download/>
### 5. Get latest code
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -76,7 +88,7 @@ cd /home/git/gitlab ...@@ -76,7 +88,7 @@ cd /home/git/gitlab
sudo -u git -H git checkout 8-17-stable-ee sudo -u git -H git checkout 8-17-stable-ee
``` ```
### 5. Install libs, migrations, etc. ### 6. Install libs, migrations, etc.
```bash ```bash
cd /home/git/gitlab cd /home/git/gitlab
...@@ -93,13 +105,16 @@ sudo -u git -H bundle clean ...@@ -93,13 +105,16 @@ sudo -u git -H bundle clean
# Run database migrations # Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
# Install/update frontend asset dependencies
sudo -u git -H npm install --production
# Clean up assets and cache # Clean up assets and cache
sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production sudo -u git -H bundle exec rake gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production
``` ```
**MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md). **MySQL installations**: Run through the `MySQL strings limits` and `Tables and data conversion to utf8mb4` [tasks](../install/database_mysql.md).
### 6. Update gitlab-workhorse ### 7. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from [Go 1.5](https://golang.org/dl) which should already be on your system from
...@@ -111,7 +126,7 @@ cd /home/git/gitlab ...@@ -111,7 +126,7 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
``` ```
### 7. Update gitlab-shell ### 8. Update gitlab-shell
```bash ```bash
cd /home/git/gitlab-shell cd /home/git/gitlab-shell
...@@ -120,7 +135,7 @@ sudo -u git -H git fetch --all --tags ...@@ -120,7 +135,7 @@ sudo -u git -H git fetch --all --tags
sudo -u git -H git checkout v4.1.1 sudo -u git -H git checkout v4.1.1
``` ```
### 8. Update configuration files ### 9. Update configuration files
#### New configuration options for `gitlab.yml` #### New configuration options for `gitlab.yml`
...@@ -194,14 +209,14 @@ For Ubuntu 16.04.1 LTS: ...@@ -194,14 +209,14 @@ For Ubuntu 16.04.1 LTS:
sudo systemctl daemon-reload sudo systemctl daemon-reload
``` ```
### 9. Start application ### 10. Start application
```bash ```bash
sudo service gitlab start sudo service gitlab start
sudo service nginx restart sudo service nginx restart
``` ```
### 10. Check application status ### 11. Check application status
Check if GitLab and its environment are configured correctly: Check if GitLab and its environment are configured correctly:
......
# Services Templates # Services templates
A GitLab administrator can add a service template that sets a default for each A GitLab administrator can add a service template that sets a default for each
project. This makes it much easier to configure individual projects. project. After a service template is enabled, it will be applied to new
projects only and its details will be pre-filled on the project's Service page.
After the template is created, the template details will be pre-filled on a ## Enable a service template
project's Service page.
## Enable a Service template
In GitLab's Admin area, navigate to **Service Templates** and choose the In GitLab's Admin area, navigate to **Service Templates** and choose the
service template you wish to create. service template you wish to create.
For example, in the image below you can see Redmine. ## Services for external issue trackers
In the image below you can see how a service template for Redmine would look
like.
![Redmine service template](img/services_templates_redmine_example.png) ![Redmine service template](img/services_templates_redmine_example.png)
--- ---
**NOTE:** For each project, you will still need to configure the issue tracking For each project, you will still need to configure the issue tracking
URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used URLs by replacing `:issues_tracker_id` in the above screenshot with the ID used
by your external issue tracker. Prior to GitLab v7.8, this ID was configured in by your external issue tracker. Prior to GitLab v7.8, this ID was configured in
the project settings, and GitLab would automatically update the URL configured the project settings, and GitLab would automatically update the URL configured
in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs in `gitlab.yml`. This behavior is now deprecated and all issue tracker URLs
must be configured directly within the project's **Services** settings. must be configured directly within the project's **Integrations** settings.
class AdditionalEmailHeadersInterceptor
def self.delivering_email(message)
message.headers(
'Auto-Submitted' => 'auto-generated',
'X-Auto-Response-Suppress' => 'All'
)
end
end
...@@ -114,7 +114,7 @@ module API ...@@ -114,7 +114,7 @@ module API
commit = user_project.commit(params[:sha]) commit = user_project.commit(params[:sha])
not_found! 'Commit' unless commit not_found! 'Commit' unless commit
notes = Note.where(commit_id: commit.id).order(:created_at) notes = user_project.notes.where(commit_id: commit.id).order(:created_at)
present paginate(notes), with: Entities::CommitNote present paginate(notes), with: Entities::CommitNote
end end
......
...@@ -15,14 +15,6 @@ module Gitlab ...@@ -15,14 +15,6 @@ module Gitlab
execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all)) execute(%W(#{git_bin_path} --git-dir=#{repo_path} bundle create #{bundle_path} --all))
end end
def git_unbundle(repo_path:, bundle_path:)
execute(%W(#{git_bin_path} clone --bare #{bundle_path} #{repo_path}))
end
def git_restore_hooks
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end
def mkdir_p(path) def mkdir_p(path)
FileUtils.mkdir_p(path, mode: DEFAULT_MODE) FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
FileUtils.chmod(DEFAULT_MODE, path) FileUtils.chmod(DEFAULT_MODE, path)
...@@ -56,10 +48,6 @@ module Gitlab ...@@ -56,10 +48,6 @@ module Gitlab
FileUtils.copy_entry(source, destination) FileUtils.copy_entry(source, destination)
true true
end end
def repository_storage_paths_args
Gitlab.config.repositories.storages.values
end
end end
end end
end end
...@@ -2,6 +2,7 @@ module Gitlab ...@@ -2,6 +2,7 @@ module Gitlab
module ImportExport module ImportExport
class RepoRestorer class RepoRestorer
include Gitlab::ImportExport::CommandLineUtil include Gitlab::ImportExport::CommandLineUtil
include Gitlab::ShellAdapter
def initialize(project:, shared:, path_to_bundle:) def initialize(project:, shared:, path_to_bundle:)
@project = project @project = project
...@@ -12,29 +13,11 @@ module Gitlab ...@@ -12,29 +13,11 @@ module Gitlab
def restore def restore
return true unless File.exist?(@path_to_bundle) return true unless File.exist?(@path_to_bundle)
mkdir_p(path_to_repo) gitlab_shell.import_repository(@project.repository_storage_path, @project.path_with_namespace, @path_to_bundle)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e rescue => e
@shared.error(e) @shared.error(e)
false false
end end
private
def path_to_repo
@project.repository.path_to_repo
end
def repo_restore_hooks
return true if wiki?
git_restore_hooks
end
def wiki?
@project.class.name == 'ProjectWiki'
end
end end
end end
end end
...@@ -80,8 +80,10 @@ module Gitlab ...@@ -80,8 +80,10 @@ module Gitlab
# import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git") # import_repository("/path/to/storage", "gitlab/gitlab-ci", "https://github.com/randx/six.git")
# #
def import_repository(storage, name, url) def import_repository(storage, name, url)
# Timeout should be less than 900 ideally, to prevent the memory killer
# to silently kill the process without knowing we are timing out here.
output, status = Popen::popen([gitlab_shell_projects_path, 'import-project', output, status = Popen::popen([gitlab_shell_projects_path, 'import-project',
storage, "#{name}.git", url, '900']) storage, "#{name}.git", url, '800'])
raise Error, output unless status.zero? raise Error, output unless status.zero?
true true
end end
......
...@@ -31,11 +31,11 @@ module Gitlab ...@@ -31,11 +31,11 @@ module Gitlab
private private
def snippet_titles def snippet_titles
limit_snippets.search(query).order('updated_at DESC') limit_snippets.search(query).order('updated_at DESC').includes(:author)
end end
def snippet_blobs def snippet_blobs
limit_snippets.search_code(query).order('updated_at DESC') limit_snippets.search_code(query).order('updated_at DESC').includes(:author)
end end
def default_scope def default_scope
......
...@@ -3,9 +3,12 @@ require 'spec_helper' ...@@ -3,9 +3,12 @@ require 'spec_helper'
describe Projects::EnvironmentsController do describe Projects::EnvironmentsController do
include ApiHelpers include ApiHelpers
let(:environment) { create(:environment) } let(:user) { create(:user) }
let(:project) { environment.project } let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:environment) do
create(:environment, name: 'production', project: project)
end
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do ...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do
end end
end end
context 'when requesting JSON response' do context 'when requesting JSON response for folders' do
it 'responds with correct JSON' do before do
get :index, environment_params(format: :json) create(:environment, project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
name: 'staging/review-2',
state: :available)
create(:environment, project: project,
name: 'staging/review-3',
state: :stopped)
end
let(:environments) { json_response['environments'] }
context 'when requesting available environments scope' do
before do
get :index, environment_params(format: :json, scope: :available)
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
end
first_environment = json_response.first it 'contains values describing environment scopes sizes' do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end
expect(first_environment).not_to be_empty context 'when requesting stopped environments scope' do
expect(first_environment['name']). to eq environment.name before do
get :index, environment_params(format: :json, scope: :stopped)
end
it 'responds with a payload describing stopped environments' do
expect(environments.count).to eq 1
expect(environments.first['name']).to eq 'staging'
expect(environments.first['size']).to eq 1
expect(environments.first['latest']['name']).to eq 'staging/review-3'
end
it 'contains values describing environment scopes sizes' do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end end
end end
end end
......
...@@ -1143,15 +1143,15 @@ describe Projects::MergeRequestsController do ...@@ -1143,15 +1143,15 @@ describe Projects::MergeRequestsController do
end end
end end
context 'when no special status for MR' do context 'when MR does not have special state' do
let(:merge_request) { create(:merge_request, source_project: project) } let(:merge_request) { create(:merge_request, source_project: project) }
it 'returns an OK response' do it 'returns an OK response' do
expect(response).to have_http_status(:ok) expect(response).to have_http_status(:ok)
end end
it 'sets status to nil' do it 'sets status to success' do
expect(assigns(:status)).to be_nil expect(assigns(:status)).to eq(:success)
expect(response).to render_template('merge') expect(response).to render_template('merge')
end end
end end
......
...@@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do ...@@ -28,6 +28,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_content('Welcome to your Issue Board!') expect(page).to have_content('Welcome to your Issue Board!')
end end
it 'disables add issues button by default' do
button = page.find('.issue-boards-search button', text: 'Add issues')
expect(button[:disabled]).to eq true
end
it 'hides the blank state when clicking nevermind button' do it 'hides the blank state when clicking nevermind button' do
page.within(find('.board-blank-state')) do page.within(find('.board-blank-state')) do
click_button("Nevermind, I'll use my own") click_button("Nevermind, I'll use my own")
......
...@@ -52,4 +52,19 @@ describe 'Merge request', :feature, :js do ...@@ -52,4 +52,19 @@ describe 'Merge request', :feature, :js do
end end
end end
end end
context 'merge error' do
before do
allow_any_instance_of(Repository).to receive(:merge).and_return(false)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
click_button 'Accept Merge Request'
wait_for_ajax
end
it 'updates the MR widget' do
page.within('.mr-widget-body') do
expect(page).to have_content('Conflicts detected during merge')
end
end
end
end end
...@@ -275,7 +275,7 @@ feature 'Builds', :feature do ...@@ -275,7 +275,7 @@ feature 'Builds', :feature do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
it 'shows a link to lastest deployment' do it 'shows a link to latest deployment' do
visit namespace_project_build_path(project.namespace, project, build) visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link('latest deployment') expect(page).to have_link('latest deployment')
......
...@@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do ...@@ -39,6 +39,13 @@ feature 'Download buttons in project main page', feature: true do
expect(page).to have_link "Download '#{build.name}'", href: href expect(page).to have_link "Download '#{build.name}'", href: href
end end
scenario 'download links have download attribute' do
expect(page).to have_selector('a', text: 'Download')
page.all('a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
end
end
end end
end end
end end
...@@ -218,6 +218,14 @@ describe 'Pipelines', :feature, :js do ...@@ -218,6 +218,14 @@ describe 'Pipelines', :feature, :js do
expect(page).to have_link(with_artifacts.name) expect(page).to have_link(with_artifacts.name)
end end
it 'has download attribute on download links' do
find('.js-pipeline-dropdown-download').click
expect(page).to have_selector('a', text: 'Download')
page.all('.build-artifacts a', text: 'Download').each do |link|
expect(link[:download]).to eq ''
end
end
end end
context 'with artifacts expired' do context 'with artifacts expired' do
......
...@@ -265,4 +265,9 @@ describe ApplicationHelper do ...@@ -265,4 +265,9 @@ describe ApplicationHelper do
expect(helper.render_markup('foo.adoc', content)).to eq('NOEL') expect(helper.render_markup('foo.adoc', content)).to eq('NOEL')
end end
end end
describe '#active_when' do
it { expect(helper.active_when(true)).to eq('active') }
it { expect(helper.active_when(false)).to eq(nil) }
end
end end
...@@ -40,6 +40,18 @@ describe PageLayoutHelper do ...@@ -40,6 +40,18 @@ describe PageLayoutHelper do
end end
end end
describe 'favicon' do
it 'defaults to favicon.ico' do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('production'))
expect(helper.favicon).to eq 'favicon.ico'
end
it 'has blue favicon for development' do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
expect(helper.favicon).to eq 'favicon-blue.ico'
end
end
describe 'page_image' do describe 'page_image' do
it 'defaults to the GitLab logo' do it 'defaults to the GitLab logo' do
expect(helper.page_image).to end_with 'assets/gitlab_logo.png' expect(helper.page_image).to end_with 'assets/gitlab_logo.png'
......
require('~/commit/pipelines/pipelines_store'); const PipelinesStore = require('~/commit/pipelines/pipelines_store');
describe('Store', () => { describe('Store', () => {
let store; let store;
beforeEach(() => { beforeEach(() => {
store = new gl.commits.pipelines.PipelinesStore(); store = new PipelinesStore();
}); });
// unregister intervals and event handlers // unregister intervals and event handlers
......
require('~/environments/components/environment_actions'); const ActionsComponent = require('~/environments/components/environment_actions');
describe('Actions Component', () => { describe('Actions Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -19,7 +19,7 @@ describe('Actions Component', () => { ...@@ -19,7 +19,7 @@ describe('Actions Component', () => {
}, },
]; ];
const component = new window.gl.environmentsList.ActionsComponent({ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
actions: actionsMock, actions: actionsMock,
...@@ -47,7 +47,7 @@ describe('Actions Component', () => { ...@@ -47,7 +47,7 @@ describe('Actions Component', () => {
}, },
]; ];
const component = new window.gl.environmentsList.ActionsComponent({ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
actions: actionsMock, actions: actionsMock,
......
require('~/environments/components/environment_external_url'); const ExternalUrlComponent = require('~/environments/components/environment_external_url');
describe('External URL Component', () => { describe('External URL Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -8,7 +8,7 @@ describe('External URL Component', () => { ...@@ -8,7 +8,7 @@ describe('External URL Component', () => {
it('should link to the provided externalUrl prop', () => { it('should link to the provided externalUrl prop', () => {
const externalURL = 'https://gitlab.com'; const externalURL = 'https://gitlab.com';
const component = new window.gl.environmentsList.ExternalUrlComponent({ const component = new ExternalUrlComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
externalUrl: externalURL, externalUrl: externalURL,
......
window.timeago = require('timeago.js'); window.timeago = require('timeago.js');
require('~/environments/components/environment_item'); const EnvironmentItem = require('~/environments/components/environment_item');
describe('Environment item', () => { describe('Environment item', () => {
preloadFixtures('static/environments/table.html.raw'); preloadFixtures('static/environments/table.html.raw');
...@@ -14,33 +14,16 @@ describe('Environment item', () => { ...@@ -14,33 +14,16 @@ describe('Environment item', () => {
beforeEach(() => { beforeEach(() => {
mockItem = { mockItem = {
name: 'review', name: 'review',
children: [ folderName: 'review',
{ size: 3,
name: 'review-app', isFolder: true,
id: 1, environment_path: 'url',
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
{
name: 'production',
id: 2,
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
],
}; };
component = new window.gl.environmentsList.EnvironmentItem({ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'), el: document.querySelector('tr#environment-row'),
propsData: { propsData: {
model: mockItem, model: mockItem,
toggleRow: () => {},
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true, canReadEnvironment: true,
}, },
...@@ -53,7 +36,7 @@ describe('Environment item', () => { ...@@ -53,7 +36,7 @@ describe('Environment item', () => {
}); });
it('Should render the number of children in a badge', () => { it('Should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
}); });
}); });
...@@ -63,8 +46,8 @@ describe('Environment item', () => { ...@@ -63,8 +46,8 @@ describe('Environment item', () => {
beforeEach(() => { beforeEach(() => {
environment = { environment = {
id: 31,
name: 'production', name: 'production',
size: 1,
state: 'stopped', state: 'stopped',
external_url: 'http://external.com', external_url: 'http://external.com',
environment_type: null, environment_type: null,
...@@ -125,11 +108,10 @@ describe('Environment item', () => { ...@@ -125,11 +108,10 @@ describe('Environment item', () => {
updated_at: '2016-11-10T15:55:58.778Z', updated_at: '2016-11-10T15:55:58.778Z',
}; };
component = new window.gl.environmentsList.EnvironmentItem({ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'), el: document.querySelector('tr#environment-row'),
propsData: { propsData: {
model: environment, model: environment,
toggleRow: () => {},
canCreateDeployment: true, canCreateDeployment: true,
canReadEnvironment: true, canReadEnvironment: true,
}, },
......
require('~/environments/components/environment_rollback'); const RollbackComponent = require('~/environments/components/environment_rollback');
describe('Rollback Component', () => { describe('Rollback Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -10,7 +10,7 @@ describe('Rollback Component', () => { ...@@ -10,7 +10,7 @@ describe('Rollback Component', () => {
}); });
it('Should link to the provided retryUrl', () => { it('Should link to the provided retryUrl', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
...@@ -22,7 +22,7 @@ describe('Rollback Component', () => { ...@@ -22,7 +22,7 @@ describe('Rollback Component', () => {
}); });
it('Should render Re-deploy label when isLastDeployment is true', () => { it('Should render Re-deploy label when isLastDeployment is true', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
...@@ -34,7 +34,7 @@ describe('Rollback Component', () => { ...@@ -34,7 +34,7 @@ describe('Rollback Component', () => {
}); });
it('Should render Rollback label when isLastDeployment is false', () => { it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
......
/* global Vue, environment */ const Vue = require('vue');
require('~/flash'); require('~/flash');
require('~/environments/stores/environments_store'); const EnvironmentsComponent = require('~/environments/components/environment');
require('~/environments/components/environment'); const { environment } = require('./mock_data');
require('./mock_data');
describe('Environment', () => { describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw'); preloadFixtures('static/environments/environments.html.raw');
...@@ -33,11 +31,8 @@ describe('Environment', () => { ...@@ -33,11 +31,8 @@ describe('Environment', () => {
}); });
it('should render the empty state', (done) => { it('should render the empty state', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
}); });
setTimeout(() => { setTimeout(() => {
...@@ -54,15 +49,30 @@ describe('Environment', () => { ...@@ -54,15 +49,30 @@ describe('Environment', () => {
}); });
}); });
describe('with environments', () => { describe('with paginated environments', () => {
const environmentsResponseInterceptor = (request, next) => { const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([environment]), { next(request.respondWith(JSON.stringify({
environments: [environment],
stopped_count: 1,
available_count: 0,
}), {
status: 200, status: 200,
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
})); }));
}; };
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -72,13 +82,6 @@ describe('Environment', () => { ...@@ -72,13 +82,6 @@ describe('Environment', () => {
}); });
it('should render a table with environments', (done) => { it('should render a table with environments', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
});
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelectorAll('table tbody tr').length, component.$el.querySelectorAll('table tbody tr').length,
...@@ -86,6 +89,59 @@ describe('Environment', () => { ...@@ -86,6 +89,59 @@ describe('Environment', () => {
done(); done();
}, 0); }, 0);
}); });
describe('pagination', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
done();
}, 0);
});
it('should update url when no search params are present', (done) => {
spyOn(gl.utils, 'visitUrl');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page is already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page and scope are already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1&scope=all');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done();
}, 0);
});
});
}); });
}); });
...@@ -107,11 +163,8 @@ describe('Environment', () => { ...@@ -107,11 +163,8 @@ describe('Environment', () => {
}); });
it('should render empty state', (done) => { it('should render empty state', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
}); });
setTimeout(() => { setTimeout(() => {
......
require('~/environments/components/environment_stop'); const StopComponent = require('~/environments/components/environment_stop');
describe('Stop Component', () => { describe('Stop Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -10,7 +10,7 @@ describe('Stop Component', () => { ...@@ -10,7 +10,7 @@ describe('Stop Component', () => {
loadFixtures('static/environments/element.html.raw'); loadFixtures('static/environments/element.html.raw');
stopURL = '/stop'; stopURL = '/stop';
component = new window.gl.environmentsList.StopComponent({ component = new StopComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
stopUrl: stopURL, stopUrl: stopURL,
......
const EnvironmentTable = require('~/environments/components/environments_table');
describe('Environment item', () => {
preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
loadFixtures('static/environments/element.html.raw');
});
it('Should render a table', () => {
const mockItem = {
name: 'review',
size: 3,
isFolder: true,
latest: {
environment_path: 'url',
},
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [{ mockItem }],
canCreateDeployment: false,
canReadEnvironment: true,
},
});
expect(component.$el.tagName).toEqual('TABLE');
});
});
/* global environmentsList */ const Store = require('~/environments/stores/environments_store');
const { environmentsList, serverData } = require('./mock_data');
require('~/environments/stores/environments_store');
require('./mock_data');
(() => { (() => {
describe('Store', () => { describe('Store', () => {
let store;
beforeEach(() => { beforeEach(() => {
gl.environmentsList.EnvironmentsStore.create(); store = new Store();
}); });
it('should start with a blank state', () => { it('should start with a blank state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); expect(store.state.environments.length).toEqual(0);
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); expect(store.state.stoppedCounter).toEqual(0);
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); expect(store.state.availableCounter).toEqual(0);
expect(store.state.paginationInformation).toEqual({});
}); });
describe('store environments', () => { it('should store environments', () => {
beforeEach(() => { store.storeEnvironments(serverData);
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); expect(store.state.environments.length).toEqual(serverData.length);
}); expect(store.state.environments[0]).toEqual(environmentsList[0]);
it('should count stopped environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
});
it('should count available environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
});
it('should store environments with same environment_type as sibilings', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
.filter(env => env.children && env.children.length > 0);
expect(parentFolder[0].children.length).toBe(2);
expect(parentFolder[0].children[0].environment_type).toBe('review');
expect(parentFolder[0].children[1].environment_type).toBe('review');
expect(parentFolder[0].children[0].name).toBe('test-environment');
expect(parentFolder[0].children[1].name).toBe('test-environment-1');
});
it('should sort the environments alphabetically', () => {
const { environments } = gl.environmentsList.EnvironmentsStore.state;
expect(environments[0].name).toBe('production');
expect(environments[1].name).toBe('review');
expect(environments[1].children[0].name).toBe('test-environment');
expect(environments[1].children[1].name).toBe('test-environment-1');
expect(environments[2].name).toBe('review_app');
});
}); });
describe('toggleFolder', () => { it('should store available count', () => {
beforeEach(() => { store.storeAvailableCount(2);
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); expect(store.state.availableCounter).toEqual(2);
}); });
it('should toggle the open property for the given environment', () => {
gl.environmentsList.EnvironmentsStore.toggleFolder('review');
const { environments } = gl.environmentsList.EnvironmentsStore.state; it('should store stopped count', () => {
const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); store.storeStoppedCount(2);
expect(store.state.stoppedCounter).toEqual(2);
});
expect(environment[0].isOpen).toBe(true); it('should store pagination information', () => {
}); const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '2',
'X-TOTAL': '37',
'X-Total-Pages': '2',
};
const expectedResult = {
perPage: 1,
page: 1,
total: 37,
totalPages: 2,
nextPage: 2,
previousPage: 2,
};
store.setPagination(pagination);
expect(store.state.paginationInformation).toEqual(expectedResult);
}); });
}); });
})(); })();
const Vue = require('vue');
require('~/flash');
const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
const { environmentsList } = require('../mock_data');
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
window.history.pushState({}, null, 'environments/folders/build');
});
let component;
describe('successfull request', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
environments: environmentsList,
stopped_count: 1,
available_count: 0,
}), {
status: 200,
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsResponseInterceptor,
);
});
it('should render a table with environments', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('table tbody tr').length,
).toEqual(2);
done();
}, 0);
});
it('should render available tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('1');
done();
}, 0);
});
it('should render parent folder name', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-folder-name').textContent,
).toContain('Environments / build');
done();
}, 0);
});
describe('pagination', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
done();
}, 0);
});
it('should update url when no search params are present', (done) => {
spyOn(gl.utils, 'visitUrl');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page is already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page and scope are already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1&scope=all');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done();
}, 0);
});
});
});
describe('unsuccessfull request', () => {
const environmentsErrorResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsErrorResponseInterceptor,
);
});
it('should not render a table', (done) => {
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
setTimeout(() => {
expect(
component.$el.querySelector('table'),
).toBe(null);
done();
}, 0);
});
it('should render available tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
});
});
const environmentsList = [ const environmentsList = [
{ {
id: 31, name: 'DEV',
name: 'production', size: 1,
id: 7,
state: 'available', state: 'available',
external_url: 'https://www.gitlab.com', external_url: null,
environment_type: null,
last_deployment: {
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
{
id: 32,
name: 'review_app',
state: 'stopped',
external_url: 'https://www.gitlab.com',
environment_type: null, environment_type: null,
last_deployment: { last_deployment: null,
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': false, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/7',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/7/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
}, },
{ {
id: 33, folderName: 'build',
name: 'test-environment', size: 5,
id: 12,
name: 'build/update-README',
state: 'available', state: 'available',
environment_type: 'review', external_url: null,
environment_type: 'build',
last_deployment: null, last_deployment: null,
'stop_action?': true, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/12',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/12/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
}, },
];
const serverData = [
{ {
id: 34, name: 'DEV',
name: 'test-environment-1', size: 1,
state: 'available', latest: {
environment_type: 'review', id: 7,
last_deployment: null, name: 'DEV',
'stop_action?': true, state: 'available',
environment_path: '/root/ci-folders/environments/31', external_url: null,
created_at: '2016-11-07T11:11:16.525Z', environment_type: null,
updated_at: '2016-11-07T11:11:16.525Z', last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
},
{
name: 'build',
size: 5,
latest: {
id: 12,
name: 'build/update-README',
state: 'available',
external_url: null,
environment_type: 'build',
last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/12',
stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
},
}, },
]; ];
window.environmentsList = environmentsList;
const environment = { const environment = {
id: 4, name: 'DEV',
name: 'production', size: 1,
state: 'available', latest: {
external_url: 'http://production.', id: 7,
environment_type: null, name: 'DEV',
last_deployment: {}, state: 'available',
'stop_action?': false, external_url: null,
environment_path: '/root/review-app/environments/4', environment_type: null,
stop_path: '/root/review-app/environments/4/stop', last_deployment: null,
created_at: '2016-12-16T11:51:04.690Z', 'stop_action?': false,
updated_at: '2016-12-16T12:04:51.133Z', environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
}; };
window.environment = environment; module.exports = {
environmentsList,
environment,
serverData,
};
%div
#environments-folder-list-view{ data: { "can-create-deployment" => "true",
"can-read-environment" => "true",
"css-class" => "",
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play") } }
...@@ -105,6 +105,7 @@ require('~/issue'); ...@@ -105,6 +105,7 @@ require('~/issue');
expectIssueState(false); expectIssueState(false);
expect($btnClose).toHaveProp('disabled', false); expect($btnClose).toHaveProp('disabled', false);
expect($('.issue_counter')).toHaveText(0);
}); });
it('fails to close an issue with success:false', function() { it('fails to close an issue with success:false', function() {
...@@ -121,6 +122,7 @@ require('~/issue'); ...@@ -121,6 +122,7 @@ require('~/issue');
expectIssueState(true); expectIssueState(true);
expect($btnClose).toHaveProp('disabled', false); expect($btnClose).toHaveProp('disabled', false);
expectErrorMessage(); expectErrorMessage();
expect($('.issue_counter')).toHaveText(1);
}); });
it('fails to closes an issue with HTTP error', function() { it('fails to closes an issue with HTTP error', function() {
...@@ -135,6 +137,7 @@ require('~/issue'); ...@@ -135,6 +137,7 @@ require('~/issue');
expectIssueState(true); expectIssueState(true);
expect($btnClose).toHaveProp('disabled', true); expect($btnClose).toHaveProp('disabled', true);
expectErrorMessage(); expectErrorMessage();
expect($('.issue_counter')).toHaveText(1);
}); });
}); });
...@@ -159,6 +162,7 @@ require('~/issue'); ...@@ -159,6 +162,7 @@ require('~/issue');
expectIssueState(true); expectIssueState(true);
expect($btnReopen).toHaveProp('disabled', false); expect($btnReopen).toHaveProp('disabled', false);
expect($('.issue_counter')).toHaveText(1);
}); });
}); });
}).call(this); }).call(this);
...@@ -108,6 +108,30 @@ require('~/lib/utils/common_utils'); ...@@ -108,6 +108,30 @@ require('~/lib/utils/common_utils');
}); });
}); });
describe('gl.utils.parseIntPagination', () => {
it('should parse to integers all string values and return pagination object', () => {
const pagination = {
'X-PER-PAGE': 10,
'X-PAGE': 2,
'X-TOTAL': 30,
'X-TOTAL-PAGES': 3,
'X-NEXT-PAGE': 3,
'X-PREV-PAGE': 1,
};
const expectedPagination = {
perPage: 10,
page: 2,
total: 30,
totalPages: 3,
nextPage: 3,
previousPage: 1,
};
expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination);
});
});
describe('gl.utils.isMetaClick', () => { describe('gl.utils.isMetaClick', () => {
it('should identify meta click on Windows/Linux', () => { it('should identify meta click on Windows/Linux', () => {
const e = { const e = {
......
...@@ -34,7 +34,7 @@ describe('Pagination component', () => { ...@@ -34,7 +34,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '1' } }); component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the previous page', () => { it('should go to the previous page', () => {
...@@ -55,7 +55,7 @@ describe('Pagination component', () => { ...@@ -55,7 +55,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Prev' } }); component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the next page', () => { it('should go to the next page', () => {
...@@ -76,7 +76,7 @@ describe('Pagination component', () => { ...@@ -76,7 +76,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Next' } }); component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5); expect(changeChanges.one).toEqual(5);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the last page', () => { it('should go to the last page', () => {
...@@ -97,7 +97,7 @@ describe('Pagination component', () => { ...@@ -97,7 +97,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Last >>' } }); component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10); expect(changeChanges.one).toEqual(10);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the first page', () => { it('should go to the first page', () => {
...@@ -118,7 +118,7 @@ describe('Pagination component', () => { ...@@ -118,7 +118,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '<< First' } }); component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should do nothing', () => { it('should do nothing', () => {
...@@ -139,7 +139,7 @@ describe('Pagination component', () => { ...@@ -139,7 +139,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '...' } }); component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
}); });
......
require 'spec_helper'
describe AdditionalEmailHeadersInterceptor do
it 'adds Auto-Submitted header' do
mail = ActionMailer::Base.mail(to: 'test@mail.com', from: 'info@mail.com', body: 'hello').deliver
expect(mail.header['To'].value).to eq('test@mail.com')
expect(mail.header['From'].value).to eq('info@mail.com')
expect(mail.header['Auto-Submitted'].value).to eq('auto-generated')
expect(mail.header['X-Auto-Response-Suppress'].value).to eq('All')
end
end
{
"description": "Nisi et repellendus ut enim quo accusamus vel magnam.",
"visibility_level": 10,
"archived": false,
"labels": [
{
"id": 2,
"title": "test2",
"color": "#428bca",
"project_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"type": "ProjectLabel",
"priorities": [
]
},
{
"id": 3,
"title": "test3",
"color": "#428bca",
"group_id": 8,
"created_at": "2016-07-22T08:55:44.161Z",
"updated_at": "2016-07-22T08:55:44.161Z",
"template": false,
"description": "",
"project_id": null,
"type": "GroupLabel",
"priorities": [
{
"id": 1,
"project_id": 5,
"label_id": 1,
"priority": 1,
"created_at": "2016-10-18T09:35:43.338Z",
"updated_at": "2016-10-18T09:35:43.338Z"
}
]
}
],
"snippets": [
],
"hooks": [
]
}
\ No newline at end of file
...@@ -3,24 +3,28 @@ include ImportExport::CommonUtil ...@@ -3,24 +3,28 @@ include ImportExport::CommonUtil
describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
describe 'restore project tree' do describe 'restore project tree' do
let(:user) { create(:user) } before(:all) do
let(:namespace) { create(:namespace, owner: user) } @user = create(:user)
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
before do RSpec::Mocks.with_temporary_scope do
allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/') @shared = Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path')
allow(@shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
@project = create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project')
project_tree_restorer = described_class.new(user: @user, shared: @shared, project: @project)
@restored_project_json = project_tree_restorer.restore
end
end
after(:all) do
@user.destroy!
end end
context 'JSON' do context 'JSON' do
it 'restores models based on JSON' do it 'restores models based on JSON' do
expect(restored_project_json).to be true expect(@restored_project_json).to be true
end end
it 'restore correct project features' do it 'restore correct project features' do
restored_project_json
project = Project.find_by_path('project') project = Project.find_by_path('project')
expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED) expect(project.project_feature.issues_access_level).to eq(ProjectFeature::DISABLED)
...@@ -31,62 +35,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -31,62 +35,42 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end end
it 'has the same label associated to two issues' do it 'has the same label associated to two issues' do
restored_project_json
expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2) expect(ProjectLabel.find_by_title('test2').issues.count).to eq(2)
end end
it 'has milestones associated to two separate issues' do it 'has milestones associated to two separate issues' do
restored_project_json
expect(Milestone.find_by_description('test milestone').issues.count).to eq(2) expect(Milestone.find_by_description('test milestone').issues.count).to eq(2)
end end
it 'creates a valid pipeline note' do it 'creates a valid pipeline note' do
restored_project_json
expect(Ci::Pipeline.first.notes).not_to be_empty expect(Ci::Pipeline.first.notes).not_to be_empty
end end
it 'restores pipelines with missing ref' do it 'restores pipelines with missing ref' do
restored_project_json
expect(Ci::Pipeline.where(ref: nil)).not_to be_empty expect(Ci::Pipeline.where(ref: nil)).not_to be_empty
end end
it 'restores the correct event with symbolised data' do it 'restores the correct event with symbolised data' do
restored_project_json
expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty expect(Event.where.not(data: nil).first.data[:ref]).not_to be_empty
end end
it 'preserves updated_at on issues' do it 'preserves updated_at on issues' do
restored_project_json
issue = Issue.where(description: 'Aliquam enim illo et possimus.').first issue = Issue.where(description: 'Aliquam enim illo et possimus.').first
expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC') expect(issue.reload.updated_at.to_s).to eq('2016-06-14 15:02:47 UTC')
end end
it 'contains the merge access levels on a protected branch' do it 'contains the merge access levels on a protected branch' do
restored_project_json
expect(ProtectedBranch.first.merge_access_levels).not_to be_empty expect(ProtectedBranch.first.merge_access_levels).not_to be_empty
end end
it 'contains the push access levels on a protected branch' do it 'contains the push access levels on a protected branch' do
restored_project_json
expect(ProtectedBranch.first.push_access_levels).not_to be_empty expect(ProtectedBranch.first.push_access_levels).not_to be_empty
end end
context 'event at forth level of the tree' do context 'event at forth level of the tree' do
let(:event) { Event.where(title: 'test levels').first } let(:event) { Event.where(title: 'test levels').first }
before do
restored_project_json
end
it 'restores the event' do it 'restores the event' do
expect(event).not_to be_nil expect(event).not_to be_nil
end end
...@@ -99,77 +83,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -99,77 +83,40 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
it 'has the correct data for merge request st_diffs' do it 'has the correct data for merge request st_diffs' do
# makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+ # makes sure we are renaming the custom method +utf8_st_diffs+ into +st_diffs+
expect { restored_project_json }.to change(MergeRequestDiff.where.not(st_diffs: nil), :count).by(9) expect(MergeRequestDiff.where.not(st_diffs: nil).count).to eq(9)
end end
it 'has labels associated to label links, associated to issues' do it 'has labels associated to label links, associated to issues' do
restored_project_json
expect(Label.first.label_links.first.target).not_to be_nil expect(Label.first.label_links.first.target).not_to be_nil
end end
it 'has project labels' do it 'has project labels' do
restored_project_json
expect(ProjectLabel.count).to eq(2) expect(ProjectLabel.count).to eq(2)
end end
it 'has no group labels' do it 'has no group labels' do
restored_project_json
expect(GroupLabel.count).to eq(0) expect(GroupLabel.count).to eq(0)
end end
context 'with group' do
let!(:project) do
create(:empty_project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group))
end
it 'has group labels' do
restored_project_json
expect(GroupLabel.count).to eq(1)
end
it 'has label priorities' do
restored_project_json
expect(GroupLabel.first.priorities).not_to be_empty
end
end
it 'has a project feature' do it 'has a project feature' do
restored_project_json expect(@project.project_feature).not_to be_nil
expect(project.project_feature).not_to be_nil
end end
it 'restores the correct service' do it 'restores the correct service' do
restored_project_json
expect(CustomIssueTrackerService.first).not_to be_nil expect(CustomIssueTrackerService.first).not_to be_nil
end end
context 'Merge requests' do context 'Merge requests' do
before do
restored_project_json
end
it 'always has the new project as a target' do it 'always has the new project as a target' do
expect(MergeRequest.find_by_title('MR1').target_project).to eq(project) expect(MergeRequest.find_by_title('MR1').target_project).to eq(@project)
end end
it 'has the same source project as originally if source/target are the same' do it 'has the same source project as originally if source/target are the same' do
expect(MergeRequest.find_by_title('MR1').source_project).to eq(project) expect(MergeRequest.find_by_title('MR1').source_project).to eq(@project)
end end
it 'has the new project as target if source/target differ' do it 'has the new project as target if source/target differ' do
expect(MergeRequest.find_by_title('MR2').target_project).to eq(project) expect(MergeRequest.find_by_title('MR2').target_project).to eq(@project)
end end
it 'has no source if source/target differ' do it 'has no source if source/target differ' do
...@@ -177,39 +124,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do ...@@ -177,39 +124,71 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
end end
end end
context 'project.json file access check' do context 'tokens are regenerated' do
it 'does not read a symlink' do it 'has a new CI trigger token' do
Dir.mktmpdir do |tmpdir| expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
setup_symlink(tmpdir, 'project.json') end
allow(shared).to receive(:export_path).and_call_original
restored_project_json
expect(shared.errors.first).not_to include('test') it 'has a new CI build token' do
end expect(Ci::Build.where(token: 'abcd')).to be_empty
end end
end end
end
end
context 'when there is an existing build with build token' do context 'Light JSON' do
it 'restores project json correctly' do let(:user) { create(:user) }
create(:ci_build, token: 'abcd') let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: "", project_path: 'path') }
let!(:project) { create(:empty_project, :builds_disabled, :issues_disabled, name: 'project', path: 'project') }
let(:project_tree_restorer) { described_class.new(user: user, shared: shared, project: project) }
let(:restored_project_json) { project_tree_restorer.restore }
expect(restored_project_json).to be true before do
end allow(ImportExport).to receive(:project_filename).and_return('project.light.json')
end allow(shared).to receive(:export_path).and_return('spec/lib/gitlab/import_export/')
end
context 'project.json file access check' do
it 'does not read a symlink' do
Dir.mktmpdir do |tmpdir|
setup_symlink(tmpdir, 'project.json')
allow(shared).to receive(:export_path).and_call_original
context 'tokens are regenerated' do
before do
restored_project_json restored_project_json
end
it 'has a new CI trigger token' do expect(shared.errors.first).not_to include('test')
expect(Ci::Trigger.where(token: 'cdbfasdf44a5958c83654733449e585')).to be_empty
end end
end
end
it 'has a new CI build token' do context 'when there is an existing build with build token' do
expect(Ci::Build.where(token: 'abcd')).to be_empty it 'restores project json correctly' do
end create(:ci_build, token: 'abcd')
expect(restored_project_json).to be true
end
end
context 'with group' do
let!(:project) do
create(:empty_project,
:builds_disabled,
:issues_disabled,
name: 'project',
path: 'project',
group: create(:group))
end
before do
restored_project_json
end
it 'has group labels' do
expect(GroupLabel.count).to eq(1)
end
it 'has label priorities' do
expect(GroupLabel.first.priorities).not_to be_empty
end end
end end
end end
......
require 'spec_helper'
describe Gitlab::ImportExport::RepoRestorer, services: true do
describe 'bundle a project Git repo' do
let(:user) { create(:user) }
let!(:project_with_repo) { create(:project, :test_repo, name: 'test-repo-restorer', path: 'test-repo-restorer') }
let!(:project) { create(:empty_project) }
let(:export_path) { "#{Dir.tmpdir}/project_tree_saver_spec" }
let(:shared) { Gitlab::ImportExport::Shared.new(relative_path: project.path_with_namespace) }
let(:bundler) { Gitlab::ImportExport::RepoSaver.new(project: project_with_repo, shared: shared) }
let(:bundle_path) { File.join(shared.export_path, Gitlab::ImportExport.project_bundle_filename) }
let(:restorer) do
described_class.new(path_to_bundle: bundle_path,
shared: shared,
project: project)
end
before do
allow_any_instance_of(Gitlab::ImportExport).to receive(:storage_path).and_return(export_path)
bundler.save
end
after do
FileUtils.rm_rf(export_path)
FileUtils.rm_rf(project_with_repo.repository.path_to_repo)
FileUtils.rm_rf(project.repository.path_to_repo)
end
it 'restores the repo successfully' do
expect(restorer.restore).to be true
end
it 'has the webhooks' do
restorer.restore
expect(Gitlab::Git::Hook.new('post-receive', project.repository.path_to_repo)).to exist
end
end
end
...@@ -474,6 +474,20 @@ describe API::Commits, api: true do ...@@ -474,6 +474,20 @@ describe API::Commits, api: true do
expect(response).to have_http_status(401) expect(response).to have_http_status(401)
end end
end end
context 'when the commit is present on two projects' do
let(:forked_project) { create(:project, :repository, creator: user2, namespace: user2.namespace) }
let!(:forked_project_note) { create(:note_on_commit, author: user2, project: forked_project, commit_id: forked_project.repository.commit.id, note: 'a comment on a commit for fork') }
it 'returns the comments for the target project' do
get api("/projects/#{forked_project.id}/repository/commits/#{forked_project.repository.commit.id}/comments", user2)
expect(response).to have_http_status(200)
expect(json_response.length).to eq(1)
expect(json_response.first['note']).to eq('a comment on a commit for fork')
expect(json_response.first['author']['id']).to eq(user2.id)
end
end
end end
describe 'POST :id/repository/commits/:sha/cherry_pick' do describe 'POST :id/repository/commits/:sha/cherry_pick' do
......
...@@ -181,6 +181,17 @@ describe EnvironmentSerializer do ...@@ -181,6 +181,17 @@ describe EnvironmentSerializer do
expect(subject.first[:name]).to eq 'production' expect(subject.first[:name]).to eq 'production'
expect(subject.second[:name]).to eq 'staging' expect(subject.second[:name]).to eq 'staging'
end end
it 'appends correct total page count header' do
expect(subject).not_to be_empty
expect(response).to have_received(:[]=).with('X-Total', '3')
end
it 'appends correct page count headers' do
expect(subject).not_to be_empty
expect(response).to have_received(:[]=).with('X-Total-Pages', '2')
expect(response).to have_received(:[]=).with('X-Per-Page', '2')
end
end end
end end
end end
......
...@@ -9,14 +9,18 @@ describe Groups::DestroyService, services: true do ...@@ -9,14 +9,18 @@ describe Groups::DestroyService, services: true do
let!(:gitlab_shell) { Gitlab::Shell.new } let!(:gitlab_shell) { Gitlab::Shell.new }
let!(:remove_path) { group.path + "+#{group.id}+deleted" } let!(:remove_path) { group.path + "+#{group.id}+deleted" }
before do
group.add_user(user, Gitlab::Access::OWNER)
end
shared_examples 'group destruction' do |async| shared_examples 'group destruction' do |async|
context 'database records' do context 'database records' do
before do before do
destroy_group(group, user, async) destroy_group(group, user, async)
end end
it { expect(Group.all).not_to include(group) } it { expect(Group.unscoped.all).not_to include(group) }
it { expect(Project.all).not_to include(project) } it { expect(Project.unscoped.all).not_to include(project) }
end end
context 'file system' do context 'file system' do
...@@ -32,7 +36,7 @@ describe Groups::DestroyService, services: true do ...@@ -32,7 +36,7 @@ describe Groups::DestroyService, services: true do
context 'Sidekiq fake' do context 'Sidekiq fake' do
before do before do
# Dont run sidekiq to check if renamed repository exists # Don't run sidekiq to check if renamed repository exists
Sidekiq::Testing.fake! { destroy_group(group, user, async) } Sidekiq::Testing.fake! { destroy_group(group, user, async) }
end end
...@@ -95,4 +99,13 @@ describe Groups::DestroyService, services: true do ...@@ -95,4 +99,13 @@ describe Groups::DestroyService, services: true do
describe 'synchronous delete' do describe 'synchronous delete' do
it_behaves_like 'group destruction', false it_behaves_like 'group destruction', false
end end
context 'projects in pending_delete' do
before do
project.pending_delete = true
project.save
end
it_behaves_like 'group destruction', false
end
end end
...@@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do ...@@ -149,35 +149,46 @@ describe MergeRequests::MergeService, services: true do
context "error handling" do context "error handling" do
let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') } let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
it 'saves error if there is an exception' do before do
allow(service).to receive(:repository).and_raise("error message") allow(Rails.logger).to receive(:error)
end
it 'logs and saves error if there is an exception' do
error_message = 'error message'
allow(service).to receive(:repository).and_raise("error message")
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
service.execute(merge_request) service.execute(merge_request)
expect(merge_request.merge_error).to eq("Something went wrong during merge: error message") expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
it 'saves error if there is an PreReceiveError exception' do it 'logs and saves error if there is an PreReceiveError exception' do
allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, "error") error_message = 'error message'
allow(service).to receive(:repository).and_raise(GitHooksService::PreReceiveError, error_message)
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
service.execute(merge_request) service.execute(merge_request)
expect(merge_request.merge_error).to eq("error") expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
it 'aborts if there is a merge conflict' do it 'logs and saves error if there is a merge conflict' do
error_message = 'Conflicts detected during merge'
allow_any_instance_of(Repository).to receive(:merge).and_return(false) allow_any_instance_of(Repository).to receive(:merge).and_return(false)
allow(service).to receive(:execute_hooks) allow(service).to receive(:execute_hooks)
service.execute(merge_request) service.execute(merge_request)
expect(merge_request.open?).to be_truthy expect(merge_request).to be_open
expect(merge_request.merge_commit_sha).to be_nil expect(merge_request.merge_commit_sha).to be_nil
expect(merge_request.merge_error).to eq("Conflicts detected during merge") expect(merge_request.merge_error).to include(error_message)
expect(Rails.logger).to have_received(:error).with(a_string_matching(error_message))
end end
end end
end end
......
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1 # yarn lockfile v1
abbrev@1, abbrev@1.0.x: abbrev@1, abbrev@1.0.x:
version "1.0.9" version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
accepts@~1.3.3, accepts@1.3.3: accepts@1.3.3, accepts@~1.3.3:
version "1.3.3" version "1.3.3"
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.3.tgz#c3ca7434938648c3e0d9c1e328dd68b622c284ca"
dependencies: dependencies:
...@@ -23,14 +25,14 @@ acorn-jsx@^3.0.0: ...@@ -23,14 +25,14 @@ acorn-jsx@^3.0.0:
dependencies: dependencies:
acorn "^3.0.4" acorn "^3.0.4"
acorn@4.0.4, acorn@^4.0.3, acorn@^4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
acorn@^3.0.4: acorn@^3.0.4:
version "3.3.0" version "3.3.0"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a" resolved "https://registry.yarnpkg.com/acorn/-/acorn-3.3.0.tgz#45e37fb39e8da3f25baee3ff5369e2bb5f22017a"
acorn@^4.0.3, acorn@^4.0.4, acorn@4.0.4:
version "4.0.4"
resolved "https://registry.yarnpkg.com/acorn/-/acorn-4.0.4.tgz#17a8d6a7a6c4ef538b814ec9abac2779293bf30a"
after@0.8.2: after@0.8.2:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f" resolved "https://registry.yarnpkg.com/after/-/after-0.8.2.tgz#fedb394f9f0e02aa9768e702bda23b505fae7e1f"
...@@ -178,7 +180,11 @@ async-each@^1.0.0: ...@@ -178,7 +180,11 @@ async-each@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d" resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
async@^1.4.0, async@^1.4.2, async@^1.5.2, async@1.x: async@0.2.x, async@~0.2.6:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
async@1.x, async@^1.4.0, async@^1.4.2, async@^1.5.2:
version "1.5.2" version "1.5.2"
resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a" resolved "https://registry.yarnpkg.com/async/-/async-1.5.2.tgz#ec6a61ae56480c0c3cb241c95618e20892f9672a"
...@@ -188,10 +194,6 @@ async@^2.1.2, async@^2.1.4: ...@@ -188,10 +194,6 @@ async@^2.1.2, async@^2.1.4:
dependencies: dependencies:
lodash "^4.14.0" lodash "^4.14.0"
async@~0.2.6, async@0.2.x:
version "0.2.10"
resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1"
async@~0.9.0: async@~0.9.0:
version "0.9.2" version "0.9.2"
resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d" resolved "https://registry.yarnpkg.com/async/-/async-0.9.2.tgz#aea74d5e61c1f899613bf64bda66d4c78f2fd17d"
...@@ -995,7 +997,7 @@ center-align@^0.1.1: ...@@ -995,7 +997,7 @@ center-align@^0.1.1:
align-text "^0.1.3" align-text "^0.1.3"
lazy-cache "^1.0.3" lazy-cache "^1.0.3"
chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3: chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
version "1.1.3" version "1.1.3"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
dependencies: dependencies:
...@@ -1140,14 +1142,6 @@ concat-map@0.0.1: ...@@ -1140,14 +1142,6 @@ concat-map@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
concat-stream@^1.4.6:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
concat-stream@1.5.0: concat-stream@1.5.0:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611" resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.5.0.tgz#53f7d43c51c5e43f81c8fdd03321c631be68d611"
...@@ -1156,6 +1150,14 @@ concat-stream@1.5.0: ...@@ -1156,6 +1150,14 @@ concat-stream@1.5.0:
readable-stream "~2.0.0" readable-stream "~2.0.0"
typedarray "~0.0.5" typedarray "~0.0.5"
concat-stream@^1.4.6:
version "1.6.0"
resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-1.6.0.tgz#0aac662fd52be78964d5532f694784e70110acf7"
dependencies:
inherits "^2.0.3"
readable-stream "^2.2.2"
typedarray "^0.0.6"
connect-history-api-fallback@^1.3.0: connect-history-api-fallback@^1.3.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169" resolved "https://registry.yarnpkg.com/connect-history-api-fallback/-/connect-history-api-fallback-1.3.0.tgz#e51d17f8f0ef0db90a64fdb47de3051556e9f169"
...@@ -1263,16 +1265,16 @@ custom-event@~1.0.0: ...@@ -1263,16 +1265,16 @@ custom-event@~1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425" resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
d3@3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
d@^0.1.1, d@~0.1.1: d@^0.1.1, d@~0.1.1:
version "0.1.1" version "0.1.1"
resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309" resolved "https://registry.yarnpkg.com/d/-/d-0.1.1.tgz#da184c535d18d8ee7ba2aa229b914009fae11309"
dependencies: dependencies:
es5-ext "~0.10.2" es5-ext "~0.10.2"
d3@3.5.11:
version "3.5.11"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
dashdash@^1.12.0: dashdash@^1.12.0:
version "1.14.1" version "1.14.1"
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0" resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
...@@ -1283,28 +1285,28 @@ date-now@^0.1.4: ...@@ -1283,28 +1285,28 @@ date-now@^0.1.4:
version "0.1.4" version "0.1.4"
resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b" resolved "https://registry.yarnpkg.com/date-now/-/date-now-0.1.4.tgz#eaf439fd4d4848ad74e5cc7dbef200672b9e345b"
debug@^2.1.1, debug@^2.2.0, debug@2.6.0: debug@0.7.4:
version "2.6.0" version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b" resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
dependencies:
ms "0.7.2"
debug@~2.2.0, debug@2.2.0: debug@2.2.0, debug@~2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da" resolved "https://registry.yarnpkg.com/debug/-/debug-2.2.0.tgz#f87057e995b1a1f6ae6a4960664137bc56f039da"
dependencies: dependencies:
ms "0.7.1" ms "0.7.1"
debug@0.7.4:
version "0.7.4"
resolved "https://registry.yarnpkg.com/debug/-/debug-0.7.4.tgz#06e1ea8082c2cb14e39806e22e2f6f757f92af39"
debug@2.3.3: debug@2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c" resolved "https://registry.yarnpkg.com/debug/-/debug-2.3.3.tgz#40c453e67e6e13c901ddec317af8986cda9eff8c"
dependencies: dependencies:
ms "0.7.2" ms "0.7.2"
debug@2.6.0, debug@^2.1.1, debug@^2.2.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.0.tgz#bc596bcabe7617f11d9fa15361eded5608b8499b"
dependencies:
ms "0.7.2"
decamelize@^1.0.0, decamelize@^1.1.1: decamelize@^1.0.0, decamelize@^1.1.1:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
...@@ -1382,7 +1384,7 @@ diffie-hellman@^5.0.0: ...@@ -1382,7 +1384,7 @@ diffie-hellman@^5.0.0:
miller-rabin "^4.0.0" miller-rabin "^4.0.0"
randombytes "^2.0.0" randombytes "^2.0.0"
doctrine@^1.2.2, doctrine@1.5.0: doctrine@1.5.0, doctrine@^1.2.2:
version "1.5.0" version "1.5.0"
resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" resolved "https://registry.yarnpkg.com/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa"
dependencies: dependencies:
...@@ -1545,7 +1547,7 @@ es6-set@~0.1.3: ...@@ -1545,7 +1547,7 @@ es6-set@~0.1.3:
es6-symbol "3" es6-symbol "3"
event-emitter "~0.3.4" event-emitter "~0.3.4"
es6-symbol@~3.1, es6-symbol@~3.1.0, es6-symbol@3: es6-symbol@3, es6-symbol@~3.1, es6-symbol@~3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa" resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.0.tgz#94481c655e7a7cad82eba832d97d5433496d7ffa"
dependencies: dependencies:
...@@ -1697,7 +1699,7 @@ espree@^3.4.0: ...@@ -1697,7 +1699,7 @@ espree@^3.4.0:
acorn "4.0.4" acorn "4.0.4"
acorn-jsx "^3.0.0" acorn-jsx "^3.0.0"
esprima@^2.7.1, esprima@2.7.x: esprima@2.7.x, esprima@^2.7.1:
version "2.7.3" version "2.7.3"
resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581" resolved "https://registry.yarnpkg.com/esprima/-/esprima-2.7.3.tgz#96e3b70d5779f6ad49cd032673d1c312767ba581"
...@@ -2293,7 +2295,7 @@ inflight@^1.0.4: ...@@ -2293,7 +2295,7 @@ inflight@^1.0.4:
once "^1.3.0" once "^1.3.0"
wrappy "1" wrappy "1"
inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@2, inherits@2.0.3: inherits@2, inherits@2.0.3, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1:
version "2.0.3" version "2.0.3"
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de"
...@@ -2499,14 +2501,14 @@ is-windows@^0.2.0: ...@@ -2499,14 +2501,14 @@ is-windows@^0.2.0:
version "0.2.0" version "0.2.0"
resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c" resolved "https://registry.yarnpkg.com/is-windows/-/is-windows-0.2.0.tgz#de1aa6d63ea29dd248737b69f1ff8b8002d2108c"
isarray@^1.0.0, isarray@~1.0.0, isarray@1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isarray@0.0.1: isarray@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf"
isarray@1.0.0, isarray@^1.0.0, isarray@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
isbinaryfile@^3.0.0: isbinaryfile@^3.0.0:
version "3.0.2" version "3.0.2"
resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621" resolved "https://registry.yarnpkg.com/isbinaryfile/-/isbinaryfile-3.0.2.tgz#4a3e974ec0cba9004d3fc6cde7209ea69368a621"
...@@ -2632,7 +2634,7 @@ jquery-ujs@1.2.1: ...@@ -2632,7 +2634,7 @@ jquery-ujs@1.2.1:
dependencies: dependencies:
jquery ">=1.8.0" jquery ">=1.8.0"
jquery@>=1.8.0, jquery@2.2.1: jquery@2.2.1, jquery@>=1.8.0:
version "2.2.1" version "2.2.1"
resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f" resolved "https://registry.yarnpkg.com/jquery/-/jquery-2.2.1.tgz#3c3e16854ad3d2ac44ac65021b17426d22ad803f"
...@@ -2644,7 +2646,7 @@ js-tokens@^3.0.0: ...@@ -2644,7 +2646,7 @@ js-tokens@^3.0.0:
version "3.0.1" version "3.0.1"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.1.tgz#08e9f132484a2c45a30907e9dc4d5567b7f114d7"
js-yaml@^3.5.1, js-yaml@^3.7.0, js-yaml@3.x: js-yaml@3.x, js-yaml@^3.5.1, js-yaml@^3.7.0:
version "3.8.1" version "3.8.1"
resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.8.1.tgz#782ba50200be7b9e5a8537001b7804db3ad02628"
dependencies: dependencies:
...@@ -2681,7 +2683,7 @@ json-stringify-safe@~5.0.1: ...@@ -2681,7 +2683,7 @@ json-stringify-safe@~5.0.1:
version "5.0.1" version "5.0.1"
resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb" resolved "https://registry.yarnpkg.com/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz#1296a2d58fd45f19a0f6ce01d65701e2c735b6eb"
json3@^3.3.2, json3@3.3.2: json3@3.3.2, json3@^3.3.2:
version "3.3.2" version "3.3.2"
resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1" resolved "https://registry.yarnpkg.com/json3/-/json3-3.3.2.tgz#3c0434743df93e2f5c42aee7b19bcb483575f4e1"
...@@ -2721,6 +2723,12 @@ karma-jasmine@^1.1.0: ...@@ -2721,6 +2723,12 @@ karma-jasmine@^1.1.0:
version "1.1.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf" resolved "https://registry.yarnpkg.com/karma-jasmine/-/karma-jasmine-1.1.0.tgz#22e4c06bf9a182e5294d1f705e3733811b810acf"
karma-mocha-reporter@^2.2.2:
version "2.2.2"
resolved "https://registry.yarnpkg.com/karma-mocha-reporter/-/karma-mocha-reporter-2.2.2.tgz#876de9a287244e54a608591732a98e66611f6abe"
dependencies:
chalk "1.1.3"
karma-phantomjs-launcher@^1.0.2: karma-phantomjs-launcher@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1" resolved "https://registry.yarnpkg.com/karma-phantomjs-launcher/-/karma-phantomjs-launcher-1.0.2.tgz#19e1041498fd75563ed86730a22c1fe579fa8fb1"
...@@ -2823,7 +2831,7 @@ loader-runner@^2.3.0: ...@@ -2823,7 +2831,7 @@ loader-runner@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2" resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.3.0.tgz#f482aea82d543e07921700d5a46ef26fdac6b8a2"
loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5, loader-utils@0.2.x: loader-utils@0.2.x, loader-utils@^0.2.11, loader-utils@^0.2.16, loader-utils@^0.2.5:
version "0.2.16" version "0.2.16"
resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-0.2.16.tgz#f08632066ed8282835dff88dfb52704765adee6d"
dependencies: dependencies:
...@@ -2985,7 +2993,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7: ...@@ -2985,7 +2993,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
dependencies: dependencies:
mime-db "~1.26.0" mime-db "~1.26.0"
mime@^1.3.4, mime@1.3.4: mime@1.3.4, mime@^1.3.4:
version "1.3.4" version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53" resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
...@@ -2993,25 +3001,19 @@ minimalistic-assert@^1.0.0: ...@@ -2993,25 +3001,19 @@ minimalistic-assert@^1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3" resolved "https://registry.yarnpkg.com/minimalistic-assert/-/minimalistic-assert-1.0.0.tgz#702be2dda6b37f4836bcb3f5db56641b64a1d3d3"
minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, "minimatch@2 || 3": "minimatch@2 || 3", minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3:
version "3.0.3" version "3.0.3"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.0.3.tgz#2a4e4090b96b2db06a9d7df01055a62a77c9b774"
dependencies: dependencies:
brace-expansion "^1.0.0" brace-expansion "^1.0.0"
minimist@^1.2.0: minimist@0.0.8, minimist@~0.0.1:
version "1.2.0"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
minimist@~0.0.1, minimist@0.0.8:
version "0.0.8" version "0.0.8"
resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d" resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
mkdirp@^0.5.0, mkdirp@^0.5.1, "mkdirp@>=0.5 0", mkdirp@~0.5.0, mkdirp@~0.5.1, mkdirp@0.5.x: minimist@^1.2.0:
version "0.5.1" version "1.2.0"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
dependencies:
minimist "0.0.8"
mkdirp@0.5.0: mkdirp@0.5.0:
version "0.5.0" version "0.5.0"
...@@ -3019,6 +3021,12 @@ mkdirp@0.5.0: ...@@ -3019,6 +3021,12 @@ mkdirp@0.5.0:
dependencies: dependencies:
minimist "0.0.8" minimist "0.0.8"
mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903"
dependencies:
minimist "0.0.8"
moment@2.x: moment@2.x:
version "2.17.1" version "2.17.1"
resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82" resolved "https://registry.yarnpkg.com/moment/-/moment-2.17.1.tgz#fed9506063f36b10f066c8b59a144d7faebe1d82"
...@@ -3130,7 +3138,7 @@ node-zopfli@^2.0.0: ...@@ -3130,7 +3138,7 @@ node-zopfli@^2.0.0:
nan "^2.0.0" nan "^2.0.0"
node-pre-gyp "^0.6.4" node-pre-gyp "^0.6.4"
nopt@~3.0.6, nopt@3.x: nopt@3.x, nopt@~3.0.6:
version "3.0.6" version "3.0.6"
resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9" resolved "https://registry.yarnpkg.com/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"
dependencies: dependencies:
...@@ -3166,14 +3174,14 @@ oauth-sign@~0.8.1: ...@@ -3166,14 +3174,14 @@ oauth-sign@~0.8.1:
version "0.8.2" version "0.8.2"
resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
object-assign@4.1.0: object-assign@4.1.0:
version "4.1.0" version "4.1.0"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.0.tgz#7a3b3d0e98063d43f4c03f2e8ae6cd51a86883a0"
object-assign@^4.0.1, object-assign@^4.1.0:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
object-component@0.0.3: object-component@0.0.3:
version "0.0.3" version "0.0.3"
resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291" resolved "https://registry.yarnpkg.com/object-component/-/object-component-0.0.3.tgz#f0c69aa50efc95b866c186f400a33769cb2f1291"
...@@ -3199,7 +3207,7 @@ on-headers@~1.0.1: ...@@ -3199,7 +3207,7 @@ on-headers@~1.0.1:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7" resolved "https://registry.yarnpkg.com/on-headers/-/on-headers-1.0.1.tgz#928f5d0f470d49342651ea6794b0857c100693f7"
once@^1.3.0, once@^1.4.0, once@1.x: once@1.x, once@^1.3.0, once@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
dependencies: dependencies:
...@@ -3480,22 +3488,18 @@ public-encrypt@^4.0.0: ...@@ -3480,22 +3488,18 @@ public-encrypt@^4.0.0:
parse-asn1 "^5.0.0" parse-asn1 "^5.0.0"
randombytes "^2.0.1" randombytes "^2.0.1"
punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
punycode@1.3.2: punycode@1.3.2:
version "1.3.2" version "1.3.2"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.3.2.tgz#9653a036fb7c1ee42342f2325cceefea3926c48d"
punycode@^1.2.4, punycode@^1.4.1:
version "1.4.1"
resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e"
qjobs@^1.1.4: qjobs@^1.1.4:
version "1.1.5" version "1.1.5"
resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73"
qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
qs@6.2.0: qs@6.2.0:
version "6.2.0" version "6.2.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.0.tgz#3b7848c03c2dece69a9522b0fae8c4126d745f3b"
...@@ -3504,6 +3508,10 @@ qs@6.2.1: ...@@ -3504,6 +3508,10 @@ qs@6.2.1:
version "6.2.1" version "6.2.1"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625" resolved "https://registry.yarnpkg.com/qs/-/qs-6.2.1.tgz#ce03c5ff0935bc1d9d69a9f14cbd18e568d67625"
qs@~6.3.0:
version "6.3.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.0.tgz#f403b264f23bc01228c74131b407f18d5ea5d442"
querystring-es3@^0.2.0: querystring-es3@^0.2.0:
version "0.2.1" version "0.2.1"
resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73" resolved "https://registry.yarnpkg.com/querystring-es3/-/querystring-es3-0.2.1.tgz#9ec61f79049875707d69414596fd907a4d711e73"
...@@ -3743,11 +3751,11 @@ resolve-from@^1.0.0: ...@@ -3743,11 +3751,11 @@ resolve-from@^1.0.0:
version "1.0.1" version "1.0.1"
resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226" resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-1.0.1.tgz#26cbfe935d1aeeeabb29bc3fe5aeb01e93d44226"
resolve@^1.1.6, resolve@1.1.x: resolve@1.1.x:
version "1.1.7" version "1.1.7"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.1.7.tgz#203114d82ad2c5ed9e8e0411b3932875e889e97b"
resolve@^1.2.0: resolve@^1.1.6, resolve@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.2.0.tgz#9589c3f2f6149d1417a40becc1663db6ec6bc26c"
...@@ -3764,7 +3772,7 @@ right-align@^0.1.1: ...@@ -3764,7 +3772,7 @@ right-align@^0.1.1:
dependencies: dependencies:
align-text "^0.1.1" align-text "^0.1.1"
rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4, rimraf@2: rimraf@2, rimraf@^2.2.8, rimraf@^2.3.3, rimraf@^2.4.3, rimraf@^2.4.4, rimraf@~2.5.1, rimraf@~2.5.4:
version "2.5.4" version "2.5.4"
resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04"
dependencies: dependencies:
...@@ -3796,7 +3804,7 @@ select2@3.5.2-browserify: ...@@ -3796,7 +3804,7 @@ select2@3.5.2-browserify:
version "3.5.2-browserify" version "3.5.2-browserify"
resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d" resolved "https://registry.yarnpkg.com/select2/-/select2-3.5.2-browserify.tgz#dc4dafda38d67a734e8a97a46f0d3529ae05391d"
semver@^5.3.0, semver@~5.3.0, "semver@2 || 3 || 4 || 5": "semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@~5.3.0:
version "5.3.0" version "5.3.0"
resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f" resolved "https://registry.yarnpkg.com/semver/-/semver-5.3.0.tgz#9b2ce5d3de02d17c6012ad326aa6b4d0cf54f94f"
...@@ -3963,7 +3971,7 @@ source-map-support@^0.4.2: ...@@ -3963,7 +3971,7 @@ source-map-support@^0.4.2:
dependencies: dependencies:
source-map "^0.5.3" source-map "^0.5.3"
source-map@^0.1.41, source-map@0.1.x: source-map@0.1.x, source-map@^0.1.41:
version "0.1.43" version "0.1.43"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346"
dependencies: dependencies:
...@@ -4063,10 +4071,6 @@ stream-http@^2.3.1: ...@@ -4063,10 +4071,6 @@ stream-http@^2.3.1:
to-arraybuffer "^1.0.0" to-arraybuffer "^1.0.0"
xtend "^4.0.0" xtend "^4.0.0"
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
string-width@^1.0.1, string-width@^1.0.2: string-width@^1.0.1, string-width@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3"
...@@ -4082,6 +4086,10 @@ string-width@^2.0.0: ...@@ -4082,6 +4086,10 @@ string-width@^2.0.0:
is-fullwidth-code-point "^2.0.0" is-fullwidth-code-point "^2.0.0"
strip-ansi "^3.0.0" strip-ansi "^3.0.0"
string_decoder@^0.10.25, string_decoder@~0.10.x:
version "0.10.31"
resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94"
stringstream@~0.0.4: stringstream@~0.0.4:
version "0.0.5" version "0.0.5"
resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878"
...@@ -4292,20 +4300,20 @@ underscore@1.8.3: ...@@ -4292,20 +4300,20 @@ underscore@1.8.3:
version "1.8.3" version "1.8.3"
resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022" resolved "https://registry.yarnpkg.com/underscore/-/underscore-1.8.3.tgz#4f3fb53b106e6097fcf9cb4109f2a5e9bdfa5022"
unpipe@~1.0.0, unpipe@1.0.0: unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
url-parse@^1.1.1: url-parse@1.0.x:
version "1.1.7" version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
dependencies: dependencies:
querystringify "0.0.x" querystringify "0.0.x"
requires-port "1.0.x" requires-port "1.0.x"
url-parse@1.0.x: url-parse@^1.1.1:
version "1.0.5" version "1.1.7"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b" resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.1.7.tgz#025cff999653a459ab34232147d89514cc87d74a"
dependencies: dependencies:
querystringify "0.0.x" querystringify "0.0.x"
requires-port "1.0.x" requires-port "1.0.x"
...@@ -4334,7 +4342,7 @@ util-deprecate@~1.0.1: ...@@ -4334,7 +4342,7 @@ util-deprecate@~1.0.1:
version "1.0.2" version "1.0.2"
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
util@^0.10.3, util@0.10.3: util@0.10.3, util@^0.10.3:
version "0.10.3" version "0.10.3"
resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9" resolved "https://registry.yarnpkg.com/util/-/util-0.10.3.tgz#7afb1afe50805246489e3db7fe0ed379336ac0f9"
dependencies: dependencies:
...@@ -4494,6 +4502,10 @@ window-size@0.1.0: ...@@ -4494,6 +4502,10 @@ window-size@0.1.0:
version "0.1.0" version "0.1.0"
resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d" resolved "https://registry.yarnpkg.com/window-size/-/window-size-0.1.0.tgz#5438cd2ea93b202efa3a19fe8887aee7c94f9c9d"
wordwrap@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
wordwrap@^1.0.0, wordwrap@~1.0.0: wordwrap@^1.0.0, wordwrap@~1.0.0:
version "1.0.0" version "1.0.0"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
...@@ -4502,10 +4514,6 @@ wordwrap@~0.0.2: ...@@ -4502,10 +4514,6 @@ wordwrap@~0.0.2:
version "0.0.3" version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107" resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
wordwrap@0.0.2:
version "0.0.2"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.2.tgz#b79669bb42ecb409f83d583cad52ca17eaa1643f"
wrap-ansi@^2.0.0: wrap-ansi@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"
...@@ -4588,4 +4596,3 @@ yauzl@2.4.1: ...@@ -4588,4 +4596,3 @@ yauzl@2.4.1:
yeast@0.1.2: yeast@0.1.2:
version "0.1.2" version "0.1.2"
resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419" resolved "https://registry.yarnpkg.com/yeast/-/yeast-0.1.2.tgz#008e06d8094320c372dbc2f8ed76a0ca6c8ac419"
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment