Commit f15340e0 authored by Fatih Acet's avatar Fatih Acet

Merge branch 'paginate-environments-bundle' into 'master'

Paginate environments bundle

Closes #25499

See merge request !9302
parents 87cbd45b ea7c7769
/* eslint-disable no-param-reassign, no-new */ /* eslint-disable no-param-reassign, no-new */
/* global Vue */
/* global EnvironmentsService */
/* global Flash */ /* global Flash */
window.Vue = require('vue'); const Vue = require('vue');
window.Vue.use(require('vue-resource')); Vue.use(require('vue-resource'));
require('../services/environments_service'); const EnvironmentsService = require('../services/environments_service');
require('./environment_item'); const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
(() => { module.exports = Vue.component('environment-component', {
window.gl = window.gl || {};
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
props: {
store: {
type: Object,
required: true,
default: () => ({}),
},
},
components: { components: {
'environment-item': gl.environmentsList.EnvironmentItem, 'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
}, },
data() { data() {
const environmentsData = document.querySelector('#environments-list-view').dataset; const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
return { return {
state: this.store.state, store,
state: store.state,
visibility: 'available', visibility: 'available',
isLoading: false, isLoading: false,
cssContainerClass: environmentsData.cssClass, cssContainerClass: environmentsData.cssClass,
...@@ -43,25 +37,30 @@ require('./environment_item'); ...@@ -43,25 +37,30 @@ require('./environment_item');
commitIconSvg: environmentsData.commitIconSvg, commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg, playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg, terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
}; };
}, },
computed: { computed: {
scope() { scope() {
return this.$options.getQueryParameter('scope'); return gl.utils.getParameterByName('scope');
}, },
canReadEnvironmentParsed() { canReadEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canReadEnvironment); return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
}, },
canCreateDeploymentParsed() { canCreateDeploymentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateDeployment); return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
}, },
canCreateEnvironmentParsed() { canCreateEnvironmentParsed() {
return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment);
}, },
}, },
/** /**
...@@ -69,19 +68,27 @@ require('./environment_item'); ...@@ -69,19 +68,27 @@ require('./environment_item');
* Toggles loading property. * Toggles loading property.
*/ */
created() { created() {
gl.environmentsService = new EnvironmentsService(this.endpoint); const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const scope = this.$options.getQueryParameter('scope'); const service = new EnvironmentsService(endpoint);
if (scope) {
this.store.storeVisibility(scope);
}
this.isLoading = true; this.isLoading = true;
return gl.environmentsService.all() return service.all()
.then(resp => resp.json()) .then(resp => ({
.then((json) => { headers: resp.headers,
this.store.storeEnvironments(json); body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false; this.isLoading = false;
}) })
.catch(() => { .catch(() => {
...@@ -90,33 +97,22 @@ require('./environment_item'); ...@@ -90,33 +97,22 @@ require('./environment_item');
}); });
}, },
/** methods: {
* Transforms the url parameter into an object and toggleRow(model) {
* returns the one requested. return this.store.toggleFolder(model.name);
*
* @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. * Will change the page number and update the URL.
* @param {String} string *
* @returns {Boolean} * @param {Number} pageNumber desired page to go to.
* @return {String}
*/ */
convertPermissionToBoolean(string) { changePage(pageNumber) {
return string === 'true'; const param = gl.utils.setParamInURL('page', pageNumber);
},
methods: { gl.utils.visitUrl(param);
toggleRow(model) { return param;
return this.store.toggleFolder(model.name);
}, },
}, },
...@@ -124,14 +120,15 @@ require('./environment_item'); ...@@ -124,14 +120,15 @@ require('./environment_item');
<div :class="cssContainerClass"> <div :class="cssContainerClass">
<div class="top-area"> <div class="top-area">
<ul v-if="!isLoading" class="nav-links"> <ul v-if="!isLoading" class="nav-links">
<li v-bind:class="{ 'active': scope === undefined }"> <li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath"> <a :href="projectEnvironmentsPath">
Available Available
<span class="badge js-available-environments-count"> <span class="badge js-available-environments-count">
{{state.availableCounter}} {{state.availableCounter}}
</span> </span>
</a> </a>
</li><li v-bind:class="{ 'active' : scope === 'stopped' }"> </li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath"> <a :href="projectStoppedEnvironmentsPath">
Stopped Stopped
<span class="badge js-stopped-environments-count"> <span class="badge js-stopped-environments-count">
...@@ -165,8 +162,7 @@ require('./environment_item'); ...@@ -165,8 +162,7 @@ require('./environment_item');
</a> </a>
</p> </p>
<a <a v-if="canCreateEnvironmentParsed"
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath" :href="newEnvironmentPath"
class="btn btn-create js-new-environment-button"> class="btn btn-create js-new-environment-button">
New Environment New Environment
...@@ -174,50 +170,23 @@ require('./environment_item'); ...@@ -174,50 +170,23 @@ require('./environment_item');
</div> </div>
<div class="table-holder" <div class="table-holder"
v-if="!isLoading && state.filteredEnvironments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in state.filteredEnvironments"
v-bind:model="model">
<tr
is="environment-item"
:model="model"
:toggleRow="toggleRow.bind(model)"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
<tr v-if="model.isOpen && model.children && model.children.length > 0" <environment-table
is="environment-item" :environments="state.environments"
v-for="children in model.children"
:model="children"
:toggleRow="toggleRow.bind(children)"
:can-create-deployment="canCreateDeploymentParsed" :can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed" :can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg" :play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg" :terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"> :commit-icon-svg="commitIconSvg">
</tr> </environment-table>
</template> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
</tbody> :change="changePage"
</table> :pageInfo="state.paginationInformation">
</table-pagination>
</div> </div>
</div> </div>
</div> </div>
`, `,
}); });
})();
/* global Vue */ const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('actions-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: { props: {
actions: { actions: {
type: Array, type: Array,
...@@ -46,5 +40,4 @@ window.Vue = require('vue'); ...@@ -46,5 +40,4 @@ window.Vue = require('vue');
</div> </div>
</div> </div>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('external-url-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: { props: {
externalUrl: { externalUrl: {
type: String, type: String,
...@@ -19,5 +16,4 @@ window.Vue = require('vue'); ...@@ -19,5 +16,4 @@ window.Vue = require('vue');
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ const Vue = require('vue');
/* global timeago */ const Timeago = require('timeago.js');
window.Vue = require('vue');
window.timeago = require('timeago.js');
require('../../lib/utils/text_utility'); require('../../lib/utils/text_utility');
require('../../vue_shared/components/commit'); require('../../vue_shared/components/commit');
require('./environment_actions'); const ActionsComponent = require('./environment_actions');
require('./environment_external_url'); const ExternalUrlComponent = require('./environment_external_url');
require('./environment_stop'); const StopComponent = require('./environment_stop');
require('./environment_rollback'); const RollbackComponent = require('./environment_rollback');
require('./environment_terminal_button'); const TerminalButtonComponent = require('./environment_terminal_button');
(() => { /**
/**
* Envrionment Item Component * Envrionment Item Component
* *
* Used in a hierarchical structure to show folders with children * Renders a table row for each environment.
* 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 || {}; const timeagoInstance = new Timeago();
window.gl.environmentsList = window.gl.environmentsList || {};
window.gl.environmentsList.timeagoInstance = new timeago(); // eslint-disable-line
gl.environmentsList.EnvironmentItem = Vue.component('environment-item', { module.exports = Vue.component('environment-item', {
components: { components: {
'commit-component': gl.CommitComponent, 'commit-component': gl.CommitComponent,
'actions-component': gl.environmentsList.ActionsComponent, 'actions-component': ActionsComponent,
'external-url-component': gl.environmentsList.ExternalUrlComponent, 'external-url-component': ExternalUrlComponent,
'stop-component': gl.environmentsList.StopComponent, 'stop-component': StopComponent,
'rollback-component': gl.environmentsList.RollbackComponent, 'rollback-component': RollbackComponent,
'terminal-button-component': gl.environmentsList.TerminalButtonComponent, 'terminal-button-component': TerminalButtonComponent,
}, },
props: { props: {
...@@ -45,11 +35,6 @@ require('./environment_terminal_button'); ...@@ -45,11 +35,6 @@ require('./environment_terminal_button');
default: () => ({}), default: () => ({}),
}, },
toggleRow: {
type: Function,
required: false,
},
canCreateDeployment: { canCreateDeployment: {
type: Boolean, type: Boolean,
required: false, required: false,
...@@ -76,50 +61,9 @@ require('./environment_terminal_button'); ...@@ -76,50 +61,9 @@ require('./environment_terminal_button');
type: String, type: String,
required: false, required: false,
}, },
},
data() {
return {
rowClass: {
'children-row': this.model['vue-isChildren'],
},
};
}, },
computed: { 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. * Verifies if `last_deployment` key exists in the current Envrionment.
* This key is required to render most of the html - this method works has * This key is required to render most of the html - this method works has
...@@ -128,7 +72,8 @@ require('./environment_terminal_button'); ...@@ -128,7 +72,8 @@ require('./environment_terminal_button');
* @returns {Boolean} * @returns {Boolean}
*/ */
hasLastDeploymentKey() { hasLastDeploymentKey() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
!this.$options.isObjectEmpty(this.model.last_deployment)) { !this.$options.isObjectEmpty(this.model.last_deployment)) {
return true; return true;
} }
...@@ -142,7 +87,9 @@ require('./environment_terminal_button'); ...@@ -142,7 +87,9 @@ require('./environment_terminal_button');
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
hasManualActions() { hasManualActions() {
return this.model.last_deployment && this.model.last_deployment.manual_actions && return this.model &&
this.model.last_deployment &&
this.model.last_deployment.manual_actions &&
this.model.last_deployment.manual_actions.length > 0; this.model.last_deployment.manual_actions.length > 0;
}, },
...@@ -152,7 +99,7 @@ require('./environment_terminal_button'); ...@@ -152,7 +99,7 @@ require('./environment_terminal_button');
* @returns {Boolean} * @returns {Boolean}
*/ */
hasStopAction() { hasStopAction() {
return this.model['stop_action?']; return this.model && this.model['stop_action?'];
}, },
/** /**
...@@ -162,7 +109,8 @@ require('./environment_terminal_button'); ...@@ -162,7 +109,8 @@ require('./environment_terminal_button');
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
canRetry() { canRetry() {
return this.hasLastDeploymentKey && return this.model &&
this.hasLastDeploymentKey &&
this.model.last_deployment && this.model.last_deployment &&
this.model.last_deployment.deployable; this.model.last_deployment.deployable;
}, },
...@@ -173,7 +121,8 @@ require('./environment_terminal_button'); ...@@ -173,7 +121,8 @@ require('./environment_terminal_button');
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
canShowDate() { canShowDate() {
return this.model.last_deployment && return this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable && this.model.last_deployment.deployable &&
this.model.last_deployment.deployable !== undefined; this.model.last_deployment.deployable !== undefined;
}, },
...@@ -184,9 +133,13 @@ require('./environment_terminal_button'); ...@@ -184,9 +133,13 @@ require('./environment_terminal_button');
* @returns {String} * @returns {String}
*/ */
createdDate() { createdDate() {
return gl.environmentsList.timeagoInstance.format( if (this.model &&
this.model.last_deployment.deployable.created_at, this.model.last_deployment &&
); this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.created_at) {
return timeagoInstance.format(this.model.last_deployment.deployable.created_at);
}
return '';
}, },
/** /**
...@@ -213,7 +166,8 @@ require('./environment_terminal_button'); ...@@ -213,7 +166,8 @@ require('./environment_terminal_button');
* @returns {String} * @returns {String}
*/ */
userImageAltDescription() { userImageAltDescription() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.user && this.model.last_deployment.user &&
this.model.last_deployment.user.username) { this.model.last_deployment.user.username) {
return `${this.model.last_deployment.user.username}'s avatar'`; return `${this.model.last_deployment.user.username}'s avatar'`;
...@@ -227,7 +181,8 @@ require('./environment_terminal_button'); ...@@ -227,7 +181,8 @@ require('./environment_terminal_button');
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitTag() { commitTag() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.tag) { this.model.last_deployment.tag) {
return this.model.last_deployment.tag; return this.model.last_deployment.tag;
} }
...@@ -240,7 +195,9 @@ require('./environment_terminal_button'); ...@@ -240,7 +195,9 @@ require('./environment_terminal_button');
* @returns {Object|Undefined} * @returns {Object|Undefined}
*/ */
commitRef() { commitRef() {
if (this.model.last_deployment && this.model.last_deployment.ref) { if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.ref) {
return this.model.last_deployment.ref; return this.model.last_deployment.ref;
} }
return undefined; return undefined;
...@@ -252,7 +209,8 @@ require('./environment_terminal_button'); ...@@ -252,7 +209,8 @@ require('./environment_terminal_button');
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitUrl() { commitUrl() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit && this.model.last_deployment.commit &&
this.model.last_deployment.commit.commit_path) { this.model.last_deployment.commit.commit_path) {
return this.model.last_deployment.commit.commit_path; return this.model.last_deployment.commit.commit_path;
...@@ -266,7 +224,8 @@ require('./environment_terminal_button'); ...@@ -266,7 +224,8 @@ require('./environment_terminal_button');
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitShortSha() { commitShortSha() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit && this.model.last_deployment.commit &&
this.model.last_deployment.commit.short_id) { this.model.last_deployment.commit.short_id) {
return this.model.last_deployment.commit.short_id; return this.model.last_deployment.commit.short_id;
...@@ -280,7 +239,8 @@ require('./environment_terminal_button'); ...@@ -280,7 +239,8 @@ require('./environment_terminal_button');
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
commitTitle() { commitTitle() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit && this.model.last_deployment.commit &&
this.model.last_deployment.commit.title) { this.model.last_deployment.commit.title) {
return this.model.last_deployment.commit.title; return this.model.last_deployment.commit.title;
...@@ -294,7 +254,8 @@ require('./environment_terminal_button'); ...@@ -294,7 +254,8 @@ require('./environment_terminal_button');
* @returns {Object|Undefined} * @returns {Object|Undefined}
*/ */
commitAuthor() { commitAuthor() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.commit && this.model.last_deployment.commit &&
this.model.last_deployment.commit.author) { this.model.last_deployment.commit.author) {
return this.model.last_deployment.commit.author; return this.model.last_deployment.commit.author;
...@@ -309,7 +270,8 @@ require('./environment_terminal_button'); ...@@ -309,7 +270,8 @@ require('./environment_terminal_button');
* @returns {String|Undefined} * @returns {String|Undefined}
*/ */
retryUrl() { retryUrl() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable && this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.retry_path) { this.model.last_deployment.deployable.retry_path) {
return this.model.last_deployment.deployable.retry_path; return this.model.last_deployment.deployable.retry_path;
...@@ -323,7 +285,8 @@ require('./environment_terminal_button'); ...@@ -323,7 +285,8 @@ require('./environment_terminal_button');
* @returns {Boolean|Undefined} * @returns {Boolean|Undefined}
*/ */
isLastDeployment() { isLastDeployment() {
return this.model.last_deployment && this.model.last_deployment['last?']; return this.model && this.model.last_deployment &&
this.model.last_deployment['last?'];
}, },
/** /**
...@@ -332,7 +295,8 @@ require('./environment_terminal_button'); ...@@ -332,7 +295,8 @@ require('./environment_terminal_button');
* @returns {String} * @returns {String}
*/ */
buildName() { buildName() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable) { this.model.last_deployment.deployable) {
return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`;
} }
...@@ -345,7 +309,8 @@ require('./environment_terminal_button'); ...@@ -345,7 +309,8 @@ require('./environment_terminal_button');
* @returns {String} * @returns {String}
*/ */
deploymentInternalId() { deploymentInternalId() {
if (this.model.last_deployment && if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.iid) { this.model.last_deployment.iid) {
return `#${this.model.last_deployment.iid}`; return `#${this.model.last_deployment.iid}`;
} }
...@@ -358,7 +323,8 @@ require('./environment_terminal_button'); ...@@ -358,7 +323,8 @@ require('./environment_terminal_button');
* @returns {Boolean} * @returns {Boolean}
*/ */
deploymentHasUser() { deploymentHasUser() {
return !this.$options.isObjectEmpty(this.model.last_deployment) && return this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user); !this.$options.isObjectEmpty(this.model.last_deployment.user);
}, },
...@@ -369,7 +335,8 @@ require('./environment_terminal_button'); ...@@ -369,7 +335,8 @@ require('./environment_terminal_button');
* @returns {Object} * @returns {Object}
*/ */
deploymentUser() { deploymentUser() {
if (!this.$options.isObjectEmpty(this.model.last_deployment) && if (this.model &&
!this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.user)) { !this.$options.isObjectEmpty(this.model.last_deployment.user)) {
return this.model.last_deployment.user; return this.model.last_deployment.user;
} }
...@@ -384,11 +351,40 @@ require('./environment_terminal_button'); ...@@ -384,11 +351,40 @@ require('./environment_terminal_button');
* @returns {Boolean} * @returns {Boolean}
*/ */
shouldRenderBuildName() { shouldRenderBuildName() {
return !this.isFolder && return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) && !this.$options.isObjectEmpty(this.model.last_deployment) &&
!this.$options.isObjectEmpty(this.model.last_deployment.deployable); !this.$options.isObjectEmpty(this.model.last_deployment.deployable);
}, },
/**
* Verifies the presence of all the keys needed to render the buil_path.
*
* @return {String}
*/
buildPath() {
if (this.model &&
this.model.last_deployment &&
this.model.last_deployment.deployable &&
this.model.last_deployment.deployable.build_path) {
return this.model.last_deployment.deployable.build_path;
}
return '';
},
/**
* Verifies the presence of all the keys needed to render the external_url.
*
* @return {String}
*/
externalURL() {
if (this.model && this.model.external_url) {
return this.model.external_url;
}
return '';
},
/** /**
* Verifies if deplyment internal ID should be rendered by verifing * Verifies if deplyment internal ID should be rendered by verifing
* if all the information needed is present * if all the information needed is present
...@@ -397,10 +393,28 @@ require('./environment_terminal_button'); ...@@ -397,10 +393,28 @@ require('./environment_terminal_button');
* @returns {Boolean} * @returns {Boolean}
*/ */
shouldRenderDeploymentID() { shouldRenderDeploymentID() {
return !this.isFolder && return !this.model.isFolder &&
!this.$options.isObjectEmpty(this.model.last_deployment) && !this.$options.isObjectEmpty(this.model.last_deployment) &&
this.model.last_deployment.iid !== undefined; this.model.last_deployment.iid !== undefined;
}, },
environmentPath() {
if (this.model && this.model.environment_path) {
return this.model.environment_path;
}
return '';
},
/**
* Constructs folder URL based on the current location and the folder id.
*
* @return {String}
*/
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
}, },
/** /**
...@@ -420,26 +434,25 @@ require('./environment_terminal_button'); ...@@ -420,26 +434,25 @@ require('./environment_terminal_button');
template: ` template: `
<tr> <tr>
<td v-bind:class="{ 'children-row': isChildren}"> <td>
<a v-if="!isFolder" <a v-if="!model.isFolder"
class="environment-name" class="environment-name"
:href="model.environment_path"> :href="environmentPath">
{{model.name}} {{model.name}}
</a> </a>
<span v-else v-on:click="toggleRow(model)" class="folder-name"> <a v-else class="folder-name" :href="folderUrl">
<span class="folder-icon"> <span class="folder-icon">
<i v-show="model.isOpen" class="fa fa-caret-down"></i> <i class="fa fa-folder" aria-hidden="true"></i>
<i v-show="!model.isOpen" class="fa fa-caret-right"></i>
</span> </span>
<span> <span>
{{model.name}} {{model.folderName}}
</span> </span>
<span class="badge"> <span class="badge">
{{childrenCounter}} {{model.size}}
</span>
</span> </span>
</a>
</td> </td>
<td class="deployment-column"> <td class="deployment-column">
...@@ -447,7 +460,7 @@ require('./environment_terminal_button'); ...@@ -447,7 +460,7 @@ require('./environment_terminal_button');
{{deploymentInternalId}} {{deploymentInternalId}}
</span> </span>
<span v-if="!isFolder && deploymentHasUser"> <span v-if="!model.isFolder && deploymentHasUser">
by by
<a :href="deploymentUser.web_url" class="js-deploy-user-container"> <a :href="deploymentUser.web_url" class="js-deploy-user-container">
<img class="avatar has-tooltip s20" <img class="avatar has-tooltip s20"
...@@ -461,13 +474,13 @@ require('./environment_terminal_button'); ...@@ -461,13 +474,13 @@ require('./environment_terminal_button');
<td class="environments-build-cell"> <td class="environments-build-cell">
<a v-if="shouldRenderBuildName" <a v-if="shouldRenderBuildName"
class="build-link" class="build-link"
:href="model.last_deployment.deployable.build_path"> :href="buildPath">
{{buildName}} {{buildName}}
</a> </a>
</td> </td>
<td> <td>
<div v-if="!isFolder && hasLastDeploymentKey" class="js-commit-component"> <div v-if="!model.isFolder && hasLastDeploymentKey" class="js-commit-component">
<commit-component <commit-component
:tag="commitTag" :tag="commitTag"
:commit-ref="commitRef" :commit-ref="commitRef"
...@@ -478,21 +491,20 @@ require('./environment_terminal_button'); ...@@ -478,21 +491,20 @@ require('./environment_terminal_button');
:commit-icon-svg="commitIconSvg"> :commit-icon-svg="commitIconSvg">
</commit-component> </commit-component>
</div> </div>
<p v-if="!isFolder && !hasLastDeploymentKey" class="commit-title"> <p v-if="!model.isFolder && !hasLastDeploymentKey" class="commit-title">
No deployments yet No deployments yet
</p> </p>
</td> </td>
<td> <td>
<span <span v-if="!model.isFolder && canShowDate"
v-if="!isFolder && canShowDate"
class="environment-created-date-timeago"> class="environment-created-date-timeago">
{{createdDate}} {{createdDate}}
</span> </span>
</td> </td>
<td class="hidden-xs"> <td class="hidden-xs">
<div v-if="!isFolder"> <div v-if="!model.isFolder">
<div v-if="hasManualActions && canCreateDeployment" <div v-if="hasManualActions && canCreateDeployment"
class="inline js-manual-actions-container"> class="inline js-manual-actions-container">
<actions-component <actions-component
...@@ -501,10 +513,10 @@ require('./environment_terminal_button'); ...@@ -501,10 +513,10 @@ require('./environment_terminal_button');
</actions-component> </actions-component>
</div> </div>
<div v-if="model.external_url && canReadEnvironment" <div v-if="externalURL && canReadEnvironment"
class="inline js-external-url-container"> class="inline js-external-url-container">
<external-url-component <external-url-component
:external-url="model.external_url"> :external-url="externalURL">
</external-url-component> </external-url-component>
</div> </div>
...@@ -515,7 +527,7 @@ require('./environment_terminal_button'); ...@@ -515,7 +527,7 @@ require('./environment_terminal_button');
</stop-component> </stop-component>
</div> </div>
<div v-if="model.terminal_path" <div v-if="model && model.terminal_path"
class="inline js-terminal-button-container"> class="inline js-terminal-button-container">
<terminal-button-component <terminal-button-component
:terminal-icon-svg="terminalIconSvg" :terminal-icon-svg="terminalIconSvg"
...@@ -534,5 +546,4 @@ require('./environment_terminal_button'); ...@@ -534,5 +546,4 @@ require('./environment_terminal_button');
</td> </td>
</tr> </tr>
`, `,
}); });
})();
/* global Vue */ /**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('rollback-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: { props: {
retryUrl: { retryUrl: {
type: String, type: String,
...@@ -29,5 +27,4 @@ window.Vue = require('vue'); ...@@ -29,5 +27,4 @@ window.Vue = require('vue');
</span> </span>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('stop-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: { props: {
stopUrl: { stopUrl: {
type: String, type: String,
...@@ -20,8 +18,7 @@ window.Vue = require('vue'); ...@@ -20,8 +18,7 @@ window.Vue = require('vue');
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('terminal-button-component', {
(() => {
window.gl = window.gl || {};
window.gl.environmentsList = window.gl.environmentsList || {};
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', {
props: { props: {
terminalPath: { terminalPath: {
type: String, type: String,
...@@ -24,5 +22,4 @@ window.Vue = require('vue'); ...@@ -24,5 +22,4 @@ window.Vue = require('vue');
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a> </a>
`, `,
}); });
})();
/**
* Render environments table.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
},
props: {
environments: {
type: Array,
required: true,
default: () => ([]),
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
template: `
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
</template>
</tbody>
</table>
`,
});
window.Vue = require('vue'); const EnvironmentsComponent = require('./components/environment');
require('./stores/environments_store');
require('./components/environment');
require('../vue_shared/vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
...@@ -9,14 +7,8 @@ $(() => { ...@@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) { if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true); gl.EnvironmentsListApp.$destroy(true);
} }
const Store = gl.environmentsList.EnvironmentsStore;
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
}); });
}); });
const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListFolderApp) {
gl.EnvironmentsListFolderApp.$destroy(true);
}
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
const Vue = require('vue');
Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
module.exports = Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
},
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
const pathname = window.location.pathname;
const endpoint = `${pathname}.json`;
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
return {
store,
folderName,
endpoint,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return gl.utils.getParameterByName('scope');
},
canReadEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* URL to link in the stopped tab.
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
},
/**
* URL to link in the available tab.
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area" v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div>
</div>
</div>
`,
});
/* globals Vue */ const Vue = require('vue');
/* eslint-disable no-unused-vars, no-param-reassign */
class EnvironmentsService { class EnvironmentsService {
constructor(endpoint) {
constructor(root) { this.environments = Vue.resource(endpoint);
Vue.http.options.root = root;
this.environments = Vue.resource(root);
Vue.http.interceptors.push((request, next) => {
// needed in order to not break the tests.
if ($.rails) {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
});
} }
all() { all() {
...@@ -22,4 +10,4 @@ class EnvironmentsService { ...@@ -22,4 +10,4 @@ class EnvironmentsService {
} }
} }
window.EnvironmentsService = EnvironmentsService; module.exports = EnvironmentsService;
/* eslint-disable no-param-reassign */ require('~/lib/utils/common_utils');
(() => { /**
window.gl = window.gl || {}; * Environments Store.
window.gl.environmentsList = window.gl.environmentsList || {}; *
* Stores received environments, count of stopped environments and count of
gl.environmentsList.EnvironmentsStore = { * available environments.
state: {}, */
class EnvironmentsStore {
create() { constructor() {
this.state = {};
this.state.environments = []; this.state.environments = [];
this.state.stoppedCounter = 0; this.state.stoppedCounter = 0;
this.state.availableCounter = 0; this.state.availableCounter = 0;
this.state.visibility = 'available'; this.state.paginationInformation = {};
this.state.filteredEnvironments = [];
return this; return this;
}, }
/** /**
* In order to display a tree view we need to modify the received
* data in to a tree structure based on `environment_type`
* 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.
* *
* Stores the received environments.
* *
* @example * In the main environments endpoint, each environment has the following schema
* it will transform this: * { name: String, size: Number, latest: Object }
* [ * In the endpoint to retrieve environments from each folder, the environment does
* { name: "environment", environment_type: "review" }, * not have the `latest` key and the data is all in the root level.
* { name: "environment_1", environment_type: null } * To avoid doing this check in the view, we store both cases the same by extracting
* { name: "environment_2, environment_type: "review" } * what is inside the `latest` key.
* ]
* 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}
* ]
* *
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
* *
* @param {Array} environments List of environments. * @param {Array} environments
* @returns {Array} Tree structured array with the received environments. * @returns {Array}
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
this.state.stoppedCounter = this.countByState(environments, 'stopped'); const filteredEnvironments = environments.map((env) => {
this.state.availableCounter = this.countByState(environments, 'available'); let filtered = {};
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) { if (env.size > 1) {
acc[acc.indexOf(occurs[0])].children.push(environment); filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
} else {
acc.push({
name: environment.environment_type,
children: [environment],
isOpen: false,
'vue-isChildren': environment['vue-isChildren'],
});
} }
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
delete filtered.latest;
} else { } else {
acc.push(environment); filtered = Object.assign(filtered, env);
} }
return acc; return filtered;
}, []).slice().sort(this.sortByName); });
this.state.environments = environmentsTree;
this.filterEnvironmentsByVisibility(this.state.environments);
return environmentsTree;
},
storeVisibility(visibility) {
this.state.visibility = visibility;
},
/**
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item); this.state.environments = filteredEnvironments;
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments; return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/**
* Toggles folder open property given the environment type.
*
* @param {String} envType
* @return {Array}
*/
toggleFolder(envType) {
const environments = this.state.environments;
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
} }
return env; setPagination(pagination = {}) {
}); const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
this.state.environments = environmentsCopy;
return environmentsCopy; this.state.paginationInformation = paginationInformation;
}, return paginationInformation;
}
/** /**
* Given an array of environments, returns the number of environments * Stores the number of available environments.
* that have the given state.
* *
* @param {Array} environments * @param {Number} count = 0
* @param {String} state * @return {Number}
* @returns {Number}
*/ */
countByState(environments, state) { storeAvailableCount(count = 0) {
return environments.filter(env => env.state === state).length; this.state.availableCounter = count;
}, return count;
}
/** /**
* Sorts the two objects provided by their name. * Stores the number of closed environments.
* *
* @param {Object} a * @param {Number} count = 0
* @param {Object} b * @return {Number}
* @returns {Number}
*/ */
sortByName(a, b) { storeStoppedCount(count = 0) {
const nameA = a.name.toUpperCase(); this.state.stoppedCounter = count;
const nameB = b.name.toUpperCase(); return count;
}
}
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line module.exports = EnvironmentsStore;
},
};
})();
...@@ -231,6 +231,21 @@ ...@@ -231,6 +231,21 @@
return upperCaseHeaders; return upperCaseHeaders;
}; };
/**
* Parses pagination object string values into numbers.
*
* @param {Object} paginationInformation
* @returns {Object}
*/
w.gl.utils.parseIntPagination = paginationInformation => ({
perPage: parseInt(paginationInformation['X-PER-PAGE'], 10),
page: parseInt(paginationInformation['X-PAGE'], 10),
total: parseInt(paginationInformation['X-TOTAL'], 10),
totalPages: parseInt(paginationInformation['X-TOTAL-PAGES'], 10),
nextPage: parseInt(paginationInformation['X-NEXT-PAGE'], 10),
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
/** /**
* Transforms a DOMStringMap into a plain object. * Transforms a DOMStringMap into a plain object.
* *
...@@ -241,5 +256,45 @@ ...@@ -241,5 +256,45 @@
acc[element] = DOMStringMapObject[element]; acc[element] = DOMStringMapObject[element];
return acc; return acc;
}, {}); }, {});
/**
* Updates the search parameter of a URL given the parameter and values provided.
*
* If no search params are present we'll add it.
* If param for page is already present, we'll update it
* If there are params but not for the given one, we'll add it at the end.
* Returns the new search parameters.
*
* @param {String} param
* @param {Number|String|Undefined|Null} value
* @return {String}
*/
w.gl.utils.setParamInURL = (param, value) => {
let search;
const locationSearch = window.location.search;
if (locationSearch.length === 0) {
search = `?${param}=${value}`;
}
if (locationSearch.indexOf(param) !== -1) {
const regex = new RegExp(param + '=\\d');
search = locationSearch.replace(regex, `${param}=${value}`);
}
if (locationSearch.length && locationSearch.indexOf(param) === -1) {
search = `${locationSearch}&${param}=${value}`;
}
return search;
};
/**
* Converts permission provided as strings to booleans.
*
* @param {String} string
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
})(window); })(window);
}).call(this); }).call(this);
...@@ -35,7 +35,16 @@ require('../vue_shared/components/pipelines_table'); ...@@ -35,7 +35,16 @@ require('../vue_shared/components/pipelines_table');
this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope); this.store.fetchDataLoop.call(this, Vue, this.pagenum, this.scope, this.apiScope);
}, },
methods: { methods: {
change(pagenum, apiScope) {
/**
* Changes the URL according to the pagination component.
*
* If no scope is provided, 'all' is assumed.
*
* @param {Number} pagenum
* @param {String} apiScope = 'all'
*/
change(pagenum, apiScope = 'all') {
gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`); gl.utils.visitUrl(`?scope=${apiScope}&p=${pagenum}`);
}, },
}, },
......
...@@ -5,16 +5,7 @@ require('../vue_realtime_listener'); ...@@ -5,16 +5,7 @@ require('../vue_realtime_listener');
((gl) => { ((gl) => {
const pageValues = (headers) => { const pageValues = (headers) => {
const normalized = gl.utils.normalizeHeaders(headers); const normalized = gl.utils.normalizeHeaders(headers);
const paginationInfo = gl.utils.normalizeHeaders(normalized);
const paginationInfo = {
perPage: +normalized['X-PER-PAGE'],
page: +normalized['X-PAGE'],
total: +normalized['X-TOTAL'],
totalPages: +normalized['X-TOTAL-PAGES'],
nextPage: +normalized['X-NEXT-PAGE'],
previousPage: +normalized['X-PREV-PAGE'],
};
return paginationInfo; return paginationInfo;
}; };
......
/* global Vue */ /* global Vue */
window.Vue = require('vue');
(() => { (() => {
window.gl = window.gl || {}; window.gl = window.gl || {};
......
...@@ -57,9 +57,7 @@ window.Vue = require('vue'); ...@@ -57,9 +57,7 @@ window.Vue = require('vue');
}, },
methods: { methods: {
changePage(e) { changePage(e) {
let apiScope = gl.utils.getParameterByName('scope'); const apiScope = gl.utils.getParameterByName('scope');
if (!apiScope) apiScope = 'all';
const text = e.target.innerText; const text = e.target.innerText;
const { totalPages, nextPage, previousPage } = this.pageInfo; const { totalPages, nextPage, previousPage } = this.pageInfo;
......
...@@ -10,6 +10,11 @@ ...@@ -10,6 +10,11 @@
font-size: 34px; font-size: 34px;
} }
.environments-folder-name {
font-weight: normal;
padding-top: 20px;
}
@media (max-width: $screen-xs-max) { @media (max-width: $screen-xs-max) {
.environments-container { .environments-container {
width: 100%; width: 100%;
...@@ -110,17 +115,20 @@ ...@@ -110,17 +115,20 @@
} }
} }
.children-row .environment-name {
margin-left: 17px;
margin-right: -17px;
}
.folder-icon { .folder-icon {
padding: 0 5px 0 0; margin-right: 3px;
color: $gl-text-color-secondary;
display: inline-block;
.fa:nth-child(1) {
margin-right: 3px;
}
} }
.folder-name { .folder-name {
cursor: pointer; cursor: pointer;
color: $gl-text-color-secondary;
display: inline-block;
} }
} }
......
...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -9,15 +9,40 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :verify_api_request!, only: :terminal_websocket_authorize before_action :verify_api_request!, only: :terminal_websocket_authorize
def index def index
@scope = params[:scope] @environments = project.environments
@environments = project.environments.includes(:last_deployment) .with_state(params[:scope] || :available)
respond_to do |format| respond_to do |format|
format.html format.html
format.json do format.json do
render json: EnvironmentSerializer render json: {
.new(project: @project, user: current_user) environments: EnvironmentSerializer
.represent(@environments) .new(project: @project, user: @current_user)
.with_pagination(request, response)
.within_folders
.represent(@environments),
available_count: project.environments.available.count,
stopped_count: project.environments.stopped.count
}
end
end
end
def folder
folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available)
respond_to do |format|
format.html
format.json do
render json: {
environments: EnvironmentSerializer
.new(project: @project, user: @current_user)
.with_pagination(request, response)
.represent(@environments),
available_count: folder_environments.available.count,
stopped_count: folder_environments.stopped.count
}
end end
end end
end end
......
...@@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer ...@@ -20,8 +20,6 @@ class EnvironmentSerializer < BaseSerializer
end end
def represent(resource, opts = {}) def represent(resource, opts = {})
resource = @paginator.paginate(resource) if paginated?
if itemized? if itemized?
itemize(resource).map do |item| itemize(resource).map do |item|
{ name: item.name, { name: item.name,
...@@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer ...@@ -29,6 +27,8 @@ class EnvironmentSerializer < BaseSerializer
latest: super(item.latest, opts) } latest: super(item.latest, opts) }
end end
else else
resource = @paginator.paginate(resource) if paginated?
super(resource, opts) super(resource, opts)
end end
end end
...@@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer ...@@ -36,15 +36,20 @@ class EnvironmentSerializer < BaseSerializer
private private
def itemize(resource) def itemize(resource)
items = resource.group(:item_name).order('item_name ASC') items = resource.order('folder_name ASC')
.pluck('COALESCE(environment_type, name) AS item_name', .group('COALESCE(environment_type, name)')
'COUNT(*) AS environments_count', .select('COALESCE(environment_type, name) AS folder_name',
'MAX(id) AS last_environment_id') 'COUNT(*) AS size', 'MAX(id) AS last_id')
# It makes a difference when you call `paginate` method, because
# although `page` is effective at the end, it calls counting methods
# immediately.
items = @paginator.paginate(items) if paginated?
environments = resource.where(id: items.map(&:last)).index_by(&:id) environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
items.map do |name, size, id| items.map do |item|
Item.new(name, size, environments[id]) Item.new(item.folder_name, item.size, environments[item.last_id])
end end
end end
end end
- @no_container = true
- page_title "Environments"
= render "projects/pipelines/head"
- content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class,
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play") } }
---
title: Adds paginationd and folders view to environments table
merge_request:
author:
...@@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do ...@@ -156,6 +156,10 @@ constraints(ProjectUrlConstrainer.new) do
get :terminal get :terminal
get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil } get '/terminal.ws/authorize', to: 'environments#terminal_websocket_authorize', constraints: { format: nil }
end end
collection do
get :folder, path: 'folders/:id'
end
end end
resource :cycle_analytics, only: [:show] resource :cycle_analytics, only: [:show]
......
...@@ -22,6 +22,7 @@ var config = { ...@@ -22,6 +22,7 @@ var config = {
commit_pipelines: './commit/pipelines/pipelines_bundle.js', commit_pipelines: './commit/pipelines/pipelines_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js', environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
filtered_search: './filtered_search/filtered_search_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js',
graphs: './graphs/graphs_bundle.js', graphs: './graphs/graphs_bundle.js',
issuable: './issuable/issuable_bundle.js', issuable: './issuable/issuable_bundle.js',
......
...@@ -3,9 +3,12 @@ require 'spec_helper' ...@@ -3,9 +3,12 @@ require 'spec_helper'
describe Projects::EnvironmentsController do describe Projects::EnvironmentsController do
include ApiHelpers include ApiHelpers
let(:environment) { create(:environment) }
let(:project) { environment.project }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:empty_project) }
let(:environment) do
create(:environment, name: 'production', project: project)
end
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do ...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do
end end
end end
context 'when requesting JSON response' do context 'when requesting JSON response for folders' do
it 'responds with correct JSON' do before do
get :index, environment_params(format: :json) create(:environment, project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
name: 'staging/review-2',
state: :available)
create(:environment, project: project,
name: 'staging/review-3',
state: :stopped)
end
let(:environments) { json_response['environments'] }
context 'when requesting available environments scope' do
before do
get :index, environment_params(format: :json, scope: :available)
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
end
it 'contains values describing environment scopes sizes' do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end
context 'when requesting stopped environments scope' do
before do
get :index, environment_params(format: :json, scope: :stopped)
end
first_environment = json_response.first it 'responds with a payload describing stopped environments' do
expect(environments.count).to eq 1
expect(environments.first['name']).to eq 'staging'
expect(environments.first['size']).to eq 1
expect(environments.first['latest']['name']).to eq 'staging/review-3'
end
expect(first_environment).not_to be_empty it 'contains values describing environment scopes sizes' do
expect(first_environment['name']). to eq environment.name expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end end
end end
end end
......
...@@ -275,7 +275,7 @@ feature 'Builds', :feature do ...@@ -275,7 +275,7 @@ feature 'Builds', :feature do
let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) } let!(:deployment) { create(:deployment, environment: environment, sha: project.commit.id) }
let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) } let(:build) { create(:ci_build, :success, environment: environment.name, pipeline: pipeline) }
it 'shows a link to lastest deployment' do it 'shows a link to latest deployment' do
visit namespace_project_build_path(project.namespace, project, build) visit namespace_project_build_path(project.namespace, project, build)
expect(page).to have_link('latest deployment') expect(page).to have_link('latest deployment')
......
require('~/environments/components/environment_actions'); const ActionsComponent = require('~/environments/components/environment_actions');
describe('Actions Component', () => { describe('Actions Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -19,7 +19,7 @@ describe('Actions Component', () => { ...@@ -19,7 +19,7 @@ describe('Actions Component', () => {
}, },
]; ];
const component = new window.gl.environmentsList.ActionsComponent({ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
actions: actionsMock, actions: actionsMock,
...@@ -47,7 +47,7 @@ describe('Actions Component', () => { ...@@ -47,7 +47,7 @@ describe('Actions Component', () => {
}, },
]; ];
const component = new window.gl.environmentsList.ActionsComponent({ const component = new ActionsComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
actions: actionsMock, actions: actionsMock,
......
require('~/environments/components/environment_external_url'); const ExternalUrlComponent = require('~/environments/components/environment_external_url');
describe('External URL Component', () => { describe('External URL Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -8,7 +8,7 @@ describe('External URL Component', () => { ...@@ -8,7 +8,7 @@ describe('External URL Component', () => {
it('should link to the provided externalUrl prop', () => { it('should link to the provided externalUrl prop', () => {
const externalURL = 'https://gitlab.com'; const externalURL = 'https://gitlab.com';
const component = new window.gl.environmentsList.ExternalUrlComponent({ const component = new ExternalUrlComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
externalUrl: externalURL, externalUrl: externalURL,
......
window.timeago = require('timeago.js'); window.timeago = require('timeago.js');
require('~/environments/components/environment_item'); const EnvironmentItem = require('~/environments/components/environment_item');
describe('Environment item', () => { describe('Environment item', () => {
preloadFixtures('static/environments/table.html.raw'); preloadFixtures('static/environments/table.html.raw');
...@@ -14,33 +14,16 @@ describe('Environment item', () => { ...@@ -14,33 +14,16 @@ describe('Environment item', () => {
beforeEach(() => { beforeEach(() => {
mockItem = { mockItem = {
name: 'review', name: 'review',
children: [ folderName: 'review',
{ size: 3,
name: 'review-app', isFolder: true,
id: 1, environment_path: 'url',
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
{
name: 'production',
id: 2,
state: 'available',
external_url: '',
last_deployment: {},
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
},
],
}; };
component = new window.gl.environmentsList.EnvironmentItem({ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'), el: document.querySelector('tr#environment-row'),
propsData: { propsData: {
model: mockItem, model: mockItem,
toggleRow: () => {},
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true, canReadEnvironment: true,
}, },
...@@ -53,7 +36,7 @@ describe('Environment item', () => { ...@@ -53,7 +36,7 @@ describe('Environment item', () => {
}); });
it('Should render the number of children in a badge', () => { it('Should render the number of children in a badge', () => {
expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.children.length); expect(component.$el.querySelector('.folder-name .badge').textContent).toContain(mockItem.size);
}); });
}); });
...@@ -63,8 +46,8 @@ describe('Environment item', () => { ...@@ -63,8 +46,8 @@ describe('Environment item', () => {
beforeEach(() => { beforeEach(() => {
environment = { environment = {
id: 31,
name: 'production', name: 'production',
size: 1,
state: 'stopped', state: 'stopped',
external_url: 'http://external.com', external_url: 'http://external.com',
environment_type: null, environment_type: null,
...@@ -125,11 +108,10 @@ describe('Environment item', () => { ...@@ -125,11 +108,10 @@ describe('Environment item', () => {
updated_at: '2016-11-10T15:55:58.778Z', updated_at: '2016-11-10T15:55:58.778Z',
}; };
component = new window.gl.environmentsList.EnvironmentItem({ component = new EnvironmentItem({
el: document.querySelector('tr#environment-row'), el: document.querySelector('tr#environment-row'),
propsData: { propsData: {
model: environment, model: environment,
toggleRow: () => {},
canCreateDeployment: true, canCreateDeployment: true,
canReadEnvironment: true, canReadEnvironment: true,
}, },
......
require('~/environments/components/environment_rollback'); const RollbackComponent = require('~/environments/components/environment_rollback');
describe('Rollback Component', () => { describe('Rollback Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -10,7 +10,7 @@ describe('Rollback Component', () => { ...@@ -10,7 +10,7 @@ describe('Rollback Component', () => {
}); });
it('Should link to the provided retryUrl', () => { it('Should link to the provided retryUrl', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
...@@ -22,7 +22,7 @@ describe('Rollback Component', () => { ...@@ -22,7 +22,7 @@ describe('Rollback Component', () => {
}); });
it('Should render Re-deploy label when isLastDeployment is true', () => { it('Should render Re-deploy label when isLastDeployment is true', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
...@@ -34,7 +34,7 @@ describe('Rollback Component', () => { ...@@ -34,7 +34,7 @@ describe('Rollback Component', () => {
}); });
it('Should render Rollback label when isLastDeployment is false', () => { it('Should render Rollback label when isLastDeployment is false', () => {
const component = new window.gl.environmentsList.RollbackComponent({ const component = new RollbackComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
retryUrl: retryURL, retryUrl: retryURL,
......
/* global Vue, environment */ const Vue = require('vue');
require('~/flash'); require('~/flash');
require('~/environments/stores/environments_store'); const EnvironmentsComponent = require('~/environments/components/environment');
require('~/environments/components/environment'); const { environment } = require('./mock_data');
require('./mock_data');
describe('Environment', () => { describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw'); preloadFixtures('static/environments/environments.html.raw');
...@@ -33,11 +31,8 @@ describe('Environment', () => { ...@@ -33,11 +31,8 @@ describe('Environment', () => {
}); });
it('should render the empty state', (done) => { it('should render the empty state', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
}); });
setTimeout(() => { setTimeout(() => {
...@@ -54,15 +49,30 @@ describe('Environment', () => { ...@@ -54,15 +49,30 @@ describe('Environment', () => {
}); });
}); });
describe('with environments', () => { describe('with paginated environments', () => {
const environmentsResponseInterceptor = (request, next) => { const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([environment]), { next(request.respondWith(JSON.stringify({
environments: [environment],
stopped_count: 1,
available_count: 0,
}), {
status: 200, status: 200,
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
})); }));
}; };
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -72,13 +82,6 @@ describe('Environment', () => { ...@@ -72,13 +82,6 @@ describe('Environment', () => {
}); });
it('should render a table with environments', (done) => { it('should render a table with environments', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
});
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelectorAll('table tbody tr').length, component.$el.querySelectorAll('table tbody tr').length,
...@@ -86,6 +89,59 @@ describe('Environment', () => { ...@@ -86,6 +89,59 @@ describe('Environment', () => {
done(); done();
}, 0); }, 0);
}); });
describe('pagination', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
done();
}, 0);
});
it('should update url when no search params are present', (done) => {
spyOn(gl.utils, 'visitUrl');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page is already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page and scope are already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1&scope=all');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done();
}, 0);
});
});
}); });
}); });
...@@ -107,11 +163,8 @@ describe('Environment', () => { ...@@ -107,11 +163,8 @@ describe('Environment', () => {
}); });
it('should render empty state', (done) => { it('should render empty state', (done) => {
component = new gl.environmentsList.EnvironmentsComponent({ component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: gl.environmentsList.EnvironmentsStore.create(),
},
}); });
setTimeout(() => { setTimeout(() => {
......
require('~/environments/components/environment_stop'); const StopComponent = require('~/environments/components/environment_stop');
describe('Stop Component', () => { describe('Stop Component', () => {
preloadFixtures('static/environments/element.html.raw'); preloadFixtures('static/environments/element.html.raw');
...@@ -10,7 +10,7 @@ describe('Stop Component', () => { ...@@ -10,7 +10,7 @@ describe('Stop Component', () => {
loadFixtures('static/environments/element.html.raw'); loadFixtures('static/environments/element.html.raw');
stopURL = '/stop'; stopURL = '/stop';
component = new window.gl.environmentsList.StopComponent({ component = new StopComponent({
el: document.querySelector('.test-dom-element'), el: document.querySelector('.test-dom-element'),
propsData: { propsData: {
stopUrl: stopURL, stopUrl: stopURL,
......
const EnvironmentTable = require('~/environments/components/environments_table');
describe('Environment item', () => {
preloadFixtures('static/environments/element.html.raw');
beforeEach(() => {
loadFixtures('static/environments/element.html.raw');
});
it('Should render a table', () => {
const mockItem = {
name: 'review',
size: 3,
isFolder: true,
latest: {
environment_path: 'url',
},
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [{ mockItem }],
canCreateDeployment: false,
canReadEnvironment: true,
},
});
expect(component.$el.tagName).toEqual('TABLE');
});
});
/* global environmentsList */ const Store = require('~/environments/stores/environments_store');
const { environmentsList, serverData } = require('./mock_data');
require('~/environments/stores/environments_store');
require('./mock_data');
(() => { (() => {
describe('Store', () => { describe('Store', () => {
beforeEach(() => { let store;
gl.environmentsList.EnvironmentsStore.create();
});
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(() => { beforeEach(() => {
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); store = new Store();
});
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', () => { it('should start with a blank state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3); expect(store.state.environments.length).toEqual(0);
expect(store.state.stoppedCounter).toEqual(0);
expect(store.state.availableCounter).toEqual(0);
expect(store.state.paginationInformation).toEqual({});
}); });
it('should store environments with same environment_type as sibilings', () => { it('should store environments', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3); store.storeEnvironments(serverData);
expect(store.state.environments.length).toEqual(serverData.length);
const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments expect(store.state.environments[0]).toEqual(environmentsList[0]);
.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', () => { it('should store available count', () => {
const { environments } = gl.environmentsList.EnvironmentsStore.state; store.storeAvailableCount(2);
expect(store.state.availableCounter).toEqual(2);
expect(environments[0].name).toBe('production');
expect(environments[1].name).toBe('review');
expect(environments[1].children[0].name).toBe('test-environment');
expect(environments[1].children[1].name).toBe('test-environment-1');
expect(environments[2].name).toBe('review_app');
});
}); });
describe('toggleFolder', () => { it('should store stopped count', () => {
beforeEach(() => { store.storeStoppedCount(2);
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); expect(store.state.stoppedCounter).toEqual(2);
}); });
it('should toggle the open property for the given environment', () => { it('should store pagination information', () => {
gl.environmentsList.EnvironmentsStore.toggleFolder('review'); const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '2',
'X-TOTAL': '37',
'X-Total-Pages': '2',
};
const { environments } = gl.environmentsList.EnvironmentsStore.state; const expectedResult = {
const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); perPage: 1,
page: 1,
total: 37,
totalPages: 2,
nextPage: 2,
previousPage: 2,
};
expect(environment[0].isOpen).toBe(true); store.setPagination(pagination);
}); expect(store.state.paginationInformation).toEqual(expectedResult);
}); });
}); });
})(); })();
const Vue = require('vue');
require('~/flash');
const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
const { environmentsList } = require('../mock_data');
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
window.history.pushState({}, null, 'environments/folders/build');
});
let component;
describe('successfull request', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
environments: environmentsList,
stopped_count: 1,
available_count: 0,
}), {
status: 200,
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsResponseInterceptor,
);
});
it('should render a table with environments', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('table tbody tr').length,
).toEqual(2);
done();
}, 0);
});
it('should render available tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('1');
done();
}, 0);
});
it('should render parent folder name', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-folder-name').textContent,
).toContain('Environments / build');
done();
}, 0);
});
describe('pagination', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
done();
}, 0);
});
it('should update url when no search params are present', (done) => {
spyOn(gl.utils, 'visitUrl');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page is already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page and scope are already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1&scope=all');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done();
}, 0);
});
});
});
describe('unsuccessfull request', () => {
const environmentsErrorResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsErrorResponseInterceptor,
);
});
it('should not render a table', (done) => {
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
setTimeout(() => {
expect(
component.$el.querySelector('table'),
).toBe(null);
done();
}, 0);
});
it('should render available tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
});
});
const environmentsList = [ const environmentsList = [
{ {
id: 31, name: 'DEV',
name: 'production', size: 1,
id: 7,
state: 'available', state: 'available',
external_url: 'https://www.gitlab.com', external_url: null,
environment_type: null, environment_type: null,
last_deployment: { last_deployment: null,
id: 64, 'stop_action?': false,
iid: 5, environment_path: '/root/review-app/environments/7',
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', stop_path: '/root/review-app/environments/7/stop',
ref: { created_at: '2017-01-31T10:53:46.894Z',
name: 'master', updated_at: '2017-01-31T10:53:46.894Z',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
}, },
{ {
id: 32, folderName: 'build',
name: 'review_app', size: 5,
state: 'stopped', id: 12,
external_url: 'https://www.gitlab.com', name: 'build/update-README',
environment_type: null, state: 'available',
last_deployment: { external_url: null,
id: 64, environment_type: 'build',
iid: 5, last_deployment: null,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': false, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/12',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/12/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
}, },
];
const serverData = [
{ {
id: 33, name: 'DEV',
name: 'test-environment', size: 1,
latest: {
id: 7,
name: 'DEV',
state: 'available', state: 'available',
environment_type: 'review', external_url: null,
environment_type: null,
last_deployment: null, last_deployment: null,
'stop_action?': true, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/7',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/7/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
}, },
{ {
id: 34, name: 'build',
name: 'test-environment-1', size: 5,
latest: {
id: 12,
name: 'build/update-README',
state: 'available', state: 'available',
environment_type: 'review', external_url: null,
environment_type: 'build',
last_deployment: null, last_deployment: null,
'stop_action?': true, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/12',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/12/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
},
}, },
]; ];
window.environmentsList = environmentsList;
const environment = { const environment = {
id: 4, name: 'DEV',
name: 'production', size: 1,
latest: {
id: 7,
name: 'DEV',
state: 'available', state: 'available',
external_url: 'http://production.', external_url: null,
environment_type: null, environment_type: null,
last_deployment: {}, last_deployment: null,
'stop_action?': false, 'stop_action?': false,
environment_path: '/root/review-app/environments/4', environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/4/stop', stop_path: '/root/review-app/environments/7/stop',
created_at: '2016-12-16T11:51:04.690Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2016-12-16T12:04:51.133Z', updated_at: '2017-01-31T10:53:46.894Z',
},
}; };
window.environment = environment; module.exports = {
environmentsList,
environment,
serverData,
};
%div
#environments-folder-list-view{ data: { "can-create-deployment" => "true",
"can-read-environment" => "true",
"css-class" => "",
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play") } }
...@@ -108,6 +108,30 @@ require('~/lib/utils/common_utils'); ...@@ -108,6 +108,30 @@ require('~/lib/utils/common_utils');
}); });
}); });
describe('gl.utils.parseIntPagination', () => {
it('should parse to integers all string values and return pagination object', () => {
const pagination = {
'X-PER-PAGE': 10,
'X-PAGE': 2,
'X-TOTAL': 30,
'X-TOTAL-PAGES': 3,
'X-NEXT-PAGE': 3,
'X-PREV-PAGE': 1,
};
const expectedPagination = {
perPage: 10,
page: 2,
total: 30,
totalPages: 3,
nextPage: 3,
previousPage: 1,
};
expect(gl.utils.parseIntPagination(pagination)).toEqual(expectedPagination);
});
});
describe('gl.utils.isMetaClick', () => { describe('gl.utils.isMetaClick', () => {
it('should identify meta click on Windows/Linux', () => { it('should identify meta click on Windows/Linux', () => {
const e = { const e = {
......
...@@ -34,7 +34,7 @@ describe('Pagination component', () => { ...@@ -34,7 +34,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '1' } }); component.changePage({ target: { innerText: '1' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the previous page', () => { it('should go to the previous page', () => {
...@@ -55,7 +55,7 @@ describe('Pagination component', () => { ...@@ -55,7 +55,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Prev' } }); component.changePage({ target: { innerText: 'Prev' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the next page', () => { it('should go to the next page', () => {
...@@ -76,7 +76,7 @@ describe('Pagination component', () => { ...@@ -76,7 +76,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Next' } }); component.changePage({ target: { innerText: 'Next' } });
expect(changeChanges.one).toEqual(5); expect(changeChanges.one).toEqual(5);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the last page', () => { it('should go to the last page', () => {
...@@ -97,7 +97,7 @@ describe('Pagination component', () => { ...@@ -97,7 +97,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: 'Last >>' } }); component.changePage({ target: { innerText: 'Last >>' } });
expect(changeChanges.one).toEqual(10); expect(changeChanges.one).toEqual(10);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should go to the first page', () => { it('should go to the first page', () => {
...@@ -118,7 +118,7 @@ describe('Pagination component', () => { ...@@ -118,7 +118,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '<< First' } }); component.changePage({ target: { innerText: '<< First' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
it('should do nothing', () => { it('should do nothing', () => {
...@@ -139,7 +139,7 @@ describe('Pagination component', () => { ...@@ -139,7 +139,7 @@ describe('Pagination component', () => {
component.changePage({ target: { innerText: '...' } }); component.changePage({ target: { innerText: '...' } });
expect(changeChanges.one).toEqual(1); expect(changeChanges.one).toEqual(1);
expect(changeChanges.two).toEqual('all'); expect(changeChanges.two).toEqual(null);
}); });
}); });
......
...@@ -181,6 +181,17 @@ describe EnvironmentSerializer do ...@@ -181,6 +181,17 @@ describe EnvironmentSerializer do
expect(subject.first[:name]).to eq 'production' expect(subject.first[:name]).to eq 'production'
expect(subject.second[:name]).to eq 'staging' expect(subject.second[:name]).to eq 'staging'
end end
it 'appends correct total page count header' do
expect(subject).not_to be_empty
expect(response).to have_received(:[]=).with('X-Total', '3')
end
it 'appends correct page count headers' do
expect(subject).not_to be_empty
expect(response).to have_received(:[]=).with('X-Total-Pages', '2')
expect(response).to have_received(:[]=).with('X-Per-Page', '2')
end
end end
end end
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment