Merge branch '22539-display-folders' into 'master'
Resolve "Display "folders" for environments" ## What does this MR do? Adds the ability to show the grouped environments inside "folders". Adds several reusable vue components in order to accomplish the recursive tree data structure presented. For the individual components, Jasmine tests were added. For the ones that depend of an API response, rspec tests are used. ## Screenshots (if relevant) ![Screen_Shot_2016-11-16_at_02.00.13](/uploads/1278012c8639b999b53f080728d283e1/Screen_Shot_2016-11-16_at_02.00.13.png) ![Screen_Shot_2016-11-16_at_02.00.25](/uploads/a3d65416ddb553e1b8f0f4c8897a75dc/Screen_Shot_2016-11-16_at_02.00.25.png) ![Screen_Shot_2016-10-17_at_16.08.50](/uploads/af63efe1d2cbd5fc069408622ef4b607/Screen_Shot_2016-10-17_at_16.08.50.png) ![environments](/uploads/b5a1801766d82ab176fc60f96b6968cb/environments.gif) ## Does this MR meet the acceptance criteria? - [x] [CHANGELOG](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CHANGELOG.md) entry added - [ ] [Documentation created/updated](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/doc_styleguide.md) - [ ] API support added - Tests - [x] Added for this feature/bug - [ ] All builds are passing - [x] Conform by the [merge request performance guides](http://docs.gitlab.com/ce/development/merge_request_performance_guidelines.html) - [x] Conform by the [style guides](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#style-guides) - [x] Branch has no merge conflicts with `master` (if it does - rebase it please) - [x] [Squashed related commits together](https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits) ## What are the relevant issue numbers? Closes #22539 See merge request !7015
This commit is contained in:
commit
b9c2fb8810
|
@ -0,0 +1,248 @@
|
|||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require_tree ../services/
|
||||
//= require ./environment_item
|
||||
|
||||
/* globals Vue, EnvironmentsService */
|
||||
/* eslint-disable no-param-reassign */
|
||||
|
||||
(() => { // eslint-disable-line
|
||||
window.gl = window.gl || {};
|
||||
|
||||
/**
|
||||
* 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 `filterEnvironmnetsByState`
|
||||
* 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.
|
||||
*/
|
||||
|
||||
const filterState = state => environment => environment.state === state && environment;
|
||||
/**
|
||||
* Given the filter function and the array of environments will return only
|
||||
* the environments that match the state provided to the filter function.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @param {Array} array
|
||||
* @return {Array}
|
||||
*/
|
||||
const filterEnvironmnetsByState = (fn, arr) => arr.map((item) => {
|
||||
if (item.children) {
|
||||
const filteredChildren = filterEnvironmnetsByState(fn, item.children).filter(Boolean);
|
||||
if (filteredChildren.length) {
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return fn(item);
|
||||
}).filter(Boolean);
|
||||
|
||||
window.gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
|
||||
props: {
|
||||
store: {
|
||||
type: Object,
|
||||
required: true,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
components: {
|
||||
'environment-item': window.gl.environmentsList.EnvironmentItem,
|
||||
},
|
||||
|
||||
data() {
|
||||
const environmentsData = document.querySelector('#environments-list-view').dataset;
|
||||
|
||||
return {
|
||||
state: this.store.state,
|
||||
visibility: 'available',
|
||||
isLoading: false,
|
||||
cssContainerClass: environmentsData.cssClass,
|
||||
endpoint: environmentsData.environmentsDataEndpoint,
|
||||
canCreateDeployment: environmentsData.canCreateDeployment,
|
||||
canReadEnvironment: environmentsData.canReadEnvironment,
|
||||
canCreateEnvironment: environmentsData.canCreateEnvironment,
|
||||
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
|
||||
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
|
||||
newEnvironmentPath: environmentsData.newEnvironmentPath,
|
||||
helpPagePath: environmentsData.helpPagePath,
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
filteredEnvironments() {
|
||||
return filterEnvironmnetsByState(filterState(this.visibility), this.state.environments);
|
||||
},
|
||||
|
||||
scope() {
|
||||
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 environmnets and stores them.
|
||||
* Toggles loading property.
|
||||
*/
|
||||
created() {
|
||||
gl.environmentsService = new EnvironmentsService(this.endpoint);
|
||||
|
||||
const scope = this.$options.getQueryParameter('scope');
|
||||
if (scope) {
|
||||
this.visibility = scope;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
return gl.environmentsService.all()
|
||||
.then(resp => resp.json())
|
||||
.then((json) => {
|
||||
this.store.storeEnvironments(json);
|
||||
this.isLoading = false;
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Transforms the url parameter into an object and
|
||||
* returns the one requested.
|
||||
*
|
||||
* @param {String} param
|
||||
* @returns {String} The value of the requested parameter.
|
||||
*/
|
||||
getQueryParameter(parameter) {
|
||||
return window.location.search.substring(1).split('&').reduce((acc, param) => {
|
||||
const paramSplited = param.split('=');
|
||||
acc[paramSplited[0]] = paramSplited[1];
|
||||
return acc;
|
||||
}, {})[parameter];
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts permission provided as strings to booleans.
|
||||
* @param {String} string
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
convertPermissionToBoolean(string) {
|
||||
return string === 'true';
|
||||
},
|
||||
|
||||
methods: {
|
||||
toggleRow(model) {
|
||||
return this.store.toggleFolder(model.name);
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<div :class="cssContainerClass">
|
||||
<div class="top-area">
|
||||
<ul v-if="!isLoading" class="nav-links">
|
||||
<li v-bind:class="{ 'active': scope === undefined }">
|
||||
<a :href="projectEnvironmentsPath">
|
||||
Available
|
||||
<span
|
||||
class="badge js-available-environments-count"
|
||||
v-html="state.availableCounter"></span>
|
||||
</a>
|
||||
</li>
|
||||
<li v-bind:class="{ 'active' : scope === 'stopped' }">
|
||||
<a :href="projectStoppedEnvironmentsPath">
|
||||
Stopped
|
||||
<span
|
||||
class="badge js-stopped-environments-count"
|
||||
v-html="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>
|
||||
|
||||
<div class="environments-container">
|
||||
<div class="environments-list-loading text-center" v-if="isLoading">
|
||||
<i class="fa fa-spinner spin"></i>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
<br />
|
||||
<a :href="helpPagePath">
|
||||
Read more about environments
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<a
|
||||
v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create">
|
||||
New Environment
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
<table class="table ci-table environments">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Environment</th>
|
||||
<th>Last deployment</th>
|
||||
<th>Build</th>
|
||||
<th>Commit</th>
|
||||
<th></th>
|
||||
<th class="hidden-xs"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="model in filteredEnvironments"
|
||||
v-bind:model="model">
|
||||
|
||||
<tr
|
||||
is="environment-item"
|
||||
:model="model"
|
||||
:toggleRow="toggleRow.bind(model)"
|
||||
:can-create-deployment="canCreateDeploymentParsed"
|
||||
:can-read-environment="canReadEnvironmentParsed"></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)">
|
||||
</tr>
|
||||
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,67 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
window.gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
|
||||
props: {
|
||||
actions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* Appends the svg icon that were render in the index page.
|
||||
* In order to reuse the svg instead of copy and paste in this template
|
||||
* we need to render it outside this component using =custom_icon partial.
|
||||
*
|
||||
* TODO: Remove this when webpack is merged.
|
||||
*
|
||||
*/
|
||||
mounted() {
|
||||
const playIcon = document.querySelector('.play-icon-svg.hidden svg');
|
||||
|
||||
const dropdownContainer = this.$el.querySelector('.dropdown-play-icon-container');
|
||||
const actionContainers = this.$el.querySelectorAll('.action-play-icon-container');
|
||||
// Phantomjs does not have support to iterate a nodelist.
|
||||
const actionsArray = [].slice.call(actionContainers);
|
||||
|
||||
if (playIcon && actionsArray && dropdownContainer) {
|
||||
dropdownContainer.appendChild(playIcon.cloneNode(true));
|
||||
|
||||
actionsArray.forEach((element) => {
|
||||
element.appendChild(playIcon.cloneNode(true));
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="inline">
|
||||
<div class="dropdown">
|
||||
<a class="dropdown-new btn btn-default" data-toggle="dropdown">
|
||||
<span class="dropdown-play-icon-container">
|
||||
</span>
|
||||
<i class="fa fa-caret-down"></i>
|
||||
</a>
|
||||
|
||||
<ul class="dropdown-menu dropdown-menu-align-right">
|
||||
<li v-for="action in actions">
|
||||
<a :href="action.play_path"
|
||||
data-method="post"
|
||||
rel="nofollow"
|
||||
class="js-manual-action-link">
|
||||
<span class="action-play-icon-container">
|
||||
</span>
|
||||
<span v-html="action.name"></span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,22 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
window.gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
|
||||
props: {
|
||||
external_url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn external_url" :href="external_url" target="_blank">
|
||||
<i class="fa fa-external-link"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,494 @@
|
|||
/*= require lib/utils/timeago */
|
||||
/*= require lib/utils/text_utility */
|
||||
/*= require vue_common_component/commit */
|
||||
/*= require ./environment_actions */
|
||||
/*= require ./environment_external_url */
|
||||
/*= require ./environment_stop */
|
||||
/*= require ./environment_rollback */
|
||||
|
||||
/* globals Vue, timeago */
|
||||
|
||||
(() => {
|
||||
/**
|
||||
* Envrionment Item Component
|
||||
*
|
||||
* 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 || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.EnvironmentItem = Vue.component('environment-item', {
|
||||
|
||||
components: {
|
||||
'commit-component': window.gl.CommitComponent,
|
||||
'actions-component': window.gl.environmentsList.ActionsComponent,
|
||||
'external-url-component': window.gl.environmentsList.ExternalUrlComponent,
|
||||
'stop-component': window.gl.environmentsList.StopComponent,
|
||||
'rollback-component': window.gl.environmentsList.RollbackComponent,
|
||||
},
|
||||
|
||||
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,
|
||||
},
|
||||
},
|
||||
|
||||
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 `stoppable?` key provided in the response.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
isStoppable() {
|
||||
return this.model['stoppable?'];
|
||||
},
|
||||
|
||||
/**
|
||||
* 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;
|
||||
},
|
||||
|
||||
/**
|
||||
* Human readable date.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
createdDate() {
|
||||
const timeagoInstance = new timeago(); // eslint-disable-line
|
||||
|
||||
return timeagoInstance.format(this.model.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;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* 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 v-bind:class="{ 'children-row': isChildren}">
|
||||
<a
|
||||
v-if="!isFolder"
|
||||
class="environment-name"
|
||||
:href="model.environment_path"
|
||||
v-html="model.name">
|
||||
</a>
|
||||
<span v-else v-on:click="toggleRow(model)" class="folder-name">
|
||||
<span class="folder-icon">
|
||||
<i v-show="model.isOpen" class="fa fa-caret-down"></i>
|
||||
<i v-show="!model.isOpen" class="fa fa-caret-right"></i>
|
||||
</span>
|
||||
|
||||
<span v-html="model.name"></span>
|
||||
|
||||
<span class="badge" v-html="childrenCounter"></span>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="deployment-column">
|
||||
<span
|
||||
v-if="shouldRenderDeploymentID"
|
||||
v-html="deploymentInternalId">
|
||||
</span>
|
||||
|
||||
<span v-if="!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>
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<a v-if="shouldRenderBuildName"
|
||||
class="build-link"
|
||||
:href="model.last_deployment.deployable.build_path"
|
||||
v-html="buildName">
|
||||
</a>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component">
|
||||
<commit-component
|
||||
:tag="commitTag"
|
||||
:ref="commitRef"
|
||||
:commit_url="commitUrl"
|
||||
:short_sha="commitShortSha"
|
||||
:title="commitTitle"
|
||||
:author="commitAuthor">
|
||||
</commit-component>
|
||||
</div>
|
||||
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title">
|
||||
No deployments yet
|
||||
</p>
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<span
|
||||
v-if="!isFolder && model.last_deployment"
|
||||
class="environment-created-date-timeago"
|
||||
v-html="createdDate">
|
||||
</span>
|
||||
</td>
|
||||
|
||||
<td class="hidden-xs">
|
||||
<div v-if="!isFolder">
|
||||
<div v-if="hasManualActions && canCreateDeployment"
|
||||
class="inline js-manual-actions-container">
|
||||
<actions-component
|
||||
:actions="manualActions">
|
||||
</actions-component>
|
||||
</div>
|
||||
|
||||
<div v-if="model.external_url && canReadEnvironment"
|
||||
class="inline js-external-url-container">
|
||||
<external-url-component
|
||||
:external_url="model.external_url">
|
||||
</external_url-component>
|
||||
</div>
|
||||
|
||||
<div v-if="isStoppable && canCreateDeployment"
|
||||
class="inline js-stop-component-container">
|
||||
<stop-component
|
||||
:stop_url="model.stop_path">
|
||||
</stop-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>
|
||||
</td>
|
||||
</tr>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,31 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
window.gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
|
||||
props: {
|
||||
retry_url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
is_last_deployment: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a class="btn" :href="retry_url" data-method="post" rel="nofollow">
|
||||
<span v-if="is_last_deployment">
|
||||
Re-deploy
|
||||
</span>
|
||||
<span v-else>
|
||||
Rollback
|
||||
</span>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,27 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
window.gl.environmentsList.StopComponent = Vue.component('stop-component', {
|
||||
props: {
|
||||
stop_url: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
},
|
||||
|
||||
template: `
|
||||
<a
|
||||
class="btn stop-env-link"
|
||||
:href="stop_url"
|
||||
data-confirm="Are you sure you want to stop this environment?"
|
||||
data-method="post"
|
||||
rel="nofollow">
|
||||
<i class="fa fa-stop stop-env-icon"></i>
|
||||
</a>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,21 @@
|
|||
//= require vue
|
||||
//= require_tree ./stores/
|
||||
//= require ./components/environment
|
||||
//= require ./vue_resource_interceptor
|
||||
|
||||
|
||||
$(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
if (window.gl.EnvironmentsListApp) {
|
||||
window.gl.EnvironmentsListApp.$destroy(true);
|
||||
}
|
||||
const Store = window.gl.environmentsList.EnvironmentsStore;
|
||||
|
||||
window.gl.EnvironmentsListApp = new window.gl.environmentsList.EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
propsData: {
|
||||
store: Store.create(),
|
||||
},
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/* globals Vue */
|
||||
/* eslint-disable no-unused-vars, no-param-reassign */
|
||||
class EnvironmentsService {
|
||||
|
||||
constructor(root) {
|
||||
Vue.http.options.root = root;
|
||||
|
||||
this.environments = Vue.resource(root);
|
||||
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
// needed in order to not break the tests.
|
||||
if ($.rails) {
|
||||
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
|
||||
}
|
||||
next();
|
||||
});
|
||||
}
|
||||
|
||||
all() {
|
||||
return this.environments.get();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
window.gl.environmentsList = window.gl.environmentsList || {};
|
||||
|
||||
gl.environmentsList.EnvironmentsStore = {
|
||||
state: {},
|
||||
|
||||
create() {
|
||||
this.state.environments = [];
|
||||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
|
||||
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`
|
||||
* sorted alphabetically.
|
||||
* In each children a `vue-` property will be added. This property will be
|
||||
* used to know if an item is a children mostly for css purposes. This is
|
||||
* needed because the children row is a fragment instance and therfore does
|
||||
* not accept non-prop attributes.
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* it will transform this:
|
||||
* [
|
||||
* { name: "environment", environment_type: "review" },
|
||||
* { name: "environment_1", environment_type: null }
|
||||
* { name: "environment_2, environment_type: "review" }
|
||||
* ]
|
||||
* into this:
|
||||
* [
|
||||
* { name: "review", children:
|
||||
* [
|
||||
* { name: "environment", environment_type: "review", vue-isChildren: true},
|
||||
* { name: "environment_2", environment_type: "review", vue-isChildren: true}
|
||||
* ]
|
||||
* },
|
||||
* {name: "environment_1", environment_type: null}
|
||||
* ]
|
||||
*
|
||||
*
|
||||
* @param {Array} environments List of environments.
|
||||
* @returns {Array} Tree structured array with the received environments.
|
||||
*/
|
||||
storeEnvironments(environments = []) {
|
||||
this.state.stoppedCounter = this.countByState(environments, 'stopped');
|
||||
this.state.availableCounter = this.countByState(environments, 'available');
|
||||
|
||||
const environmentsTree = environments.reduce((acc, environment) => {
|
||||
if (environment.environment_type !== null) {
|
||||
const occurs = acc.filter(element => element.children &&
|
||||
element.name === environment.environment_type);
|
||||
|
||||
environment['vue-isChildren'] = true;
|
||||
|
||||
if (occurs.length) {
|
||||
acc[acc.indexOf(occurs[0])].children.push(environment);
|
||||
acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
|
||||
} else {
|
||||
acc.push({
|
||||
name: environment.environment_type,
|
||||
children: [environment],
|
||||
isOpen: false,
|
||||
'vue-isChildren': environment['vue-isChildren'],
|
||||
});
|
||||
}
|
||||
} else {
|
||||
acc.push(environment);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(this.sortByName);
|
||||
|
||||
this.state.environments = environmentsTree;
|
||||
|
||||
return environmentsTree;
|
||||
},
|
||||
|
||||
/**
|
||||
* 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
|
||||
},
|
||||
};
|
||||
})();
|
|
@ -0,0 +1,12 @@
|
|||
/* global Vue */
|
||||
Vue.http.interceptors.push((request, next) => {
|
||||
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
|
||||
|
||||
next((response) => {
|
||||
if (typeof response.data === 'string') {
|
||||
response.data = JSON.parse(response.data); // eslint-disable-line
|
||||
}
|
||||
|
||||
Vue.activeResources--; // eslint-disable-line
|
||||
});
|
||||
});
|
|
@ -112,6 +112,9 @@
|
|||
gl.text.removeListeners = function(form) {
|
||||
return $('.js-md', form).off();
|
||||
};
|
||||
gl.text.humanize = function(string) {
|
||||
return string.charAt(0).toUpperCase() + string.replace(/_/g, ' ').slice(1);
|
||||
}
|
||||
return gl.text.truncate = function(string, maxLength) {
|
||||
return string.substr(0, (maxLength - 3)) + '...';
|
||||
};
|
||||
|
|
|
@ -0,0 +1,176 @@
|
|||
/*= require vue */
|
||||
/* global Vue */
|
||||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
window.gl.CommitComponent = Vue.component('commit-component', {
|
||||
|
||||
props: {
|
||||
/**
|
||||
* Indicates the existance of a tag.
|
||||
* Used to render the correct icon, if true will render `fa-tag` icon,
|
||||
* if false will render `fa-code-fork` icon.
|
||||
*/
|
||||
tag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided is used to render the branch name and url.
|
||||
* Should contain the following properties:
|
||||
* name
|
||||
* ref_url
|
||||
*/
|
||||
ref: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to link to the commit sha.
|
||||
*/
|
||||
commit_url: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to show the commit short_sha that links to the commit url.
|
||||
*/
|
||||
short_sha: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided shows the commit tile.
|
||||
*/
|
||||
title: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
|
||||
/**
|
||||
* If provided renders information about the author of the commit.
|
||||
* When provided should include:
|
||||
* `avatar_url` to render the avatar icon
|
||||
* `web_url` to link to user profile
|
||||
* `username` to render alt and title tags
|
||||
*/
|
||||
author: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* ref section were provided.
|
||||
*
|
||||
* TODO: Improve this! Use lodash _.has when we have it.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasRef() {
|
||||
return this.ref && this.ref.name && this.ref.ref_url;
|
||||
},
|
||||
|
||||
/**
|
||||
* Used to verify if all the properties needed to render the commit
|
||||
* author section were provided.
|
||||
*
|
||||
* TODO: Improve this! Use lodash _.has when we have it.
|
||||
*
|
||||
* @returns {Boolean}
|
||||
*/
|
||||
hasAuthor() {
|
||||
return this.author &&
|
||||
this.author.avatar_url &&
|
||||
this.author.web_url &&
|
||||
this.author.username;
|
||||
},
|
||||
|
||||
/**
|
||||
* If information about the author is provided will return a string
|
||||
* to be rendered as the alt attribute of the img tag.
|
||||
*
|
||||
* @returns {String}
|
||||
*/
|
||||
userImageAltDescription() {
|
||||
return this.author &&
|
||||
this.author.username ? `${this.author.username}'s avatar` : null;
|
||||
},
|
||||
},
|
||||
|
||||
/**
|
||||
* In order to reuse the svg instead of copy and paste in this template
|
||||
* we need to render it outside this component using =custom_icon partial.
|
||||
* Make sure it has this structure:
|
||||
* .commit-icon-svg.hidden
|
||||
* svg
|
||||
*
|
||||
* TODO: Find a better way to include SVG
|
||||
*/
|
||||
mounted() {
|
||||
const commitIconContainer = this.$el.querySelector('.commit-icon-container');
|
||||
const commitIcon = document.querySelector('.commit-icon-svg.hidden svg');
|
||||
|
||||
if (commitIconContainer && commitIcon) {
|
||||
commitIconContainer.appendChild(commitIcon.cloneNode(true));
|
||||
}
|
||||
},
|
||||
|
||||
template: `
|
||||
<div class="branch-commit">
|
||||
|
||||
<div v-if="hasRef" class="icon-container">
|
||||
<i v-if="tag" class="fa fa-tag"></i>
|
||||
<i v-if="!tag" class="fa fa-code-fork"></i>
|
||||
</div>
|
||||
|
||||
<a v-if="hasRef"
|
||||
class="monospace branch-name"
|
||||
:href="ref.ref_url"
|
||||
v-html="ref.name">
|
||||
</a>
|
||||
|
||||
<div class="icon-container commit-icon commit-icon-container">
|
||||
</div>
|
||||
|
||||
<a class="commit-id monospace"
|
||||
:href="commit_url"
|
||||
v-html="short_sha">
|
||||
</a>
|
||||
|
||||
<p class="commit-title">
|
||||
<span v-if="title">
|
||||
<a v-if="hasAuthor"
|
||||
class="avatar-image-container"
|
||||
:href="author.web_url">
|
||||
<img
|
||||
class="avatar has-tooltip s20"
|
||||
:src="author.avatar_url"
|
||||
:alt="userImageAltDescription"
|
||||
:title="author.username" />
|
||||
</a>
|
||||
|
||||
<a class="commit-row-message"
|
||||
:href="commit_url" v-html="title">
|
||||
</a>
|
||||
</span>
|
||||
<span v-else>
|
||||
Cant find HEAD commit for this branch
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
})();
|
|
@ -1,10 +1,23 @@
|
|||
.environments-container,
|
||||
.deployments-container {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.environments-list-loading {
|
||||
width: 100%;
|
||||
font-size: 34px;
|
||||
}
|
||||
|
||||
@media (max-width: $screen-sm-min) {
|
||||
.environments-container {
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.environments {
|
||||
table-layout: fixed;
|
||||
|
||||
.deployment-column {
|
||||
.avatar {
|
||||
float: none;
|
||||
|
@ -15,6 +28,10 @@
|
|||
margin: 0;
|
||||
}
|
||||
|
||||
.avatar-image-container {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.icon-play {
|
||||
height: 13px;
|
||||
width: 12px;
|
||||
|
@ -38,7 +55,8 @@
|
|||
color: $gl-dark-link-color;
|
||||
}
|
||||
|
||||
.stop-env-link {
|
||||
.stop-env-link,
|
||||
.external-url {
|
||||
color: $table-text-gray;
|
||||
|
||||
.stop-env-icon {
|
||||
|
@ -58,10 +76,29 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.children-row .environment-name {
|
||||
margin-left: 17px;
|
||||
margin-right: -17px;
|
||||
}
|
||||
|
||||
.folder-icon {
|
||||
padding: 0 5px 0 0;
|
||||
}
|
||||
|
||||
.folder-name {
|
||||
cursor: pointer;
|
||||
|
||||
.badge {
|
||||
font-weight: normal;
|
||||
background-color: $gray-darker;
|
||||
color: $gl-placeholder-color;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.table.ci-table.environments {
|
||||
|
||||
.icon-container {
|
||||
width: 20px;
|
||||
text-align: center;
|
||||
|
|
|
@ -8,13 +8,16 @@ class Projects::EnvironmentsController < Projects::ApplicationController
|
|||
|
||||
def index
|
||||
@scope = params[:scope]
|
||||
@all_environments = project.environments
|
||||
@environments =
|
||||
if @scope == 'stopped'
|
||||
@all_environments.stopped
|
||||
else
|
||||
@all_environments.available
|
||||
@environments = project.environments
|
||||
|
||||
respond_to do |format|
|
||||
format.html
|
||||
format.json do
|
||||
render json: EnvironmentSerializer
|
||||
.new(project: @project)
|
||||
.represent(@environments)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
module EnvironmentsHelper
|
||||
def environments_list_data
|
||||
{
|
||||
endpoint: namespace_project_environments_path(@project.namespace, @project, format: :json)
|
||||
}
|
||||
end
|
||||
end
|
|
@ -4,21 +4,21 @@ class BuildEntity < Grape::Entity
|
|||
expose :id
|
||||
expose :name
|
||||
|
||||
expose :build_url do |build|
|
||||
url_to(:namespace_project_build, build)
|
||||
expose :build_path do |build|
|
||||
path_to(:namespace_project_build, build)
|
||||
end
|
||||
|
||||
expose :retry_url do |build|
|
||||
url_to(:retry_namespace_project_build, build)
|
||||
expose :retry_path do |build|
|
||||
path_to(:retry_namespace_project_build, build)
|
||||
end
|
||||
|
||||
expose :play_url, if: ->(build, _) { build.manual? } do |build|
|
||||
url_to(:play_namespace_project_build, build)
|
||||
expose :play_path, if: ->(build, _) { build.manual? } do |build|
|
||||
path_to(:play_namespace_project_build, build)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def url_to(route, build)
|
||||
send("#{route}_url", build.project.namespace, build.project, build)
|
||||
def path_to(route, build)
|
||||
send("#{route}_path", build.project.namespace, build.project, build)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -9,4 +9,11 @@ class CommitEntity < API::Entities::RepoCommit
|
|||
request.project,
|
||||
id: commit.id)
|
||||
end
|
||||
|
||||
expose :commit_path do |commit|
|
||||
namespace_project_tree_path(
|
||||
request.project.namespace,
|
||||
request.project,
|
||||
id: commit.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,8 +10,8 @@ class DeploymentEntity < Grape::Entity
|
|||
deployment.ref
|
||||
end
|
||||
|
||||
expose :ref_url do |deployment|
|
||||
namespace_project_tree_url(
|
||||
expose :ref_path do |deployment|
|
||||
namespace_project_tree_path(
|
||||
deployment.project.namespace,
|
||||
deployment.project,
|
||||
id: deployment.ref)
|
||||
|
|
|
@ -9,8 +9,15 @@ class EnvironmentEntity < Grape::Entity
|
|||
expose :last_deployment, using: DeploymentEntity
|
||||
expose :stoppable?
|
||||
|
||||
expose :environment_url do |environment|
|
||||
namespace_project_environment_url(
|
||||
expose :environment_path do |environment|
|
||||
namespace_project_environment_path(
|
||||
environment.project.namespace,
|
||||
environment.project,
|
||||
environment)
|
||||
end
|
||||
|
||||
expose :stop_path do |environment|
|
||||
stop_namespace_project_environment_path(
|
||||
environment.project.namespace,
|
||||
environment.project,
|
||||
environment)
|
||||
|
|
|
@ -1,35 +0,0 @@
|
|||
- last_deployment = environment.last_deployment
|
||||
|
||||
%tr.environment
|
||||
%td
|
||||
= link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
|
||||
|
||||
%td.deployment-column
|
||||
- if last_deployment
|
||||
%span ##{last_deployment.iid}
|
||||
- if last_deployment.user
|
||||
by
|
||||
= user_avatar(user: last_deployment.user, size: 20)
|
||||
|
||||
%td
|
||||
- if last_deployment && last_deployment.deployable
|
||||
= link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
|
||||
= "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
|
||||
|
||||
%td
|
||||
- if last_deployment
|
||||
= render 'projects/deployments/commit', deployment: last_deployment
|
||||
- else
|
||||
%p.commit-title
|
||||
No deployments yet
|
||||
|
||||
%td
|
||||
- if last_deployment
|
||||
#{time_ago_with_tooltip(last_deployment.created_at)}
|
||||
|
||||
%td.hidden-xs
|
||||
.pull-right
|
||||
= render 'projects/environments/external_url', environment: environment
|
||||
= render 'projects/deployments/actions', deployment: last_deployment
|
||||
= render 'projects/environments/stop', environment: environment
|
||||
= render 'projects/deployments/rollback', deployment: last_deployment
|
|
@ -2,47 +2,19 @@
|
|||
- page_title "Environments"
|
||||
= render "projects/pipelines/head"
|
||||
|
||||
%div{ class: container_class }
|
||||
.top-area
|
||||
%ul.nav-links
|
||||
%li{class: ('active' if @scope.nil?)}
|
||||
= link_to project_environments_path(@project) do
|
||||
Available
|
||||
%span.badge.js-available-environments-count
|
||||
= number_with_delimiter(@all_environments.available.count)
|
||||
- content_for :page_specific_javascripts do
|
||||
= page_specific_javascript_tag("environments/environments_bundle.js")
|
||||
.commit-icon-svg.hidden
|
||||
= custom_icon("icon_commit")
|
||||
.play-icon-svg.hidden
|
||||
= custom_icon("icon_play")
|
||||
|
||||
%li{class: ('active' if @scope == 'stopped')}
|
||||
= link_to project_environments_path(@project, scope: :stopped) do
|
||||
Stopped
|
||||
%span.badge.js-stopped-environments-count
|
||||
= number_with_delimiter(@all_environments.stopped.count)
|
||||
|
||||
- if can?(current_user, :create_environment, @project) && !@all_environments.blank?
|
||||
.nav-controls
|
||||
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
|
||||
New environment
|
||||
|
||||
.environments-container
|
||||
- if @all_environments.blank?
|
||||
.blank-state.blank-state-no-icon
|
||||
%h2.blank-state-title
|
||||
You don't have any environments right now.
|
||||
%p.blank-state-text
|
||||
Environments are places where code gets deployed, such as staging or production.
|
||||
%br
|
||||
= succeed "." do
|
||||
= link_to "Read more about environments", help_page_path("ci/environments")
|
||||
- if can?(current_user, :create_environment, @project)
|
||||
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
|
||||
New environment
|
||||
- else
|
||||
.table-holder
|
||||
%table.table.ci-table.environments
|
||||
%tbody
|
||||
%th Environment
|
||||
%th Last Deployment
|
||||
%th Build
|
||||
%th Commit
|
||||
%th
|
||||
%th.hidden-xs
|
||||
= render @environments
|
||||
#environments-list-view{ data: { environments_data: environments_list_data,
|
||||
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
|
||||
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
|
||||
"can-create-environment" => can?(current_user, :create_environment, @project).to_s,
|
||||
"project-environments-path" => project_environments_path(@project),
|
||||
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
|
||||
"new-environment-path" => new_namespace_project_environment_path(@project.namespace, @project),
|
||||
"help-page-path" => help_page_path("ci/environments"),
|
||||
"css-class" => container_class}}
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Display "folders" for environments
|
||||
merge_request: 7015
|
||||
author:
|
|
@ -94,6 +94,7 @@ module Gitlab
|
|||
config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
|
||||
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
|
||||
config.assets.precompile << "boards/test_utils/simulate_drag.js"
|
||||
config.assets.precompile << "environments/environments_bundle.js"
|
||||
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
|
||||
config.assets.precompile << "snippet/snippet_bundle.js"
|
||||
config.assets.precompile << "lib/utils/*.js"
|
||||
|
|
|
@ -106,6 +106,9 @@ ActiveRecord::Schema.define(version: 20161117114805) do
|
|||
t.integer "housekeeping_incremental_repack_period", default: 10, null: false
|
||||
t.integer "housekeeping_full_repack_period", default: 50, null: false
|
||||
t.integer "housekeeping_gc_period", default: 200, null: false
|
||||
t.boolean "sidekiq_throttling_enabled", default: false
|
||||
t.string "sidekiq_throttling_queues"
|
||||
t.decimal "sidekiq_throttling_factor"
|
||||
end
|
||||
|
||||
create_table "audit_events", force: :cascade do |t|
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe Projects::EnvironmentsController do
|
||||
include ApiHelpers
|
||||
|
||||
let(:environment) { create(:environment) }
|
||||
let(:project) { environment.project }
|
||||
let(:user) { create(:user) }
|
||||
|
@ -11,6 +13,27 @@ describe Projects::EnvironmentsController do
|
|||
sign_in(user)
|
||||
end
|
||||
|
||||
describe 'GET index' do
|
||||
context 'when standardrequest has been made' do
|
||||
it 'responds with status code 200' do
|
||||
get :index, environment_params
|
||||
|
||||
expect(response).to be_ok
|
||||
end
|
||||
end
|
||||
|
||||
context 'when requesting JSON response' do
|
||||
it 'responds with correct JSON' do
|
||||
get :index, environment_params(format: :json)
|
||||
|
||||
first_environment = json_response.first
|
||||
|
||||
expect(first_environment).not_to be_empty
|
||||
expect(first_environment['name']). to eq environment.name
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET show' do
|
||||
context 'with valid id' do
|
||||
it 'responds with a status code 200' do
|
||||
|
@ -48,11 +71,9 @@ describe Projects::EnvironmentsController do
|
|||
end
|
||||
end
|
||||
|
||||
def environment_params
|
||||
{
|
||||
namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: environment.id
|
||||
}
|
||||
def environment_params(opts = {})
|
||||
opts.reverse_merge(namespace_id: project.namespace,
|
||||
project_id: project,
|
||||
id: environment.id)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Environment', :feature do
|
||||
given(:project) { create(:empty_project) }
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
|
||||
background do
|
||||
login_as(user)
|
||||
project.team << [user, role]
|
||||
end
|
||||
|
||||
feature 'environment details page' do
|
||||
given!(:environment) { create(:environment, project: project) }
|
||||
given!(:deployment) { }
|
||||
given!(:manual) { }
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
end
|
||||
|
||||
context 'without deployments' do
|
||||
scenario 'does show no deployments' do
|
||||
expect(page).to have_content('You don\'t have any deployments right now.')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deployments' do
|
||||
context 'when there is no related deployable' do
|
||||
given(:deployment) do
|
||||
create(:deployment, environment: environment, deployable: nil)
|
||||
end
|
||||
|
||||
scenario 'does show deployment SHA' do
|
||||
expect(page).to have_link(deployment.short_sha)
|
||||
end
|
||||
|
||||
scenario 'does not show a re-deploy button for deployment without build' do
|
||||
expect(page).not_to have_link('Re-deploy')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with related deployable present' do
|
||||
given(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
|
||||
given(:deployment) do
|
||||
create(:deployment, environment: environment, deployable: build)
|
||||
end
|
||||
|
||||
scenario 'does show build name' do
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
|
||||
scenario 'does show re-deploy button' do
|
||||
expect(page).to have_link('Re-deploy')
|
||||
end
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_link('Stop')
|
||||
end
|
||||
|
||||
context 'with manual action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
|
||||
|
||||
scenario 'does show a play button' do
|
||||
expect(page).to have_link(manual.name.humanize)
|
||||
end
|
||||
|
||||
scenario 'does allow to play manual action' do
|
||||
expect(manual).to be_skipped
|
||||
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
|
||||
expect(page).to have_content(manual.name)
|
||||
expect(manual.reload).to be_pending
|
||||
end
|
||||
|
||||
context 'with external_url' do
|
||||
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
|
||||
scenario 'does show an external link button' do
|
||||
expect(page).to have_link(nil, href: environment.external_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with stop action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
|
||||
|
||||
scenario 'does show stop button' do
|
||||
expect(page).to have_link('Stop')
|
||||
end
|
||||
|
||||
scenario 'does allow to stop environment' do
|
||||
click_link('Stop')
|
||||
|
||||
expect(page).to have_content('close_app')
|
||||
end
|
||||
|
||||
context 'for reporter' do
|
||||
let(:role) { :reporter }
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_link('Stop')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
feature 'auto-close environment when branch is deleted' do
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given!(:environment) do
|
||||
create(:environment, :with_review_app, project: project,
|
||||
ref: 'feature')
|
||||
end
|
||||
|
||||
scenario 'user visits environment page' do
|
||||
visit_environment(environment)
|
||||
|
||||
expect(page).to have_link('Stop')
|
||||
end
|
||||
|
||||
scenario 'user deletes the branch with running environment' do
|
||||
visit namespace_project_branches_path(project.namespace, project)
|
||||
|
||||
remove_branch_with_hooks(project, user, 'feature') do
|
||||
page.within('.js-branch-feature') { find('a.btn-remove').click }
|
||||
end
|
||||
|
||||
visit_environment(environment)
|
||||
|
||||
expect(page).to have_no_link('Stop')
|
||||
end
|
||||
|
||||
##
|
||||
# This is a workaround for problem described in #24543
|
||||
#
|
||||
def remove_branch_with_hooks(project, user, branch)
|
||||
params = {
|
||||
oldrev: project.commit(branch).id,
|
||||
newrev: Gitlab::Git::BLANK_SHA,
|
||||
ref: "refs/heads/#{branch}"
|
||||
}
|
||||
|
||||
yield
|
||||
|
||||
GitPushService.new(project, user, params).execute
|
||||
end
|
||||
end
|
||||
|
||||
def visit_environment(environment)
|
||||
visit namespace_project_environment_path(environment.project.namespace,
|
||||
environment.project,
|
||||
environment)
|
||||
end
|
||||
end
|
|
@ -1,6 +1,6 @@
|
|||
require 'spec_helper'
|
||||
|
||||
feature 'Environments', feature: true do
|
||||
feature 'Environments page', :feature, :js do
|
||||
given(:project) { create(:empty_project) }
|
||||
given(:user) { create(:user) }
|
||||
given(:role) { :developer }
|
||||
|
@ -10,221 +10,138 @@ feature 'Environments', feature: true do
|
|||
login_as(user)
|
||||
end
|
||||
|
||||
describe 'when showing environments' do
|
||||
given!(:environment) { }
|
||||
given!(:deployment) { }
|
||||
given!(:manual) { }
|
||||
given!(:environment) { }
|
||||
given!(:deployment) { }
|
||||
given!(:manual) { }
|
||||
|
||||
before do
|
||||
visit_environments(project)
|
||||
before do
|
||||
visit_environments(project)
|
||||
end
|
||||
|
||||
describe 'page tabs' do
|
||||
scenario 'shows "Available" and "Stopped" tab with links' do
|
||||
expect(page).to have_link('Available')
|
||||
expect(page).to have_link('Stopped')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without environments' do
|
||||
scenario 'does show no environments' do
|
||||
expect(page).to have_content('You don\'t have any environments right now.')
|
||||
end
|
||||
|
||||
context 'shows two tabs' do
|
||||
scenario 'shows "Available" and "Stopped" tab with links' do
|
||||
expect(page).to have_link('Available')
|
||||
expect(page).to have_link('Stopped')
|
||||
end
|
||||
end
|
||||
|
||||
context 'without environments' do
|
||||
scenario 'does show no environments' do
|
||||
expect(page).to have_content('You don\'t have any environments right now.')
|
||||
end
|
||||
|
||||
scenario 'does show 0 as counter for environments in both tabs' do
|
||||
expect(page.find('.js-available-environments-count').text).to eq('0')
|
||||
expect(page.find('.js-stopped-environments-count').text).to eq('0')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with environments' do
|
||||
given(:environment) { create(:environment, project: project) }
|
||||
|
||||
scenario 'does show environment name' do
|
||||
expect(page).to have_link(environment.name)
|
||||
end
|
||||
|
||||
scenario 'does show number of available and stopped environments' do
|
||||
expect(page.find('.js-available-environments-count').text).to eq('1')
|
||||
expect(page.find('.js-stopped-environments-count').text).to eq('0')
|
||||
end
|
||||
|
||||
context 'without deployments' do
|
||||
scenario 'does show no deployments' do
|
||||
expect(page).to have_content('No deployments yet')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deployments' do
|
||||
given(:deployment) { create(:deployment, environment: environment) }
|
||||
|
||||
scenario 'does show deployment SHA' do
|
||||
expect(page).to have_link(deployment.short_sha)
|
||||
end
|
||||
|
||||
scenario 'does show deployment internal id' do
|
||||
expect(page).to have_content(deployment.iid)
|
||||
end
|
||||
|
||||
context 'with build and manual actions' do
|
||||
given(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
|
||||
|
||||
scenario 'does show a play button' do
|
||||
expect(page).to have_link(manual.name.humanize)
|
||||
end
|
||||
|
||||
scenario 'does allow to play manual action' do
|
||||
expect(manual).to be_skipped
|
||||
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
|
||||
expect(page).to have_content(manual.name)
|
||||
expect(manual.reload).to be_pending
|
||||
end
|
||||
|
||||
scenario 'does show build name and id' do
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
end
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_selector('.stop-env-link')
|
||||
end
|
||||
|
||||
scenario 'does not show external link button' do
|
||||
expect(page).not_to have_css('external-url')
|
||||
end
|
||||
|
||||
context 'with external_url' do
|
||||
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
|
||||
scenario 'does show an external link button' do
|
||||
expect(page).to have_link(nil, href: environment.external_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with stop action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
|
||||
|
||||
scenario 'does show stop button' do
|
||||
expect(page).to have_selector('.stop-env-link')
|
||||
end
|
||||
|
||||
scenario 'starts build when stop button clicked' do
|
||||
first('.stop-env-link').click
|
||||
|
||||
expect(page).to have_content('close_app')
|
||||
end
|
||||
|
||||
context 'for reporter' do
|
||||
let(:role) { :reporter }
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_selector('.stop-env-link')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
scenario 'does have a New environment button' do
|
||||
expect(page).to have_link('New environment')
|
||||
scenario 'does show 0 as counter for environments in both tabs' do
|
||||
expect(page.find('.js-available-environments-count').text).to eq('0')
|
||||
expect(page.find('.js-stopped-environments-count').text).to eq('0')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'when showing the environment' do
|
||||
given(:environment) { create(:environment, project: project) }
|
||||
given!(:deployment) { }
|
||||
given!(:manual) { }
|
||||
|
||||
before do
|
||||
visit_environment(environment)
|
||||
scenario 'does show environment name' do
|
||||
expect(page).to have_link(environment.name)
|
||||
end
|
||||
|
||||
scenario 'does show number of available and stopped environments' do
|
||||
expect(page.find('.js-available-environments-count').text).to eq('1')
|
||||
expect(page.find('.js-stopped-environments-count').text).to eq('0')
|
||||
end
|
||||
|
||||
context 'without deployments' do
|
||||
scenario 'does show no deployments' do
|
||||
expect(page).to have_content('You don\'t have any deployments right now.')
|
||||
expect(page).to have_content('No deployments yet')
|
||||
end
|
||||
end
|
||||
|
||||
context 'with deployments' do
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given(:deployment) do
|
||||
create(:deployment, environment: environment, deployable: nil)
|
||||
create(:deployment, environment: environment,
|
||||
sha: project.commit.id)
|
||||
end
|
||||
|
||||
scenario 'does show deployment SHA' do
|
||||
expect(page).to have_link(deployment.short_sha)
|
||||
end
|
||||
|
||||
scenario 'does not show a re-deploy button for deployment without build' do
|
||||
expect(page).not_to have_link('Re-deploy')
|
||||
scenario 'does show deployment internal id' do
|
||||
expect(page).to have_content(deployment.iid)
|
||||
end
|
||||
|
||||
context 'with build' do
|
||||
context 'with build and manual actions' do
|
||||
given(:pipeline) { create(:ci_pipeline, project: project) }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
|
||||
scenario 'does show build name' do
|
||||
expect(page).to have_link("#{build.name} (##{build.id})")
|
||||
given(:manual) do
|
||||
create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
|
||||
end
|
||||
|
||||
scenario 'does show re-deploy button' do
|
||||
expect(page).to have_link('Re-deploy')
|
||||
given(:deployment) do
|
||||
create(:deployment, environment: environment,
|
||||
deployable: build,
|
||||
sha: project.commit.id)
|
||||
end
|
||||
|
||||
scenario 'does show a play button' do
|
||||
find('.dropdown-play-icon-container').click
|
||||
expect(page).to have_content(manual.name.humanize)
|
||||
end
|
||||
|
||||
scenario 'does allow to play manual action', js: true do
|
||||
expect(manual).to be_skipped
|
||||
|
||||
find('.dropdown-play-icon-container').click
|
||||
expect(page).to have_content(manual.name.humanize)
|
||||
|
||||
expect { click_link(manual.name.humanize) }
|
||||
.not_to change { Ci::Pipeline.count }
|
||||
|
||||
expect(manual.reload).to be_pending
|
||||
end
|
||||
|
||||
scenario 'does show build name and id' do
|
||||
expect(page).to have_link("#{build.name} ##{build.id}")
|
||||
end
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_link('Stop')
|
||||
expect(page).not_to have_selector('.stop-env-link')
|
||||
end
|
||||
|
||||
context 'with manual action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production') }
|
||||
scenario 'does not show external link button' do
|
||||
expect(page).not_to have_css('external-url')
|
||||
end
|
||||
|
||||
scenario 'does show a play button' do
|
||||
expect(page).to have_link(manual.name.humanize)
|
||||
context 'with external_url' do
|
||||
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
|
||||
scenario 'does show an external link button' do
|
||||
expect(page).to have_link(nil, href: environment.external_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with stop action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
|
||||
|
||||
scenario 'does show stop button' do
|
||||
expect(page).to have_selector('.stop-env-link')
|
||||
end
|
||||
|
||||
scenario 'does allow to play manual action' do
|
||||
expect(manual).to be_skipped
|
||||
expect{ click_link(manual.name.humanize) }.not_to change { Ci::Pipeline.count }
|
||||
expect(page).to have_content(manual.name)
|
||||
expect(manual.reload).to be_pending
|
||||
scenario 'starts build when stop button clicked' do
|
||||
find('.stop-env-link').click
|
||||
|
||||
expect(page).to have_content('close_app')
|
||||
end
|
||||
|
||||
context 'with external_url' do
|
||||
given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
|
||||
given(:build) { create(:ci_build, pipeline: pipeline) }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
|
||||
context 'for reporter' do
|
||||
let(:role) { :reporter }
|
||||
|
||||
scenario 'does show an external link button' do
|
||||
expect(page).to have_link(nil, href: environment.external_url)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with stop action' do
|
||||
given(:manual) { create(:ci_build, :manual, pipeline: pipeline, name: 'close_app') }
|
||||
given(:deployment) { create(:deployment, environment: environment, deployable: build, on_stop: 'close_app') }
|
||||
|
||||
scenario 'does show stop button' do
|
||||
expect(page).to have_link('Stop')
|
||||
end
|
||||
|
||||
scenario 'does allow to stop environment' do
|
||||
click_link('Stop')
|
||||
|
||||
expect(page).to have_content('close_app')
|
||||
end
|
||||
|
||||
context 'for reporter' do
|
||||
let(:role) { :reporter }
|
||||
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_link('Stop')
|
||||
end
|
||||
scenario 'does not show stop button' do
|
||||
expect(page).not_to have_selector('.stop-env-link')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -232,6 +149,10 @@ feature 'Environments', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
scenario 'does have a New environment button' do
|
||||
expect(page).to have_link('New environment')
|
||||
end
|
||||
|
||||
describe 'when creating a new environment' do
|
||||
before do
|
||||
visit_environments(project)
|
||||
|
@ -274,55 +195,7 @@ feature 'Environments', feature: true do
|
|||
end
|
||||
end
|
||||
|
||||
feature 'auto-close environment when branch deleted' do
|
||||
given(:project) { create(:project) }
|
||||
|
||||
given!(:environment) do
|
||||
create(:environment, :with_review_app, project: project,
|
||||
ref: 'feature')
|
||||
end
|
||||
|
||||
scenario 'user visits environment page' do
|
||||
visit_environment(environment)
|
||||
|
||||
expect(page).to have_link('Stop')
|
||||
end
|
||||
|
||||
scenario 'user deletes the branch with running environment' do
|
||||
visit namespace_project_branches_path(project.namespace, project)
|
||||
|
||||
remove_branch_with_hooks(project, user, 'feature') do
|
||||
page.within('.js-branch-feature') { find('a.btn-remove').click }
|
||||
end
|
||||
|
||||
visit_environment(environment)
|
||||
|
||||
expect(page).to have_no_link('Stop')
|
||||
end
|
||||
|
||||
##
|
||||
# This is a workaround for problem described in #24543
|
||||
#
|
||||
def remove_branch_with_hooks(project, user, branch)
|
||||
params = {
|
||||
oldrev: project.commit(branch).id,
|
||||
newrev: Gitlab::Git::BLANK_SHA,
|
||||
ref: "refs/heads/#{branch}"
|
||||
}
|
||||
|
||||
yield
|
||||
|
||||
GitPushService.new(project, user, params).execute
|
||||
end
|
||||
end
|
||||
|
||||
def visit_environments(project)
|
||||
visit namespace_project_environments_path(project.namespace, project)
|
||||
end
|
||||
|
||||
def visit_environment(environment)
|
||||
visit namespace_project_environment_path(environment.project.namespace,
|
||||
environment.project,
|
||||
environment)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -0,0 +1,37 @@
|
|||
//= require vue
|
||||
//= require environments/components/environment_actions
|
||||
|
||||
describe('Actions Component', () => {
|
||||
fixture.preload('environments/element.html');
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.load('environments/element.html');
|
||||
});
|
||||
|
||||
it('Should render a dropdown with the provided actions', () => {
|
||||
const actionsMock = [
|
||||
{
|
||||
name: 'bar',
|
||||
play_path: 'https://gitlab.com/play',
|
||||
},
|
||||
{
|
||||
name: 'foo',
|
||||
play_path: '#',
|
||||
},
|
||||
];
|
||||
|
||||
const component = new window.gl.environmentsList.ActionsComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
actions: actionsMock,
|
||||
},
|
||||
});
|
||||
|
||||
expect(
|
||||
component.$el.querySelectorAll('.dropdown-menu li').length
|
||||
).toEqual(actionsMock.length);
|
||||
expect(
|
||||
component.$el.querySelector('.dropdown-menu li a').getAttribute('href')
|
||||
).toEqual(actionsMock[0].play_path);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
//= require vue
|
||||
//= require environments/components/environment_external_url
|
||||
|
||||
describe('External URL Component', () => {
|
||||
fixture.preload('environments/element.html');
|
||||
beforeEach(() => {
|
||||
fixture.load('environments/element.html');
|
||||
});
|
||||
|
||||
it('should link to the provided external_url', () => {
|
||||
const externalURL = 'https://gitlab.com';
|
||||
const component = new window.gl.environmentsList.ExternalUrlComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
external_url: externalURL,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.$el.getAttribute('href')).toEqual(externalURL);
|
||||
expect(component.$el.querySelector('fa-external-link')).toBeDefined();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,215 @@
|
|||
//= require vue
|
||||
//= require environments/components/environment_item
|
||||
|
||||
describe('Environment item', () => {
|
||||
fixture.preload('environments/table.html');
|
||||
beforeEach(() => {
|
||||
fixture.load('environments/table.html');
|
||||
});
|
||||
|
||||
describe('When item is folder', () => {
|
||||
let mockItem;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
mockItem = {
|
||||
name: 'review',
|
||||
children: [
|
||||
{
|
||||
name: 'review-app',
|
||||
id: 1,
|
||||
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({
|
||||
el: document.querySelector('tr#environment-row'),
|
||||
propsData: {
|
||||
model: mockItem,
|
||||
toggleRow: () => {},
|
||||
canCreateDeployment: false,
|
||||
canReadEnvironment: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('Should render folder icon and name', () => {
|
||||
expect(component.$el.querySelector('.folder-name').textContent).toContain(mockItem.name);
|
||||
expect(component.$el.querySelector('.folder-icon')).toBeDefined();
|
||||
});
|
||||
|
||||
it('Should render the number of children in a badge', () => {
|
||||
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when item is not folder', () => {
|
||||
let environment;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
environment = {
|
||||
id: 31,
|
||||
name: 'production',
|
||||
state: 'stopped',
|
||||
external_url: 'http://external.com',
|
||||
environment_type: null,
|
||||
last_deployment: {
|
||||
id: 66,
|
||||
iid: 6,
|
||||
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_path: 'root/ci-folders/tree/master',
|
||||
},
|
||||
tag: true,
|
||||
'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: 1279,
|
||||
name: 'deploy',
|
||||
build_path: '/root/ci-folders/builds/1279',
|
||||
retry_path: '/root/ci-folders/builds/1279/retry',
|
||||
},
|
||||
manual_actions: [
|
||||
{
|
||||
name: 'action',
|
||||
play_path: '/play',
|
||||
},
|
||||
],
|
||||
},
|
||||
'stoppable?': true,
|
||||
environment_path: 'root/ci-folders/environments/31',
|
||||
created_at: '2016-11-07T11:11:16.525Z',
|
||||
updated_at: '2016-11-10T15:55:58.778Z',
|
||||
};
|
||||
|
||||
component = new window.gl.environmentsList.EnvironmentItem({
|
||||
el: document.querySelector('tr#environment-row'),
|
||||
propsData: {
|
||||
model: environment,
|
||||
toggleRow: () => {},
|
||||
canCreateDeployment: true,
|
||||
canReadEnvironment: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should render environment name', () => {
|
||||
expect(component.$el.querySelector('.environment-name').textContent).toEqual(environment.name);
|
||||
});
|
||||
|
||||
describe('With deployment', () => {
|
||||
it('should render deployment internal id', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.deployment-column span').textContent
|
||||
).toContain(environment.last_deployment.iid);
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.deployment-column span').textContent
|
||||
).toContain('#');
|
||||
});
|
||||
|
||||
describe('With user information', () => {
|
||||
it('should render user avatar with link to profile', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-deploy-user-container').getAttribute('href')
|
||||
).toEqual(environment.last_deployment.user.web_url);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With build url', () => {
|
||||
it('Should link to build url provided', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.build-link').getAttribute('href')
|
||||
).toEqual(environment.last_deployment.deployable.build_path);
|
||||
});
|
||||
|
||||
it('Should render deployable name and id', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.build-link').getAttribute('href')
|
||||
).toEqual(environment.last_deployment.deployable.build_path);
|
||||
});
|
||||
});
|
||||
|
||||
describe('With commit information', () => {
|
||||
it('should render commit component', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-commit-component')
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('With manual actions', () => {
|
||||
it('Should render actions component', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-manual-actions-container')
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('With external URL', () => {
|
||||
it('should render external url component', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-external-url-container')
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('With stop action', () => {
|
||||
it('Should render stop action component', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-stop-component-container')
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('With retry action', () => {
|
||||
it('Should render rollback component', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-rollback-component-container')
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
//= require vue
|
||||
//= require environments/components/environment_rollback
|
||||
describe('Rollback Component', () => {
|
||||
fixture.preload('environments/element.html');
|
||||
|
||||
const retryURL = 'https://gitlab.com/retry';
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.load('environments/element.html');
|
||||
});
|
||||
|
||||
it('Should link to the provided retry_url', () => {
|
||||
const component = new window.gl.environmentsList.RollbackComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
retry_url: retryURL,
|
||||
is_last_deployment: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.$el.getAttribute('href')).toEqual(retryURL);
|
||||
});
|
||||
|
||||
it('Should render Re-deploy label when is_last_deployment is true', () => {
|
||||
const component = new window.gl.environmentsList.RollbackComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
retry_url: retryURL,
|
||||
is_last_deployment: true,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.$el.querySelector('span').textContent).toContain('Re-deploy');
|
||||
});
|
||||
|
||||
|
||||
it('Should render Rollback label when is_last_deployment is false', () => {
|
||||
const component = new window.gl.environmentsList.RollbackComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
retry_url: retryURL,
|
||||
is_last_deployment: false,
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.$el.querySelector('span').textContent).toContain('Rollback');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,28 @@
|
|||
//= require vue
|
||||
//= require environments/components/environment_stop
|
||||
describe('Stop Component', () => {
|
||||
fixture.preload('environments/element.html');
|
||||
|
||||
let stopURL;
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
fixture.load('environments/element.html');
|
||||
|
||||
stopURL = '/stop';
|
||||
component = new window.gl.environmentsList.StopComponent({
|
||||
el: document.querySelector('.test-dom-element'),
|
||||
propsData: {
|
||||
stop_url: stopURL,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should link to the provided URL', () => {
|
||||
expect(component.$el.getAttribute('href')).toEqual(stopURL);
|
||||
});
|
||||
|
||||
it('should have a data-confirm attribute', () => {
|
||||
expect(component.$el.getAttribute('data-confirm')).toEqual('Are you sure you want to stop this environment?');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,69 @@
|
|||
//= require vue
|
||||
//= require environments/stores/environments_store
|
||||
//= require ./mock_data
|
||||
/* globals environmentsList */
|
||||
(() => {
|
||||
beforeEach(() => {
|
||||
gl.environmentsList.EnvironmentsStore.create();
|
||||
});
|
||||
|
||||
describe('Store', () => {
|
||||
it('should start with a blank state', () => {
|
||||
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0);
|
||||
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0);
|
||||
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0);
|
||||
});
|
||||
|
||||
describe('store environments', () => {
|
||||
beforeEach(() => {
|
||||
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
|
||||
});
|
||||
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList);
|
||||
});
|
||||
|
||||
it('should toggle the open property for the given environment', () => {
|
||||
gl.environmentsList.EnvironmentsStore.toggleFolder('review');
|
||||
|
||||
const { environments } = gl.environmentsList.EnvironmentsStore.state;
|
||||
const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review');
|
||||
|
||||
expect(environment[0].isOpen).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
})();
|
|
@ -0,0 +1,135 @@
|
|||
/* eslint-disable no-unused-vars */
|
||||
const environmentsList = [
|
||||
{
|
||||
id: 31,
|
||||
name: 'production',
|
||||
state: 'available',
|
||||
external_url: 'https://www.gitlab.com',
|
||||
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: [],
|
||||
},
|
||||
'stoppable?': 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,
|
||||
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: [],
|
||||
},
|
||||
'stoppable?': false,
|
||||
environment_path: '/root/ci-folders/environments/31',
|
||||
created_at: '2016-11-07T11:11:16.525Z',
|
||||
updated_at: '2016-11-07T11:11:16.525Z',
|
||||
},
|
||||
{
|
||||
id: 33,
|
||||
name: 'test-environment',
|
||||
state: 'available',
|
||||
environment_type: 'review',
|
||||
last_deployment: null,
|
||||
'stoppable?': 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: 34,
|
||||
name: 'test-environment-1',
|
||||
state: 'available',
|
||||
environment_type: 'review',
|
||||
last_deployment: null,
|
||||
'stoppable?': true,
|
||||
environment_path: '/root/ci-folders/environments/31',
|
||||
created_at: '2016-11-07T11:11:16.525Z',
|
||||
updated_at: '2016-11-07T11:11:16.525Z',
|
||||
},
|
||||
];
|
|
@ -0,0 +1 @@
|
|||
.test-dom-element
|
|
@ -0,0 +1,9 @@
|
|||
%div
|
||||
#environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
|
||||
"can-create-deployment" => "true",
|
||||
"can-read-environment" => "true",
|
||||
"can-create-environment" => "true",
|
||||
"project-environments-path" => "https://gitlab.com/foo/environments",
|
||||
"project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped",
|
||||
"new-environment-path" => "https://gitlab.com/foo/environments/new",
|
||||
"help-page-path" => "https://gitlab.com/help_page"}}
|
|
@ -0,0 +1,11 @@
|
|||
%table
|
||||
%thead
|
||||
%tr
|
||||
%th Environment
|
||||
%th Last deployment
|
||||
%th Build
|
||||
%th Commit
|
||||
%th
|
||||
%th
|
||||
%tbody
|
||||
%tr#environment-row
|
|
@ -0,0 +1,126 @@
|
|||
//= require vue_common_component/commit
|
||||
|
||||
describe('Commit component', () => {
|
||||
let props;
|
||||
let component;
|
||||
|
||||
it('should render a code-fork icon if it does not represent a tag', () => {
|
||||
fixture.set('<div class="test-commit-container"></div>');
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
propsData: {
|
||||
tag: false,
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
},
|
||||
commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
short_sha: 'b7836edd',
|
||||
title: 'Commit message',
|
||||
author: {
|
||||
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
|
||||
web_url: 'https://gitlab.com/jschatz1',
|
||||
username: 'jschatz1',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-code-fork');
|
||||
});
|
||||
|
||||
describe('Given all the props', () => {
|
||||
beforeEach(() => {
|
||||
fixture.set('<div class="test-commit-container"></div>');
|
||||
|
||||
props = {
|
||||
tag: true,
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
},
|
||||
commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
short_sha: 'b7836edd',
|
||||
title: 'Commit message',
|
||||
author: {
|
||||
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
|
||||
web_url: 'https://gitlab.com/jschatz1',
|
||||
username: 'jschatz1',
|
||||
},
|
||||
};
|
||||
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
propsData: props,
|
||||
});
|
||||
});
|
||||
|
||||
it('should render a tag icon if it represents a tag', () => {
|
||||
expect(component.$el.querySelector('.icon-container i').classList).toContain('fa-tag');
|
||||
});
|
||||
|
||||
it('should render a link to the ref url', () => {
|
||||
expect(component.$el.querySelector('.branch-name').getAttribute('href')).toEqual(props.ref.ref_url);
|
||||
});
|
||||
|
||||
it('should render the ref name', () => {
|
||||
expect(component.$el.querySelector('.branch-name').textContent).toContain(props.ref.name);
|
||||
});
|
||||
|
||||
it('should render the commit short sha with a link to the commit url', () => {
|
||||
expect(component.$el.querySelector('.commit-id').getAttribute('href')).toEqual(props.commit_url);
|
||||
expect(component.$el.querySelector('.commit-id').textContent).toContain(props.short_sha);
|
||||
});
|
||||
|
||||
describe('Given commit title and author props', () => {
|
||||
it('Should render a link to the author profile', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href')
|
||||
).toEqual(props.author.web_url);
|
||||
});
|
||||
|
||||
it('Should render the author avatar with title and alt attributes', () => {
|
||||
expect(
|
||||
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('title')
|
||||
).toContain(props.author.username);
|
||||
expect(
|
||||
component.$el.querySelector('.commit-title .avatar-image-container img').getAttribute('alt')
|
||||
).toContain(`${props.author.username}'s avatar`);
|
||||
});
|
||||
});
|
||||
|
||||
it('should render the commit title', () => {
|
||||
expect(
|
||||
component.$el.querySelector('a.commit-row-message').getAttribute('href')
|
||||
).toEqual(props.commit_url);
|
||||
expect(
|
||||
component.$el.querySelector('a.commit-row-message').textContent
|
||||
).toContain(props.title);
|
||||
});
|
||||
});
|
||||
|
||||
describe('When commit title is not provided', () => {
|
||||
it('Should render default message', () => {
|
||||
fixture.set('<div class="test-commit-container"></div>');
|
||||
props = {
|
||||
tag: false,
|
||||
ref: {
|
||||
name: 'master',
|
||||
ref_url: 'http://localhost/namespace2/gitlabhq/tree/master',
|
||||
},
|
||||
commit_url: 'https://gitlab.com/gitlab-org/gitlab-ce/commit/b7836eddf62d663c665769e1b0960197fd215067',
|
||||
short_sha: 'b7836edd',
|
||||
title: null,
|
||||
author: {},
|
||||
};
|
||||
|
||||
component = new window.gl.CommitComponent({
|
||||
el: document.querySelector('.test-commit-container'),
|
||||
propsData: props,
|
||||
});
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.commit-title span').textContent
|
||||
).toContain('Cant find HEAD commit for this branch');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -10,9 +10,9 @@ describe BuildEntity do
|
|||
context 'when build is a regular job' do
|
||||
let(:build) { create(:ci_build) }
|
||||
|
||||
it 'contains url to build page and retry action' do
|
||||
expect(subject).to include(:build_url, :retry_url)
|
||||
expect(subject).not_to include(:play_url)
|
||||
it 'contains paths to build page and retry action' do
|
||||
expect(subject).to include(:build_path, :retry_path)
|
||||
expect(subject).not_to include(:play_path)
|
||||
end
|
||||
|
||||
it 'does not contain sensitive information' do
|
||||
|
@ -24,8 +24,8 @@ describe BuildEntity do
|
|||
context 'when build is a manual action' do
|
||||
let(:build) { create(:ci_build, :manual) }
|
||||
|
||||
it 'contains url to play action' do
|
||||
expect(subject).to include(:play_url)
|
||||
it 'contains path to play action' do
|
||||
expect(subject).to include(:play_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -31,7 +31,11 @@ describe CommitEntity do
|
|||
end
|
||||
end
|
||||
|
||||
it 'contains commit URL' do
|
||||
it 'contains path to commit' do
|
||||
expect(subject).to include(:commit_path)
|
||||
end
|
||||
|
||||
it 'contains URL to commit' do
|
||||
expect(subject).to include(:commit_url)
|
||||
end
|
||||
|
||||
|
|
|
@ -15,6 +15,6 @@ describe DeploymentEntity do
|
|||
|
||||
it 'exposes nested information about branch' do
|
||||
expect(subject[:ref][:name]).to eq 'master'
|
||||
expect(subject[:ref][:ref_url]).not_to be_empty
|
||||
expect(subject[:ref][:ref_path]).not_to be_empty
|
||||
end
|
||||
end
|
||||
|
|
|
@ -13,6 +13,6 @@ describe EnvironmentEntity do
|
|||
end
|
||||
|
||||
it 'exposes core elements of environment' do
|
||||
expect(subject).to include(:id, :name, :state, :environment_url)
|
||||
expect(subject).to include(:id, :name, :state, :environment_path)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -33,7 +33,7 @@ describe EnvironmentSerializer do
|
|||
|
||||
it 'contains important elements of environment' do
|
||||
expect(json)
|
||||
.to include(:name, :external_url, :environment_url, :last_deployment)
|
||||
.to include(:name, :external_url, :environment_path, :last_deployment)
|
||||
end
|
||||
|
||||
it 'contains relevant information about last deployment' do
|
||||
|
|
Loading…
Reference in New Issue