Commit 13615ef8 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch 'fe-paginated-environments-api-add-pagination' into 'fe-paginated-environments-api'

Add pagination to environments table

See merge request !9090
parents a254dcf0 25c92938
...@@ -4,18 +4,21 @@ ...@@ -4,18 +4,21 @@
const Vue = require('vue'); const Vue = require('vue');
Vue.use(require('vue-resource')); Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service'); const EnvironmentsService = require('../services/environments_service');
const EnvironmentItem = require('./environment_item'); const EnvironmentTable = require('./environments_table');
const Store = require('../stores/environments_store'); const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
module.exports = Vue.component('environment-component', { module.exports = Vue.component('environment-component', {
components: { components: {
'environment-item': 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 Store(); const store = new EnvironmentsStore();
return { return {
store, store,
...@@ -34,25 +37,30 @@ module.exports = Vue.component('environment-component', { ...@@ -34,25 +37,30 @@ module.exports = Vue.component('environment-component', {
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);
}, },
}, },
/** /**
...@@ -60,19 +68,25 @@ module.exports = Vue.component('environment-component', { ...@@ -60,19 +68,25 @@ module.exports = Vue.component('environment-component', {
* Toggles loading property. * Toggles loading property.
*/ */
created() { created() {
const scope = this.$options.getQueryParameter('scope') || this.visibility; const scope = gl.utils.getParameterByName('scope') || this.visibility;
const endpoint = `${this.endpoint}?scope=${scope}`; const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint); const service = new EnvironmentsService(endpoint);
this.isLoading = true; this.isLoading = true;
return service.all() return service.all()
.then(resp => resp.json()) .then(resp => ({
.then((json) => { headers: resp.headers,
this.store.storeAvailableCount(json.available_count); body: resp.json(),
this.store.storeStoppedCount(json.stopped_count); }))
this.store.storeEnvironments(json.environments); .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(() => { .then(() => {
this.isLoading = false; this.isLoading = false;
...@@ -83,41 +97,30 @@ module.exports = Vue.component('environment-component', { ...@@ -83,41 +97,30 @@ module.exports = Vue.component('environment-component', {
}); });
}, },
/**
* Transforms the url parameter into an object and
* returns the one requested.
*
* @param {String} param
* @returns {String} The value of the requested parameter.
*/
getQueryParameter(parameter) {
return window.location.search.substring(1).split('&').reduce((acc, param) => {
const paramSplited = param.split('=');
acc[paramSplited[0]] = paramSplited[1];
return acc;
}, {})[parameter];
},
/**
* Converts permission provided as strings to booleans.
* @param {String} string
* @returns {Boolean}
*/
convertPermissionToBoolean(string) {
return string === 'true';
},
methods: { methods: {
toggleRow(model) { toggleRow(model) {
return this.store.toggleFolder(model.name); return this.store.toggleFolder(model.name);
}, },
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
* @return {String}
*/
changePage(pageNumber) {
const param = gl.utils.setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
}, },
template: ` template: `
<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">
...@@ -168,30 +171,20 @@ module.exports = Vue.component('environment-component', { ...@@ -168,30 +171,20 @@ module.exports = Vue.component('environment-component', {
<div class="table-holder" <div class="table-holder"
v-if="!isLoading && state.environments.length > 0"> v-if="!isLoading && state.environments.length > 0">
<table class="table ci-table environments">
<thead> <environment-table
<tr> :environments="state.environments"
<th class="environments-name">Environment</th> :can-create-deployment="canCreateDeploymentParsed"
<th class="environments-deploy">Last deployment</th> :can-read-environment="canReadEnvironmentParsed"
<th class="environments-build">Job</th> :play-icon-svg="playIconSvg"
<th class="environments-commit">Commit</th> :terminal-icon-svg="terminalIconSvg"
<th class="environments-date">Updated</th> :commit-icon-svg="commitIconSvg">
<th class="hidden-xs environments-actions"></th> </environment-table>
</tr>
</thead> <table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
<tbody> :change="changePage"
<template v-for="model in state.environments" :pageInfo="state.paginationInformation">
v-bind:model="model"> </table-pagination>
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
</template>
</tbody>
</table>
</div> </div>
</div> </div>
</div> </div>
......
/**
* 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>
`,
});
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>
`,
});
require('~/lib/utils/common_utils');
/** /**
* Environments Store. * Environments Store.
* *
...@@ -10,6 +11,7 @@ class EnvironmentsStore { ...@@ -10,6 +11,7 @@ class EnvironmentsStore {
this.state.environments = []; this.state.environments = [];
this.state.stoppedCounter = 0; this.state.stoppedCounter = 0;
this.state.availableCounter = 0; this.state.availableCounter = 0;
this.state.paginationInformation = {};
return this; return this;
} }
...@@ -18,8 +20,12 @@ class EnvironmentsStore { ...@@ -18,8 +20,12 @@ class EnvironmentsStore {
* *
* Stores the received environments. * Stores the received environments.
* *
* Each environment has the following schema * In the main environments endpoint, each environment has the following schema
* { name: String, size: Number, latest: Object } * { name: String, size: Number, latest: Object }
* In the endpoint to retrieve environments from each folder, the environment does
* not have the `latest` key and the data is all in the root level.
* To avoid doing this check in the view, we store both cases the same by extracting
* what is inside the `latest` key.
* *
* If the `size` is bigger than 1, it means it should be rendered as a folder. * 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. * In those cases we add `isFolder` key in order to render it properly.
...@@ -29,11 +35,20 @@ class EnvironmentsStore { ...@@ -29,11 +35,20 @@ class EnvironmentsStore {
*/ */
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
let filtered = {};
if (env.size > 1) { if (env.size > 1) {
return Object.assign({}, env, { isFolder: true }); filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
}
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
delete filtered.latest;
} else {
filtered = Object.assign(filtered, env);
} }
return env; return filtered;
}); });
this.state.environments = filteredEnvironments; this.state.environments = filteredEnvironments;
...@@ -41,6 +56,14 @@ class EnvironmentsStore { ...@@ -41,6 +56,14 @@ class EnvironmentsStore {
return filteredEnvironments; return filteredEnvironments;
} }
setPagination(pagination = {}) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
this.state.paginationInformation = paginationInformation;
return paginationInformation;
}
/** /**
* Stores the number of available environments. * Stores the number of available environments.
* *
......
...@@ -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;
}; };
......
...@@ -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%;
...@@ -113,6 +118,7 @@ ...@@ -113,6 +118,7 @@
.folder-icon { .folder-icon {
margin-right: 3px; margin-right: 3px;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: inline-block;
.fa:nth-child(1) { .fa:nth-child(1) {
margin-right: 3px; margin-right: 3px;
...@@ -122,6 +128,7 @@ ...@@ -122,6 +128,7 @@
.folder-name { .folder-name {
cursor: pointer; cursor: pointer;
color: $gl-text-color-secondary; color: $gl-text-color-secondary;
display: inline-block;
} }
} }
......
- @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:
...@@ -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',
......
...@@ -14,11 +14,10 @@ describe('Environment item', () => { ...@@ -14,11 +14,10 @@ describe('Environment item', () => {
beforeEach(() => { beforeEach(() => {
mockItem = { mockItem = {
name: 'review', name: 'review',
folderName: 'review',
size: 3, size: 3,
isFolder: true, isFolder: true,
latest: { environment_path: 'url',
environment_path: 'url',
},
}; };
component = new EnvironmentItem({ component = new EnvironmentItem({
...@@ -49,21 +48,36 @@ describe('Environment item', () => { ...@@ -49,21 +48,36 @@ describe('Environment item', () => {
environment = { environment = {
name: 'production', name: 'production',
size: 1, size: 1,
latest: { state: 'stopped',
state: 'stopped', external_url: 'http://external.com',
external_url: 'http://external.com', environment_type: null,
environment_type: null, last_deployment: {
last_deployment: { id: 66,
id: 66, iid: 6,
iid: 6, sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', ref: {
ref: { name: 'master',
name: 'master', ref_path: 'root/ci-folders/tree/master',
ref_path: 'root/ci-folders/tree/master', },
}, tag: true,
tag: true, 'last?': true,
'last?': true, user: {
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', name: 'Administrator',
username: 'root', username: 'root',
id: 1, id: 1,
...@@ -71,44 +85,27 @@ describe('Environment item', () => { ...@@ -71,44 +85,27 @@ describe('Environment item', () => {
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root', web_url: 'http://localhost:3000/root',
}, },
commit: { commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
short_id: '500aabcb',
title: 'Update .gitlab-ci.yml',
author_name: 'Administrator',
author_email: 'admin@example.com',
created_at: '2016-11-07T18:28:13.000+00:00',
message: 'Update .gitlab-ci.yml',
author: {
name: 'Administrator',
username: 'root',
id: 1,
state: 'active',
avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://localhost:3000/root',
},
commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd',
},
deployable: {
id: 1279,
name: 'deploy',
build_path: '/root/ci-folders/builds/1279',
retry_path: '/root/ci-folders/builds/1279/retry',
created_at: '2016-11-29T18:11:58.430Z',
updated_at: '2016-11-29T18:11:58.430Z',
},
manual_actions: [
{
name: 'action',
play_path: '/play',
},
],
}, },
'stop_action?': true, deployable: {
environment_path: 'root/ci-folders/environments/31', id: 1279,
created_at: '2016-11-07T11:11:16.525Z', name: 'deploy',
updated_at: '2016-11-10T15:55:58.778Z', build_path: '/root/ci-folders/builds/1279',
retry_path: '/root/ci-folders/builds/1279/retry',
created_at: '2016-11-29T18:11:58.430Z',
updated_at: '2016-11-29T18:11:58.430Z',
},
manual_actions: [
{
name: 'action',
play_path: '/play',
},
],
}, },
'stop_action?': true,
environment_path: 'root/ci-folders/environments/31',
created_at: '2016-11-07T11:11:16.525Z',
updated_at: '2016-11-10T15:55:58.778Z',
}; };
component = new EnvironmentItem({ component = new EnvironmentItem({
...@@ -129,7 +126,7 @@ describe('Environment item', () => { ...@@ -129,7 +126,7 @@ describe('Environment item', () => {
it('should render deployment internal id', () => { it('should render deployment internal id', () => {
expect( expect(
component.$el.querySelector('.deployment-column span').textContent, component.$el.querySelector('.deployment-column span').textContent,
).toContain(environment.latest.last_deployment.iid); ).toContain(environment.last_deployment.iid);
expect( expect(
component.$el.querySelector('.deployment-column span').textContent, component.$el.querySelector('.deployment-column span').textContent,
...@@ -139,7 +136,7 @@ describe('Environment item', () => { ...@@ -139,7 +136,7 @@ describe('Environment item', () => {
it('should render last deployment date', () => { it('should render last deployment date', () => {
const timeagoInstance = new timeago(); // eslint-disable-line const timeagoInstance = new timeago(); // eslint-disable-line
const formatedDate = timeagoInstance.format( const formatedDate = timeagoInstance.format(
environment.latest.last_deployment.deployable.created_at, environment.last_deployment.deployable.created_at,
); );
expect( expect(
...@@ -151,7 +148,7 @@ describe('Environment item', () => { ...@@ -151,7 +148,7 @@ describe('Environment item', () => {
it('should render user avatar with link to profile', () => { it('should render user avatar with link to profile', () => {
expect( expect(
component.$el.querySelector('.js-deploy-user-container').getAttribute('href'), component.$el.querySelector('.js-deploy-user-container').getAttribute('href'),
).toEqual(environment.latest.last_deployment.user.web_url); ).toEqual(environment.last_deployment.user.web_url);
}); });
}); });
...@@ -159,13 +156,13 @@ describe('Environment item', () => { ...@@ -159,13 +156,13 @@ describe('Environment item', () => {
it('Should link to build url provided', () => { it('Should link to build url provided', () => {
expect( expect(
component.$el.querySelector('.build-link').getAttribute('href'), component.$el.querySelector('.build-link').getAttribute('href'),
).toEqual(environment.latest.last_deployment.deployable.build_path); ).toEqual(environment.last_deployment.deployable.build_path);
}); });
it('Should render deployable name and id', () => { it('Should render deployable name and id', () => {
expect( expect(
component.$el.querySelector('.build-link').getAttribute('href'), component.$el.querySelector('.build-link').getAttribute('href'),
).toEqual(environment.latest.last_deployment.deployable.build_path); ).toEqual(environment.last_deployment.deployable.build_path);
}); });
}); });
......
...@@ -49,7 +49,7 @@ describe('Environment', () => { ...@@ -49,7 +49,7 @@ describe('Environment', () => {
}); });
}); });
describe('with environments', () => { describe('with paginated environments', () => {
const environmentsResponseInterceptor = (request, next) => { const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({ next(request.respondWith(JSON.stringify({
environments: [environment], environments: [environment],
...@@ -57,11 +57,22 @@ describe('Environment', () => { ...@@ -57,11 +57,22 @@ describe('Environment', () => {
available_count: 0, 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(() => {
...@@ -71,10 +82,6 @@ describe('Environment', () => { ...@@ -71,10 +82,6 @@ describe('Environment', () => {
}); });
it('should render a table with environments', (done) => { it('should render a table with environments', (done) => {
component = new EnvironmentsComponent({
el: document.querySelector('#environments-list-view'),
});
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelectorAll('table tbody tr').length, component.$el.querySelectorAll('table tbody tr').length,
...@@ -82,6 +89,59 @@ describe('Environment', () => { ...@@ -82,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);
});
});
}); });
}); });
......
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');
});
});
const Store = require('~/environments/stores/environments_store'); const Store = require('~/environments/stores/environments_store');
const { environmentsList } = require('./mock_data'); const { environmentsList, serverData } = require('./mock_data');
(() => { (() => {
describe('Store', () => { describe('Store', () => {
...@@ -10,24 +10,49 @@ const { environmentsList } = require('./mock_data'); ...@@ -10,24 +10,49 @@ const { environmentsList } = require('./mock_data');
}); });
it('should start with a blank state', () => { it('should start with a blank state', () => {
expect(store.state.environments.length).toBe(0); expect(store.state.environments.length).toEqual(0);
expect(store.state.stoppedCounter).toBe(0); expect(store.state.stoppedCounter).toEqual(0);
expect(store.state.availableCounter).toBe(0); expect(store.state.availableCounter).toEqual(0);
expect(store.state.paginationInformation).toEqual({});
}); });
it('should store environments', () => { it('should store environments', () => {
store.storeEnvironments(environmentsList); store.storeEnvironments(serverData);
expect(store.state.environments.length).toBe(environmentsList.length); expect(store.state.environments.length).toEqual(serverData.length);
expect(store.state.environments[0]).toEqual(environmentsList[0]);
}); });
it('should store available count', () => { it('should store available count', () => {
store.storeAvailableCount(2); store.storeAvailableCount(2);
expect(store.state.availableCounter).toBe(2); expect(store.state.availableCounter).toEqual(2);
}); });
it('should store stopped count', () => { it('should store stopped count', () => {
store.storeStoppedCount(2); store.storeStoppedCount(2);
expect(store.state.stoppedCounter).toBe(2); expect(store.state.stoppedCounter).toEqual(2);
});
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 = [
{
name: 'DEV',
size: 1,
id: 7,
state: 'available',
external_url: null,
environment_type: null,
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',
},
{
folderName: 'build',
size: 5,
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',
},
];
const serverData = [
{ {
name: 'DEV', name: 'DEV',
size: 1, size: 1,
...@@ -56,4 +88,5 @@ const environment = { ...@@ -56,4 +88,5 @@ const environment = {
module.exports = { module.exports = {
environmentsList, environmentsList,
environment, 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);
}); });
}); });
......
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