Commit ea7c7769 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'fe-paginated-environments-api' into 'paginate-environments-bundle'

Integrate with new environments API

See merge request !8954
parents edecab20 13615ef8
/* global Vue */ const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('actions-component', {
props: {
(() => { actions: {
window.gl = window.gl || {}; type: Array,
window.gl.environmentsList = window.gl.environmentsList || {}; required: false,
default: () => [],
gl.environmentsList.ActionsComponent = Vue.component('actions-component', {
props: {
actions: {
type: Array,
required: false,
default: () => [],
},
playIconSvg: {
type: String,
required: false,
},
}, },
template: ` playIconSvg: {
<div class="inline"> type: String,
<div class="dropdown"> required: false,
<a class="dropdown-new btn btn-default" data-toggle="dropdown"> },
<span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span> },
<i class="fa fa-caret-down"></i>
</a> template: `
<div class="inline">
<ul class="dropdown-menu dropdown-menu-align-right"> <div class="dropdown">
<li v-for="action in actions"> <a class="dropdown-new btn btn-default" data-toggle="dropdown">
<a :href="action.play_path" <span class="js-dropdown-play-icon-container" v-html="playIconSvg"></span>
data-method="post" <i class="fa fa-caret-down"></i>
rel="nofollow" </a>
class="js-manual-action-link">
<ul class="dropdown-menu dropdown-menu-align-right">
<span class="js-action-play-icon-container" v-html="playIconSvg"></span> <li v-for="action in actions">
<a :href="action.play_path"
<span> data-method="post"
{{action.name}} rel="nofollow"
</span> class="js-manual-action-link">
</a>
</li> <span class="js-action-play-icon-container" v-html="playIconSvg"></span>
</ul>
</div> <span>
{{action.name}}
</span>
</a>
</li>
</ul>
</div> </div>
`, </div>
}); `,
})(); });
/* global Vue */ /**
* Renders the external url link in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('external-url-component', {
props: {
(() => { externalUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
gl.environmentsList.ExternalUrlComponent = Vue.component('external-url-component', {
props: {
externalUrl: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn external_url" :href="externalUrl" target="_blank"> <a class="btn external_url" :href="externalUrl" target="_blank">
<i class="fa fa-external-link"></i> <i class="fa fa-external-link"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders Rollback or Re deploy button in environments table depending
* of the provided property `isLastDeployment`
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('rollback-component', {
props: {
(() => { retryUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
},
gl.environmentsList.RollbackComponent = Vue.component('rollback-component', {
props: {
retryUrl: {
type: String,
default: '',
},
isLastDeployment: { isLastDeployment: {
type: Boolean, type: Boolean,
default: true, default: true,
},
}, },
},
template: ` template: `
<a class="btn" :href="retryUrl" data-method="post" rel="nofollow"> <a class="btn" :href="retryUrl" data-method="post" rel="nofollow">
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
Re-deploy Re-deploy
</span> </span>
<span v-else> <span v-else>
Rollback Rollback
</span> </span>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders the stop "button" that allows stop an environment.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('stop-component', {
props: {
(() => { stopUrl: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
gl.environmentsList.StopComponent = Vue.component('stop-component', {
props: {
stopUrl: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn stop-env-link" <a class="btn stop-env-link"
:href="stopUrl" :href="stopUrl"
data-confirm="Are you sure you want to stop this environment?" data-confirm="Are you sure you want to stop this environment?"
data-method="post" data-method="post"
rel="nofollow"> rel="nofollow">
<i class="fa fa-stop stop-env-icon"></i> <i class="fa fa-stop stop-env-icon" aria-hidden="true"></i>
</a> </a>
`, `,
}); });
})();
/* global Vue */ /**
* Renders a terminal button to open a web terminal.
* Used in environments table.
*/
const Vue = require('vue');
window.Vue = require('vue'); module.exports = Vue.component('terminal-button-component', {
props: {
(() => { terminalPath: {
window.gl = window.gl || {}; type: String,
window.gl.environmentsList = window.gl.environmentsList || {}; default: '',
},
gl.environmentsList.TerminalButtonComponent = Vue.component('terminal-button-component', { terminalIconSvg: {
props: { type: String,
terminalPath: { default: '',
type: String,
default: '',
},
terminalIconSvg: {
type: String,
default: '',
},
}, },
},
template: ` template: `
<a class="btn terminal-button" <a class="btn terminal-button"
:href="terminalPath"> :href="terminalPath">
<span class="js-terminal-icon-container" v-html="terminalIconSvg"></span> <span class="js-terminal-icon-container" v-html="terminalIconSvg"></span>
</a> </a>
`, `,
}); });
})();
/**
* Render environments table.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
},
props: {
environments: {
type: Array,
required: true,
default: () => ([]),
},
canReadEnvironment: {
type: Boolean,
required: false,
default: false,
},
canCreateDeployment: {
type: Boolean,
required: false,
default: false,
},
commitIconSvg: {
type: String,
required: false,
},
playIconSvg: {
type: String,
required: false,
},
terminalIconSvg: {
type: String,
required: false,
},
},
template: `
<table class="table ci-table environments">
<thead>
<tr>
<th class="environments-name">Environment</th>
<th class="environments-deploy">Last deployment</th>
<th class="environments-build">Job</th>
<th class="environments-commit">Commit</th>
<th class="environments-date">Updated</th>
<th class="hidden-xs environments-actions"></th>
</tr>
</thead>
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
</template>
</tbody>
</table>
`,
});
window.Vue = require('vue'); const EnvironmentsComponent = require('./components/environment');
require('./stores/environments_store');
require('./components/environment');
require('../vue_shared/vue_resource_interceptor'); require('../vue_shared/vue_resource_interceptor');
$(() => { $(() => {
...@@ -9,14 +7,8 @@ $(() => { ...@@ -9,14 +7,8 @@ $(() => {
if (gl.EnvironmentsListApp) { if (gl.EnvironmentsListApp) {
gl.EnvironmentsListApp.$destroy(true); gl.EnvironmentsListApp.$destroy(true);
} }
const Store = gl.environmentsList.EnvironmentsStore;
gl.EnvironmentsListApp = new gl.environmentsList.EnvironmentsComponent({ gl.EnvironmentsListApp = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'), el: document.querySelector('#environments-list-view'),
propsData: {
store: Store.create(),
},
}); });
}); });
const EnvironmentsFolderComponent = require('./environments_folder_view');
require('../../vue_shared/vue_resource_interceptor');
$(() => {
window.gl = window.gl || {};
if (gl.EnvironmentsListFolderApp) {
gl.EnvironmentsListFolderApp.$destroy(true);
}
gl.EnvironmentsListFolderApp = new EnvironmentsFolderComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
const Vue = require('vue');
Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
module.exports = Vue.component('environment-folder-view', {
components: {
'environment-table': EnvironmentTable,
'table-pagination': gl.VueGlPagination,
},
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
const pathname = window.location.pathname;
const endpoint = `${pathname}.json`;
const folderName = pathname.substr(pathname.lastIndexOf('/') + 1);
return {
store,
folderName,
endpoint,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
// svgs
commitIconSvg: environmentsData.commitIconSvg,
playIconSvg: environmentsData.playIconSvg,
terminalIconSvg: environmentsData.terminalIconSvg,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return gl.utils.getParameterByName('scope');
},
canReadEnvironmentParsed() {
return gl.utils.convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return gl.utils.convertPermissionToBoolean(this.canCreateDeployment);
},
/**
* URL to link in the stopped tab.
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
},
/**
* URL to link in the available tab.
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
}))
.then((response) => {
this.store.storeAvailableCount(response.body.available_count);
this.store.storeStoppedCount(response.body.stopped_count);
this.store.storeEnvironments(response.body.environments);
this.store.setPagination(response.headers);
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the environments.', 'alert');
});
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
},
template: `
<div :class="cssContainerClass">
<div class="top-area" v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b>
</h4>
<ul class="nav-links">
<li v-bind:class="{ 'active': scope === null || scope === 'available' }">
<a :href="availablePath" class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li v-bind:class="{ 'active' : scope === 'stopped' }">
<a :href="stoppedPath" class="js-stopped-environments-folder-tab">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
</div>
<div class="environments-container">
<div class="environments-list-loading text-center" v-if="isLoading">
<i class="fa fa-spinner fa-spin"></i>
</div>
<div class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
</div>
</div>
</div>
`,
});
/* globals Vue */ const Vue = require('vue');
/* eslint-disable no-unused-vars, no-param-reassign */
class EnvironmentsService { class EnvironmentsService {
constructor(endpoint) {
constructor(root) { this.environments = Vue.resource(endpoint);
Vue.http.options.root = root;
this.environments = Vue.resource(root);
Vue.http.interceptors.push((request, next) => {
// needed in order to not break the tests.
if ($.rails) {
request.headers['X-CSRF-Token'] = $.rails.csrfToken();
}
next();
});
} }
all() { all() {
...@@ -22,4 +10,4 @@ class EnvironmentsService { ...@@ -22,4 +10,4 @@ class EnvironmentsService {
} }
} }
window.EnvironmentsService = EnvironmentsService; module.exports = EnvironmentsService;
/* eslint-disable no-param-reassign */ require('~/lib/utils/common_utils');
(() => { /**
window.gl = window.gl || {}; * Environments Store.
window.gl.environmentsList = window.gl.environmentsList || {}; *
* Stores received environments, count of stopped environments and count of
gl.environmentsList.EnvironmentsStore = { * available environments.
state: {}, */
class EnvironmentsStore {
create() { constructor() {
this.state.environments = []; this.state = {};
this.state.stoppedCounter = 0; this.state.environments = [];
this.state.availableCounter = 0; this.state.stoppedCounter = 0;
this.state.visibility = 'available'; this.state.availableCounter = 0;
this.state.filteredEnvironments = []; this.state.paginationInformation = {};
return this; return this;
}, }
/** /**
* In order to display a tree view we need to modify the received *
* data in to a tree structure based on `environment_type` * Stores the received environments.
* sorted alphabetically. *
* In each children a `vue-` property will be added. This property will be * In the main environments endpoint, each environment has the following schema
* used to know if an item is a children mostly for css purposes. This is * { name: String, size: Number, latest: Object }
* needed because the children row is a fragment instance and therfore does * In the endpoint to retrieve environments from each folder, the environment does
* not accept non-prop attributes. * not have the `latest` key and the data is all in the root level.
* * To avoid doing this check in the view, we store both cases the same by extracting
* * what is inside the `latest` key.
* @example *
* it will transform this: * If the `size` is bigger than 1, it means it should be rendered as a folder.
* [ * In those cases we add `isFolder` key in order to render it properly.
* { name: "environment", environment_type: "review" }, *
* { name: "environment_1", environment_type: null } * @param {Array} environments
* { name: "environment_2, environment_type: "review" } * @returns {Array}
* ] */
* into this: storeEnvironments(environments = []) {
* [ const filteredEnvironments = environments.map((env) => {
* { name: "review", children: let filtered = {};
* [
* { name: "environment", environment_type: "review", vue-isChildren: true}, if (env.size > 1) {
* { name: "environment_2", environment_type: "review", vue-isChildren: true} filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
* ] }
* },
* {name: "environment_1", environment_type: null} if (env.latest) {
* ] filtered = Object.assign(filtered, env, env.latest);
* delete filtered.latest;
* } else {
* @param {Array} environments List of environments. filtered = Object.assign(filtered, env);
* @returns {Array} Tree structured array with the received environments. }
*/
storeEnvironments(environments = []) { return filtered;
this.state.stoppedCounter = this.countByState(environments, 'stopped'); });
this.state.availableCounter = this.countByState(environments, 'available');
this.state.environments = filteredEnvironments;
const environmentsTree = environments.reduce((acc, environment) => {
if (environment.environment_type !== null) { return filteredEnvironments;
const occurs = acc.filter(element => element.children && }
element.name === environment.environment_type);
setPagination(pagination = {}) {
environment['vue-isChildren'] = true; const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
if (occurs.length) {
acc[acc.indexOf(occurs[0])].children.push(environment); this.state.paginationInformation = paginationInformation;
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName); return paginationInformation;
} else { }
acc.push({
name: environment.environment_type, /**
children: [environment], * Stores the number of available environments.
isOpen: false, *
'vue-isChildren': environment['vue-isChildren'], * @param {Number} count = 0
}); * @return {Number}
} */
} else { storeAvailableCount(count = 0) {
acc.push(environment); this.state.availableCounter = count;
} return count;
}
return acc;
}, []).slice().sort(this.sortByName); /**
* Stores the number of closed environments.
this.state.environments = environmentsTree; *
* @param {Number} count = 0
this.filterEnvironmentsByVisibility(this.state.environments); * @return {Number}
*/
return environmentsTree; storeStoppedCount(count = 0) {
}, this.state.stoppedCounter = count;
return count;
storeVisibility(visibility) { }
this.state.visibility = visibility; }
},
/** module.exports = EnvironmentsStore;
* Given the visibility prop provided by the url query parameter and which
* changes according to the active tab we need to filter which environments
* should be visible.
*
* The environments array is a recursive tree structure and we need to filter
* both root level environments and children environments.
*
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
* functions work together.
* The first one works as the filter that verifies if the given environment matches
* the given state.
* The second guarantees both root level and children elements are filtered as well.
*
* Given array of environments will return only
* the environments that match the state stored.
*
* @param {Array} array
* @return {Array}
*/
filterEnvironmentsByVisibility(arr) {
const filteredEnvironments = arr.map((item) => {
if (item.children) {
const filteredChildren = this.filterEnvironmentsByVisibility(
item.children,
).filter(Boolean);
if (filteredChildren.length) {
item.children = filteredChildren;
return item;
}
}
return this.filterState(this.state.visibility, item);
}).filter(Boolean);
this.state.filteredEnvironments = filteredEnvironments;
return filteredEnvironments;
},
/**
* Given the state and the environment,
* returns only if the environment state matches the one provided.
*
* @param {String} state
* @param {Object} environment
* @return {Object}
*/
filterState(state, environment) {
return environment.state === state && environment;
},
/**
* Toggles folder open property given the environment type.
*
* @param {String} envType
* @return {Array}
*/
toggleFolder(envType) {
const environments = this.state.environments;
const environmentsCopy = environments.map((env) => {
if (env['vue-isChildren'] && env.name === envType) {
env.isOpen = !env.isOpen;
}
return env;
});
this.state.environments = environmentsCopy;
return environmentsCopy;
},
/**
* Given an array of environments, returns the number of environments
* that have the given state.
*
* @param {Array} environments
* @param {String} state
* @returns {Number}
*/
countByState(environments, state) {
return environments.filter(env => env.state === state).length;
},
/**
* Sorts the two objects provided by their name.
*
* @param {Object} a
* @param {Object} b
* @returns {Number}
*/
sortByName(a, b) {
const nameA = a.name.toUpperCase();
const nameB = b.name.toUpperCase();
return nameA < nameB ? -1 : nameA > nameB ? 1 : 0; // eslint-disable-line
},
};
})();
...@@ -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;
} }
} }
...@@ -135,4 +143,4 @@ ...@@ -135,4 +143,4 @@
margin-right: 0; margin-right: 0;
} }
} }
} }
\ No newline at end of file
...@@ -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(:user) { create(:user) }
let(:project) { environment.project } let(:project) { create(:empty_project) }
let(:user) { create(:user) }
let(:environment) do
create(:environment, name: 'production', project: project)
end
before do before do
project.team << [user, :master] project.team << [user, :master]
...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do ...@@ -22,14 +25,58 @@ describe Projects::EnvironmentsController do
end end
end end
context 'when requesting JSON response' do context 'when requesting JSON response for folders' do
it 'responds with correct JSON' do before do
get :index, environment_params(format: :json) create(:environment, project: project,
name: 'staging/review-1',
state: :available)
create(:environment, project: project,
name: 'staging/review-2',
state: :available)
create(:environment, project: project,
name: 'staging/review-3',
state: :stopped)
end
let(:environments) { json_response['environments'] }
context 'when requesting available environments scope' do
before do
get :index, environment_params(format: :json, scope: :available)
end
it 'responds with a payload describing available environments' do
expect(environments.count).to eq 2
expect(environments.first['name']).to eq 'production'
expect(environments.second['name']).to eq 'staging'
expect(environments.second['size']).to eq 2
expect(environments.second['latest']['name']).to eq 'staging/review-2'
end
first_environment = json_response.first it 'contains values describing environment scopes sizes' do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end
expect(first_environment).not_to be_empty context 'when requesting stopped environments scope' do
expect(first_environment['name']). to eq environment.name before do
get :index, environment_params(format: :json, scope: :stopped)
end
it 'responds with a payload describing stopped environments' do
expect(environments.count).to eq 1
expect(environments.first['name']).to eq 'staging'
expect(environments.first['size']).to eq 1
expect(environments.first['latest']['name']).to eq 'staging/review-3'
end
it 'contains values describing environment scopes sizes' do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
end end
end end
end end
......
...@@ -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', () => {
let store;
beforeEach(() => { beforeEach(() => {
gl.environmentsList.EnvironmentsStore.create(); store = new Store();
}); });
it('should start with a blank state', () => { it('should start with a blank state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(0); expect(store.state.environments.length).toEqual(0);
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(0); expect(store.state.stoppedCounter).toEqual(0);
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(0); expect(store.state.availableCounter).toEqual(0);
expect(store.state.paginationInformation).toEqual({});
}); });
describe('store environments', () => { it('should store environments', () => {
beforeEach(() => { store.storeEnvironments(serverData);
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); expect(store.state.environments.length).toEqual(serverData.length);
}); expect(store.state.environments[0]).toEqual(environmentsList[0]);
it('should count stopped environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.stoppedCounter).toBe(1);
});
it('should count available environments and save the count in the state', () => {
expect(gl.environmentsList.EnvironmentsStore.state.availableCounter).toBe(3);
});
it('should store environments with same environment_type as sibilings', () => {
expect(gl.environmentsList.EnvironmentsStore.state.environments.length).toBe(3);
const parentFolder = gl.environmentsList.EnvironmentsStore.state.environments
.filter(env => env.children && env.children.length > 0);
expect(parentFolder[0].children.length).toBe(2);
expect(parentFolder[0].children[0].environment_type).toBe('review');
expect(parentFolder[0].children[1].environment_type).toBe('review');
expect(parentFolder[0].children[0].name).toBe('test-environment');
expect(parentFolder[0].children[1].name).toBe('test-environment-1');
});
it('should sort the environments alphabetically', () => {
const { environments } = gl.environmentsList.EnvironmentsStore.state;
expect(environments[0].name).toBe('production');
expect(environments[1].name).toBe('review');
expect(environments[1].children[0].name).toBe('test-environment');
expect(environments[1].children[1].name).toBe('test-environment-1');
expect(environments[2].name).toBe('review_app');
});
}); });
describe('toggleFolder', () => { it('should store available count', () => {
beforeEach(() => { store.storeAvailableCount(2);
gl.environmentsList.EnvironmentsStore.storeEnvironments(environmentsList); expect(store.state.availableCounter).toEqual(2);
}); });
it('should toggle the open property for the given environment', () => {
gl.environmentsList.EnvironmentsStore.toggleFolder('review');
const { environments } = gl.environmentsList.EnvironmentsStore.state; it('should store stopped count', () => {
const environment = environments.filter(env => env['vue-isChildren'] === true && env.name === 'review'); store.storeStoppedCount(2);
expect(store.state.stoppedCounter).toEqual(2);
});
expect(environment[0].isOpen).toBe(true); it('should store pagination information', () => {
}); const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '2',
'X-TOTAL': '37',
'X-Total-Pages': '2',
};
const expectedResult = {
perPage: 1,
page: 1,
total: 37,
totalPages: 2,
nextPage: 2,
previousPage: 2,
};
store.setPagination(pagination);
expect(store.state.paginationInformation).toEqual(expectedResult);
}); });
}); });
})(); })();
const Vue = require('vue');
require('~/flash');
const EnvironmentsFolderViewComponent = require('~/environments/folder/environments_folder_view');
const { environmentsList } = require('../mock_data');
describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw');
beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw');
window.history.pushState({}, null, 'environments/folders/build');
});
let component;
describe('successfull request', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
environments: environmentsList,
stopped_count: 1,
available_count: 0,
}), {
status: 200,
headers: {
'X-nExt-pAge': '2',
'x-page': '1',
'X-Per-Page': '1',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '2',
},
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsResponseInterceptor,
);
});
it('should render a table with environments', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('table tbody tr').length,
).toEqual(2);
done();
}, 0);
});
it('should render available tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('1');
done();
}, 0);
});
it('should render parent folder name', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-folder-name').textContent,
).toContain('Environments / build');
done();
}, 0);
});
describe('pagination', () => {
it('should render pagination', (done) => {
setTimeout(() => {
expect(
component.$el.querySelectorAll('.gl-pagination li').length,
).toEqual(5);
done();
}, 0);
});
it('should update url when no search params are present', (done) => {
spyOn(gl.utils, 'visitUrl');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page is already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done();
}, 0);
});
it('should update url when page and scope are already present', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2');
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?page=1&scope=all');
setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done();
}, 0);
});
});
});
describe('unsuccessfull request', () => {
const environmentsErrorResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 500,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsErrorResponseInterceptor,
);
});
it('should not render a table', (done) => {
component = new EnvironmentsFolderViewComponent({
el: document.querySelector('#environments-folder-list-view'),
});
setTimeout(() => {
expect(
component.$el.querySelector('table'),
).toBe(null);
done();
}, 0);
});
it('should render available tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent,
).toContain('Available');
expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
it('should render stopped tab with count 0', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent,
).toContain('Stopped');
expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent,
).toContain('0');
done();
}, 0);
});
});
});
const environmentsList = [ const environmentsList = [
{ {
id: 31, name: 'DEV',
name: 'production', size: 1,
id: 7,
state: 'available', state: 'available',
external_url: 'https://www.gitlab.com', external_url: null,
environment_type: null,
last_deployment: {
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': true,
environment_path: '/root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-07T11:11:16.525Z',
},
{
id: 32,
name: 'review_app',
state: 'stopped',
external_url: 'https://www.gitlab.com',
environment_type: null, environment_type: null,
last_deployment: { last_deployment: null,
id: 64,
iid: 5,
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
ref: {
name: 'master',
ref_url: 'http://localhost:3000/root/ci-folders/tree/master',
},
tag: false,
'last?': true,
user: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit: {
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1278,
name: 'build',
build_path: '/root/ci-folders/builds/1278',
retry_path: '/root/ci-folders/builds/1278/retry',
},
manual_actions: [],
},
'stop_action?': false, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/7',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/7/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
}, },
{ {
id: 33, folderName: 'build',
name: 'test-environment', size: 5,
id: 12,
name: 'build/update-README',
state: 'available', state: 'available',
environment_type: 'review', external_url: null,
environment_type: 'build',
last_deployment: null, last_deployment: null,
'stop_action?': true, 'stop_action?': false,
environment_path: '/root/ci-folders/environments/31', environment_path: '/root/review-app/environments/12',
created_at: '2016-11-07T11:11:16.525Z', stop_path: '/root/review-app/environments/12/stop',
updated_at: '2016-11-07T11:11:16.525Z', created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
}, },
];
const serverData = [
{ {
id: 34, name: 'DEV',
name: 'test-environment-1', size: 1,
state: 'available', latest: {
environment_type: 'review', id: 7,
last_deployment: null, name: 'DEV',
'stop_action?': true, state: 'available',
environment_path: '/root/ci-folders/environments/31', external_url: null,
created_at: '2016-11-07T11:11:16.525Z', environment_type: null,
updated_at: '2016-11-07T11:11:16.525Z', last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
},
{
name: 'build',
size: 5,
latest: {
id: 12,
name: 'build/update-README',
state: 'available',
external_url: null,
environment_type: 'build',
last_deployment: null,
'stop_action?': false,
environment_path: '/root/review-app/environments/12',
stop_path: '/root/review-app/environments/12/stop',
created_at: '2017-02-01T19:42:18.400Z',
updated_at: '2017-02-01T19:42:18.400Z',
},
}, },
]; ];
window.environmentsList = environmentsList;
const environment = { const environment = {
id: 4, name: 'DEV',
name: 'production', size: 1,
state: 'available', latest: {
external_url: 'http://production.', id: 7,
environment_type: null, name: 'DEV',
last_deployment: {}, state: 'available',
'stop_action?': false, external_url: null,
environment_path: '/root/review-app/environments/4', environment_type: null,
stop_path: '/root/review-app/environments/4/stop', last_deployment: null,
created_at: '2016-12-16T11:51:04.690Z', 'stop_action?': false,
updated_at: '2016-12-16T12:04:51.133Z', environment_path: '/root/review-app/environments/7',
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
},
}; };
window.environment = environment; module.exports = {
environmentsList,
environment,
serverData,
};
%div
#environments-folder-list-view{ data: { "can-create-deployment" => "true",
"can-read-environment" => "true",
"css-class" => "",
"commit-icon-svg" => custom_icon("icon_commit"),
"terminal-icon-svg" => custom_icon("icon_terminal"),
"play-icon-svg" => custom_icon("icon_play") } }
...@@ -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