diff options
author | Filipa Lacerda <lacerda.filipa@gmail.com> | 2017-02-16 12:28:05 +0000 |
---|---|---|
committer | Filipa Lacerda <lacerda.filipa@gmail.com> | 2017-02-16 12:28:05 +0000 |
commit | 25c929387225394baf39420bb98d2c0f35eeab43 (patch) | |
tree | 6990df66de41df4c75411b50a400ac08a5cdba81 | |
parent | 19791b65b20a21f1d12315b5b75f52a01531693b (diff) | |
parent | ab3c546ff50d24e12c0883f1d7a0f749e46b5199 (diff) | |
download | gitlab-ce-25c929387225394baf39420bb98d2c0f35eeab43.tar.gz |
Merge branch 'fe-paginated-environments-api-add-subview' into 'fe-paginated-environments-api-add-pagination'
Adds sub view to list environments under a folder
See merge request !9169
19 files changed, 851 insertions, 257 deletions
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6 index d3d4efb8289..0cbf952ea5c 100644 --- a/app/assets/javascripts/environments/components/environment.js.es6 +++ b/app/assets/javascripts/environments/components/environment.js.es6 @@ -4,20 +4,21 @@ const Vue = require('vue'); Vue.use(require('vue-resource')); const EnvironmentsService = require('../services/environments_service'); -const EnvironmentItem = require('./environment_item'); -const Store = require('../stores/environments_store'); +const EnvironmentTable = require('./environments_table'); +const EnvironmentsStore = require('../stores/environments_store'); require('../../vue_shared/components/table_pagination'); +require('../../lib/utils/common_utils'); module.exports = Vue.component('environment-component', { components: { - 'environment-item': EnvironmentItem, + 'environment-table': EnvironmentTable, 'table-pagination': gl.VueGlPagination, }, data() { const environmentsData = document.querySelector('#environments-list-view').dataset; - const store = new Store(); + const store = new EnvironmentsStore(); return { store, @@ -45,19 +46,19 @@ module.exports = Vue.component('environment-component', { computed: { scope() { - return this.$options.getQueryParameter('scope'); + return gl.utils.getParameterByName('scope'); }, canReadEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canReadEnvironment); + return gl.utils.convertPermissionToBoolean(this.canReadEnvironment); }, canCreateDeploymentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateDeployment); + return gl.utils.convertPermissionToBoolean(this.canCreateDeployment); }, canCreateEnvironmentParsed() { - return this.$options.convertPermissionToBoolean(this.canCreateEnvironment); + return gl.utils.convertPermissionToBoolean(this.canCreateEnvironment); }, }, @@ -67,8 +68,8 @@ module.exports = Vue.component('environment-component', { * Toggles loading property. */ created() { - const scope = this.$options.getQueryParameter('scope') || this.visibility; - const pageNumber = this.$options.getQueryParameter('page') || this.pageNumber; + const scope = gl.utils.getParameterByName('scope') || this.visibility; + const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber; const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`; @@ -96,30 +97,6 @@ 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: { toggleRow(model) { return this.store.toggleFolder(model.name); @@ -128,26 +105,11 @@ module.exports = Vue.component('environment-component', { /** * Will change the page number and update the URL. * - * If no search params are present, we'll add param for page - * If param for page is already present, we'll update it - * If there are params but none for page, we'll add it at the end. - * * @param {Number} pageNumber desired page to go to. + * @return {String} */ changePage(pageNumber) { - let param; - if (window.location.search.length === 0) { - param = `?page=${pageNumber}`; - } - - if (window.location.search.indexOf('page') !== -1) { - param = window.location.search.replace(/page=\d/g, `page=${pageNumber}`); - } - - if (window.location.search.length && - window.location.search.indexOf('page') === -1) { - param = `${window.location.search}&page=${pageNumber}`; - } + const param = gl.utils.setParamInURL('page', pageNumber); gl.utils.visitUrl(param); return param; @@ -158,7 +120,7 @@ module.exports = Vue.component('environment-component', { <div :class="cssContainerClass"> <div class="top-area"> <ul v-if="!isLoading" class="nav-links"> - <li v-bind:class="{ 'active': scope === undefined || scope === 'available' }"> + <li v-bind:class="{ 'active': scope === null || scope === 'available' }"> <a :href="projectEnvironmentsPath"> Available <span class="badge js-available-environments-count"> @@ -209,30 +171,15 @@ module.exports = Vue.component('environment-component', { <div class="table-holder" v-if="!isLoading && state.environments.length > 0"> - <table class="table ci-table environments"> - <thead> - <tr> - <th class="environments-name">Environment</th> - <th class="environments-deploy">Last deployment</th> - <th class="environments-build">Job</th> - <th class="environments-commit">Commit</th> - <th class="environments-date">Updated</th> - <th class="hidden-xs environments-actions"></th> - </tr> - </thead> - <tbody> - <template v-for="model in state.environments" - v-bind:model="model"> - <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> + + <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" diff --git a/app/assets/javascripts/environments/components/environment_item.js.es6 b/app/assets/javascripts/environments/components/environment_item.js.es6 index fc45c3c5f53..24fd58a301a 100644 --- a/app/assets/javascripts/environments/components/environment_item.js.es6 +++ b/app/assets/javascripts/environments/components/environment_item.js.es6 @@ -72,9 +72,9 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean} */ hasLastDeploymentKey() { - if (this.model.latest && - this.model.latest.last_deployment && - !this.$options.isObjectEmpty(this.model.latest.last_deployment)) { + if (this.model && + this.model.last_deployment && + !this.$options.isObjectEmpty(this.model.last_deployment)) { return true; } return false; @@ -87,10 +87,10 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean|Undefined} */ hasManualActions() { - return this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.manual_actions && - this.model.latest.last_deployment.manual_actions.length > 0; + return this.model && + this.model.last_deployment && + this.model.last_deployment.manual_actions && + this.model.last_deployment.manual_actions.length > 0; }, /** @@ -99,7 +99,7 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean} */ hasStopAction() { - return this.model.latest['stop_action?']; + return this.model && this.model['stop_action?']; }, /** @@ -109,10 +109,10 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean|Undefined} */ canRetry() { - return this.model.latest && + return this.model && this.hasLastDeploymentKey && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable; + this.model.last_deployment && + this.model.last_deployment.deployable; }, /** @@ -121,10 +121,10 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean|Undefined} */ canShowDate() { - return this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable && - this.model.latest.last_deployment.deployable !== undefined; + return this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable !== undefined; }, /** @@ -133,11 +133,11 @@ module.exports = Vue.component('environment-item', { * @returns {String} */ createdDate() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable && - this.model.latest.last_deployment.deployable.created_at) { - return timeagoInstance.format(this.model.latest.last_deployment.deployable.created_at); + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.created_at) { + return timeagoInstance.format(this.model.last_deployment.deployable.created_at); } return ''; }, @@ -149,7 +149,7 @@ module.exports = Vue.component('environment-item', { */ manualActions() { if (this.hasManualActions) { - return this.model.latest.last_deployment.manual_actions.map((action) => { + return this.model.last_deployment.manual_actions.map((action) => { const parsedAction = { name: gl.text.humanize(action.name), play_path: action.play_path, @@ -166,11 +166,11 @@ module.exports = Vue.component('environment-item', { * @returns {String} */ userImageAltDescription() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.user && - this.model.latest.last_deployment.user.username) { - return `${this.model.latest.last_deployment.user.username}'s avatar'`; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.user && + this.model.last_deployment.user.username) { + return `${this.model.last_deployment.user.username}'s avatar'`; } return ''; }, @@ -181,10 +181,10 @@ module.exports = Vue.component('environment-item', { * @returns {String|Undefined} */ commitTag() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.tag) { - return this.model.latest.last_deployment.tag; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.tag) { + return this.model.last_deployment.tag; } return undefined; }, @@ -195,10 +195,10 @@ module.exports = Vue.component('environment-item', { * @returns {Object|Undefined} */ commitRef() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.ref) { - return this.model.latest.last_deployment.ref; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.ref) { + return this.model.last_deployment.ref; } return undefined; }, @@ -209,11 +209,11 @@ module.exports = Vue.component('environment-item', { * @returns {String|Undefined} */ commitUrl() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.commit && - this.model.latest.last_deployment.commit.commit_path) { - return this.model.latest.last_deployment.commit.commit_path; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.commit_path) { + return this.model.last_deployment.commit.commit_path; } return undefined; }, @@ -224,11 +224,11 @@ module.exports = Vue.component('environment-item', { * @returns {String|Undefined} */ commitShortSha() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.commit && - this.model.latest.last_deployment.commit.short_id) { - return this.model.latest.last_deployment.commit.short_id; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.short_id) { + return this.model.last_deployment.commit.short_id; } return undefined; }, @@ -239,11 +239,11 @@ module.exports = Vue.component('environment-item', { * @returns {String|Undefined} */ commitTitle() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.commit && - this.model.latest.last_deployment.commit.title) { - return this.model.latest.last_deployment.commit.title; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.title) { + return this.model.last_deployment.commit.title; } return undefined; }, @@ -254,11 +254,11 @@ module.exports = Vue.component('environment-item', { * @returns {Object|Undefined} */ commitAuthor() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.commit && - this.model.latest.last_deployment.commit.author) { - return this.model.latest.last_deployment.commit.author; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.commit && + this.model.last_deployment.commit.author) { + return this.model.last_deployment.commit.author; } return undefined; @@ -270,11 +270,11 @@ module.exports = Vue.component('environment-item', { * @returns {String|Undefined} */ retryUrl() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable && - this.model.latest.last_deployment.deployable.retry_path) { - return this.model.latest.last_deployment.deployable.retry_path; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.retry_path) { + return this.model.last_deployment.deployable.retry_path; } return undefined; }, @@ -285,8 +285,8 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean|Undefined} */ isLastDeployment() { - return this.model.latest && this.model.latest.last_deployment && - this.model.latest.last_deployment['last?']; + return this.model && this.model.last_deployment && + this.model.last_deployment['last?']; }, /** @@ -295,10 +295,10 @@ module.exports = Vue.component('environment-item', { * @returns {String} */ buildName() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable) { - return `${this.model.latest.last_deployment.deployable.name} #${this.model.latest.last_deployment.deployable.id}`; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable) { + return `${this.model.last_deployment.deployable.name} #${this.model.last_deployment.deployable.id}`; } return ''; }, @@ -309,10 +309,10 @@ module.exports = Vue.component('environment-item', { * @returns {String} */ deploymentInternalId() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.iid) { - return `#${this.model.latest.last_deployment.iid}`; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.iid) { + return `#${this.model.last_deployment.iid}`; } return ''; }, @@ -323,9 +323,9 @@ module.exports = Vue.component('environment-item', { * @returns {Boolean} */ deploymentHasUser() { - return this.model.latest && - !this.$options.isObjectEmpty(this.model.latest.last_deployment) && - !this.$options.isObjectEmpty(this.model.latest.last_deployment.user); + return this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user); }, /** @@ -335,10 +335,10 @@ module.exports = Vue.component('environment-item', { * @returns {Object} */ deploymentUser() { - if (this.model.latest && - !this.$options.isObjectEmpty(this.model.latest.last_deployment) && - !this.$options.isObjectEmpty(this.model.latest.last_deployment.user)) { - return this.model.latest.last_deployment.user; + if (this.model && + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.user)) { + return this.model.last_deployment.user; } return {}; }, @@ -352,9 +352,8 @@ module.exports = Vue.component('environment-item', { */ shouldRenderBuildName() { return !this.model.isFolder && - this.model.latest && - !this.$options.isObjectEmpty(this.model.latest.last_deployment) && - !this.$options.isObjectEmpty(this.model.latest.last_deployment.deployable); + !this.$options.isObjectEmpty(this.model.last_deployment) && + !this.$options.isObjectEmpty(this.model.last_deployment.deployable); }, /** @@ -363,11 +362,11 @@ module.exports = Vue.component('environment-item', { * @return {String} */ buildPath() { - if (this.model.latest && - this.model.latest.last_deployment && - this.model.latest.last_deployment.deployable && - this.model.latest.last_deployment.deployable.build_path) { - return this.model.latest.last_deployment.deployable.build_path; + if (this.model && + this.model.last_deployment && + this.model.last_deployment.deployable && + this.model.last_deployment.deployable.build_path) { + return this.model.last_deployment.deployable.build_path; } return ''; @@ -379,8 +378,8 @@ module.exports = Vue.component('environment-item', { * @return {String} */ externalURL() { - if (this.model.latest && this.model.latest.external_url) { - return this.model.latest.external_url; + if (this.model && this.model.external_url) { + return this.model.external_url; } return ''; @@ -395,18 +394,27 @@ module.exports = Vue.component('environment-item', { */ shouldRenderDeploymentID() { return !this.model.isFolder && - this.model.latest && - !this.$options.isObjectEmpty(this.model.latest.last_deployment) && - this.model.latest.last_deployment.iid !== undefined; + !this.$options.isObjectEmpty(this.model.last_deployment) && + this.model.last_deployment.iid !== undefined; }, environmentPath() { - if (this.model && this.model.latest && this.model.latest.environment_path) { - return this.model.latest.environment_path; + if (this.model && this.model.environment_path) { + return this.model.environment_path; } return ''; }, + + /** + * Constructs folder URL based on the current location and the folder id. + * + * @return {String} + */ + folderUrl() { + return `${window.location.pathname}/folders/${this.model.folderName}`; + }, + }, /** @@ -432,14 +440,13 @@ module.exports = Vue.component('environment-item', { :href="environmentPath"> {{model.name}} </a> - <a v-else class="folder-name"> + <a v-else class="folder-name" :href="folderUrl"> <span class="folder-icon"> - <i class="fa fa-caret-right" aria-hidden="true"></i> <i class="fa fa-folder" aria-hidden="true"></i> </span> <span> - {{model.name}} + {{model.folderName}} </span> <span class="badge"> @@ -513,18 +520,18 @@ module.exports = Vue.component('environment-item', { </external-url-component> </div> - <div v-if="hasStopAction && canCreateDeployment && model.latest" + <div v-if="hasStopAction && canCreateDeployment" class="inline js-stop-component-container"> <stop-component - :stop-url="model.latest.stop_path"> + :stop-url="model.stop_path"> </stop-component> </div> - <div v-if="model.latest && model.latest.terminal_path" + <div v-if="model && model.terminal_path" class="inline js-terminal-button-container"> <terminal-button-component :terminal-icon-svg="terminalIconSvg" - :terminal-path="model.latest.terminal_path"> + :terminal-path="model.terminal_path"> </terminal-button-component> </div> diff --git a/app/assets/javascripts/environments/components/environments_table.js.es6 b/app/assets/javascripts/environments/components/environments_table.js.es6 new file mode 100644 index 00000000000..fd35d77fd3d --- /dev/null +++ b/app/assets/javascripts/environments/components/environments_table.js.es6 @@ -0,0 +1,74 @@ +/** + * 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> + `, +}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 new file mode 100644 index 00000000000..29f704c1a37 --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js.es6 @@ -0,0 +1,14 @@ +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'), + }); +}); diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 new file mode 100644 index 00000000000..0b1204559da --- /dev/null +++ b/app/assets/javascripts/environments/folder/environments_folder_view.js.es6 @@ -0,0 +1,181 @@ +/* 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> + `, +}); diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6 index 2dc6bbe4761..15cd9bde08e 100644 --- a/app/assets/javascripts/environments/stores/environments_store.js.es6 +++ b/app/assets/javascripts/environments/stores/environments_store.js.es6 @@ -20,8 +20,12 @@ class EnvironmentsStore { * * 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 } + * 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. * In those cases we add `isFolder` key in order to render it properly. @@ -31,11 +35,20 @@ class EnvironmentsStore { */ storeEnvironments(environments = []) { const filteredEnvironments = environments.map((env) => { + let filtered = {}; + 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; @@ -45,14 +58,7 @@ class EnvironmentsStore { setPagination(pagination = {}) { const normalizedHeaders = gl.utils.normalizeHeaders(pagination); - const paginationInformation = { - perPage: parseInt(normalizedHeaders['X-PER-PAGE'], 10), - page: parseInt(normalizedHeaders['X-PAGE'], 10), - total: parseInt(normalizedHeaders['X-TOTAL'], 10), - totalPages: parseInt(normalizedHeaders['X-TOTAL-PAGES'], 10), - nextPage: parseInt(normalizedHeaders['X-NEXT-PAGE'], 10), - previousPage: parseInt(normalizedHeaders['X-PREV-PAGE'], 10), - }; + const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders); this.state.paginationInformation = paginationInformation; return paginationInformation; diff --git a/app/assets/javascripts/lib/utils/common_utils.js.es6 b/app/assets/javascripts/lib/utils/common_utils.js.es6 index bcb3a706b51..764aff51fee 100644 --- a/app/assets/javascripts/lib/utils/common_utils.js.es6 +++ b/app/assets/javascripts/lib/utils/common_utils.js.es6 @@ -232,6 +232,21 @@ }; /** + * 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. * * @param {DOMStringMap} DOMStringMapObject @@ -241,5 +256,45 @@ acc[element] = DOMStringMapObject[element]; 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); }).call(this); diff --git a/app/assets/javascripts/vue_pipelines_index/store.js.es6 b/app/assets/javascripts/vue_pipelines_index/store.js.es6 index 0ee21f00fdc..572f0493c9f 100644 --- a/app/assets/javascripts/vue_pipelines_index/store.js.es6 +++ b/app/assets/javascripts/vue_pipelines_index/store.js.es6 @@ -5,16 +5,7 @@ require('../vue_realtime_listener'); ((gl) => { const pageValues = (headers) => { const normalized = gl.utils.normalizeHeaders(headers); - - 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'], - }; - + const paginationInfo = gl.utils.normalizeHeaders(normalized); return paginationInfo; }; diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss index 606cf501b82..181dcb7721f 100644 --- a/app/assets/stylesheets/pages/environments.scss +++ b/app/assets/stylesheets/pages/environments.scss @@ -10,6 +10,11 @@ font-size: 34px; } +.environments-folder-name { + font-weight: normal; + padding-top: 20px; +} + @media (max-width: $screen-xs-max) { .environments-container { width: 100%; @@ -113,6 +118,7 @@ .folder-icon { margin-right: 3px; color: $gl-text-color-secondary; + display: inline-block; .fa:nth-child(1) { margin-right: 3px; @@ -122,6 +128,7 @@ .folder-name { cursor: pointer; color: $gl-text-color-secondary; + display: inline-block; } } diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml new file mode 100644 index 00000000000..d9cb7bc0331 --- /dev/null +++ b/app/views/projects/environments/folder.html.haml @@ -0,0 +1,13 @@ +- @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") } } diff --git a/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml new file mode 100644 index 00000000000..7e626982de6 --- /dev/null +++ b/changelogs/unreleased/fe-paginated-environments-api-add-subview.yml @@ -0,0 +1,4 @@ +--- +title: Adds paginationd and folders view to environments table +merge_request: +author: diff --git a/config/webpack.config.js b/config/webpack.config.js index 00f448c1fbb..7fda5405ea2 100644 --- a/config/webpack.config.js +++ b/config/webpack.config.js @@ -22,6 +22,7 @@ var config = { commit_pipelines: './commit/pipelines/pipelines_bundle.js', diff_notes: './diff_notes/diff_notes_bundle.js', environments: './environments/environments_bundle.js', + environments_folder: './environments/folder/environments_folder_bundle.js', filtered_search: './filtered_search/filtered_search_bundle.js', graphs: './graphs/graphs_bundle.js', issuable: './issuable/issuable_bundle.js', diff --git a/spec/javascripts/environments/environment_item_spec.js.es6 b/spec/javascripts/environments/environment_item_spec.js.es6 index 88413d9ae2b..7fea80ed799 100644 --- a/spec/javascripts/environments/environment_item_spec.js.es6 +++ b/spec/javascripts/environments/environment_item_spec.js.es6 @@ -14,11 +14,10 @@ describe('Environment item', () => { beforeEach(() => { mockItem = { name: 'review', + folderName: 'review', size: 3, isFolder: true, - latest: { - environment_path: 'url', - }, + environment_path: 'url', }; component = new EnvironmentItem({ @@ -49,21 +48,36 @@ describe('Environment item', () => { environment = { name: 'production', size: 1, - latest: { - state: 'stopped', - external_url: 'http://external.com', - environment_type: null, - last_deployment: { - id: 66, - iid: 6, - sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - ref: { - name: 'master', - ref_path: 'root/ci-folders/tree/master', - }, - tag: true, - 'last?': true, - user: { + state: 'stopped', + external_url: 'http://external.com', + environment_type: null, + last_deployment: { + id: 66, + iid: 6, + sha: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + ref: { + name: 'master', + ref_path: 'root/ci-folders/tree/master', + }, + tag: true, + 'last?': true, + user: { + name: 'Administrator', + username: 'root', + id: 1, + state: 'active', + avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', + web_url: 'http://localhost:3000/root', + }, + commit: { + id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', + short_id: '500aabcb', + title: 'Update .gitlab-ci.yml', + author_name: 'Administrator', + author_email: 'admin@example.com', + created_at: '2016-11-07T18:28:13.000+00:00', + message: 'Update .gitlab-ci.yml', + author: { name: 'Administrator', username: 'root', id: 1, @@ -71,44 +85,27 @@ describe('Environment item', () => { avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', web_url: 'http://localhost:3000/root', }, - commit: { - id: '500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - short_id: '500aabcb', - title: 'Update .gitlab-ci.yml', - author_name: 'Administrator', - author_email: 'admin@example.com', - created_at: '2016-11-07T18:28:13.000+00:00', - message: 'Update .gitlab-ci.yml', - author: { - name: 'Administrator', - username: 'root', - id: 1, - state: 'active', - avatar_url: 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon', - web_url: 'http://localhost:3000/root', - }, - commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', - }, - deployable: { - id: 1279, - name: 'deploy', - build_path: '/root/ci-folders/builds/1279', - retry_path: '/root/ci-folders/builds/1279/retry', - created_at: '2016-11-29T18:11:58.430Z', - updated_at: '2016-11-29T18:11:58.430Z', - }, - manual_actions: [ - { - name: 'action', - play_path: '/play', - }, - ], + commit_path: '/root/ci-folders/tree/500aabcb17c97bdcf2d0c410b70cb8556f0362dd', }, - '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', + 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, + 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({ @@ -129,7 +126,7 @@ describe('Environment item', () => { it('should render deployment internal id', () => { expect( component.$el.querySelector('.deployment-column span').textContent, - ).toContain(environment.latest.last_deployment.iid); + ).toContain(environment.last_deployment.iid); expect( component.$el.querySelector('.deployment-column span').textContent, @@ -139,7 +136,7 @@ describe('Environment item', () => { it('should render last deployment date', () => { const timeagoInstance = new timeago(); // eslint-disable-line const formatedDate = timeagoInstance.format( - environment.latest.last_deployment.deployable.created_at, + environment.last_deployment.deployable.created_at, ); expect( @@ -151,7 +148,7 @@ describe('Environment item', () => { it('should render user avatar with link to profile', () => { expect( 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', () => { it('Should link to build url provided', () => { expect( 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', () => { expect( component.$el.querySelector('.build-link').getAttribute('href'), - ).toEqual(environment.latest.last_deployment.deployable.build_path); + ).toEqual(environment.last_deployment.deployable.build_path); }); }); diff --git a/spec/javascripts/environments/environment_table_spec.js.es6 b/spec/javascripts/environments/environment_table_spec.js.es6 new file mode 100644 index 00000000000..be4330b5012 --- /dev/null +++ b/spec/javascripts/environments/environment_table_spec.js.es6 @@ -0,0 +1,30 @@ +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'); + }); +}); diff --git a/spec/javascripts/environments/environments_store_spec.js.es6 b/spec/javascripts/environments/environments_store_spec.js.es6 index 71c1abd0c91..77e182b3830 100644 --- a/spec/javascripts/environments/environments_store_spec.js.es6 +++ b/spec/javascripts/environments/environments_store_spec.js.es6 @@ -1,5 +1,5 @@ const Store = require('~/environments/stores/environments_store'); -const { environmentsList } = require('./mock_data'); +const { environmentsList, serverData } = require('./mock_data'); (() => { describe('Store', () => { @@ -17,8 +17,9 @@ const { environmentsList } = require('./mock_data'); }); it('should store environments', () => { - store.storeEnvironments(environmentsList); - expect(store.state.environments.length).toEqual(environmentsList.length); + store.storeEnvironments(serverData); + expect(store.state.environments.length).toEqual(serverData.length); + expect(store.state.environments[0]).toEqual(environmentsList[0]); }); it('should store available count', () => { diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 new file mode 100644 index 00000000000..d1335b5b304 --- /dev/null +++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js.es6 @@ -0,0 +1,202 @@ +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); + }); + }); +}); diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6 index 081897f5456..5c395c6b2d8 100644 --- a/spec/javascripts/environments/mock_data.js.es6 +++ b/spec/javascripts/environments/mock_data.js.es6 @@ -2,6 +2,38 @@ 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', + size: 1, latest: { id: 7, name: 'DEV', @@ -56,4 +88,5 @@ const environment = { module.exports = { environmentsList, environment, + serverData, }; diff --git a/spec/javascripts/fixtures/environments/environments_folder_view.html.haml b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml new file mode 100644 index 00000000000..aceec139730 --- /dev/null +++ b/spec/javascripts/fixtures/environments/environments_folder_view.html.haml @@ -0,0 +1,7 @@ +%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") } } diff --git a/spec/javascripts/lib/utils/common_utils_spec.js.es6 b/spec/javascripts/lib/utils/common_utils_spec.js.es6 index 006ede21093..f4d3e77e515 100644 --- a/spec/javascripts/lib/utils/common_utils_spec.js.es6 +++ b/spec/javascripts/lib/utils/common_utils_spec.js.es6 @@ -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', () => { it('should identify meta click on Windows/Linux', () => { const e = { |