Commit 904a625e authored by Phil Hughes's avatar Phil Hughes

Merge branch '32098-environments-reusable' into 'master'

Improve environments performance

Closes #32098

See merge request gitlab-org/gitlab-ce!15484
parents 1d8ab59e 45631562
<script>
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
export default {
props: {
isLoading: {
type: Boolean,
required: true,
},
environments: {
type: Array,
required: true,
},
pagination: {
type: Object,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
},
components: {
environmentTable,
loadingIcon,
tablePagination,
},
methods: {
onChangePage(page) {
this.$emit('onChangePage', page);
},
},
};
</script>
<template>
<div class="environments-container">
<loading-icon
label="Loading environments"
v-if="isLoading"
size="3"
/>
<slot name="emptyState"></slot>
<div
class="table-holder"
v-if="!isLoading && environments.length > 0">
<environment-table
:environments="environments"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
/>
<table-pagination
v-if="pagination && pagination.totalPages > 1"
:change="onChangePage"
:pageInfo="pagination"
/>
</div>
</div>
</template>
<script>
export default {
name: 'environmentsEmptyState',
props: {
newPath: {
type: String,
required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
helpPath: {
type: String,
required: true,
},
},
};
</script>
<template>
<div class="blank-state-row">
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
{{s__("Environments|You don't have any environments right now.")}}
</h2>
<p class="blank-state-text">
{{s__("Environments|Environments are places where code gets deployed, such as staging or production.")}}
<br />
<a :href="helpPath">
{{s__("Environments|Read more about environments")}}
</a>
</p>
<a
v-if="canCreateEnvironment"
:href="newPath"
class="btn btn-create js-new-environment-button">
{{s__("Environments|New environment")}}
</a>
</div>
</div>
</template>
<script>
import Visibility from 'visibilityjs';
import Flash from '../../flash';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
import eventHub from '../event_hub';
import Poll from '../../lib/utils/poll';
import environmentsMixin from '../mixins/environments_mixin';
export default {
components: {
environmentTable,
tablePagination,
loadingIcon,
},
mixins: [
environmentsMixin,
],
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
return {
store,
state: store.state,
visibility: 'available',
isLoading: false,
cssContainerClass: environmentsData.cssClass,
endpoint: environmentsData.environmentsDataEndpoint,
canCreateDeployment: environmentsData.canCreateDeployment,
canReadEnvironment: environmentsData.canReadEnvironment,
canCreateEnvironment: environmentsData.canCreateEnvironment,
projectEnvironmentsPath: environmentsData.projectEnvironmentsPath,
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return getParameterByName('scope');
},
canReadEnvironmentParsed() {
return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return convertPermissionToBoolean(this.canCreateDeployment);
},
canCreateEnvironmentParsed() {
return convertPermissionToBoolean(this.canCreateEnvironment);
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
methods: {
toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, true);
}
},
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
* @return {String}
*/
changePage(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
fetchChildEnvironments(folder, showLoader = false) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
.then(resp => resp.json())
.then(response => this.store.setfolderContent(folder, response.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
postAction(endpoint) {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => new Flash('An error occurred while making the request.'));
}
},
successCallback(resp) {
this.saveData(resp);
// We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders();
if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder));
}
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
},
},
};
</script>
<template>
<div :class="cssContainerClass">
<div class="top-area">
<ul
v-if="!isLoading"
class="nav-links">
<li :class="{ active: scope === null || scope === 'available' }">
<a :href="projectEnvironmentsPath">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :class="{ active : scope === 'stopped' }">
<a :href="projectStoppedEnvironmentsPath">
Stopped
<span class="badge js-stopped-environments-count">
{{state.stoppedCounter}}
</span>
</a>
</li>
</ul>
<div
v-if="canCreateEnvironmentParsed && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
New environment
</a>
</div>
</div>
<div class="environments-container">
<loading-icon
label="Loading environments"
size="3"
v-if="isLoading"
/>
<div
class="blank-state-row"
v-if="!isLoading && state.environments.length === 0">
<div class="blank-state-center">
<h2 class="blank-state-title js-blank-state-title">
You don't have any environments right now.
</h2>
<p class="blank-state-text">
Environments are places where code gets deployed, such as staging or production.
<br />
<a :href="helpPagePath">
Read more about environments
</a>
</p>
<a
v-if="canCreateEnvironmentParsed"
:href="newEnvironmentPath"
class="btn btn-create js-new-environment-button">
New environment
</a>
</div>
</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"
/>
</div>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation" />
</div>
</div>
</template>
<script> <script>
import tooltip from '../../vue_shared/directives/tooltip'; import tooltip from '../../vue_shared/directives/tooltip';
import { s__ } from '../../locale';
/** /**
* Renders the external url link in environments table. * Renders the external url link in environments table.
...@@ -18,7 +19,7 @@ export default { ...@@ -18,7 +19,7 @@ export default {
computed: { computed: {
title() { title() {
return 'Open'; return s__('Environments|Open');
}, },
}, },
}; };
......
...@@ -432,7 +432,7 @@ export default { ...@@ -432,7 +432,7 @@ export default {
v-if="!model.isFolder" v-if="!model.isFolder"
class="table-mobile-header" class="table-mobile-header"
role="rowheader"> role="rowheader">
Environment {{s__("Environments|Environment")}}
</div> </div>
<a <a
v-if="!model.isFolder" v-if="!model.isFolder"
...@@ -505,7 +505,7 @@ export default { ...@@ -505,7 +505,7 @@ export default {
<div <div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Commit {{s__("Environments|Commit")}}
</div> </div>
<div <div
v-if="hasLastDeploymentKey" v-if="hasLastDeploymentKey"
...@@ -521,7 +521,7 @@ export default { ...@@ -521,7 +521,7 @@ export default {
<div <div
v-if="!hasLastDeploymentKey" v-if="!hasLastDeploymentKey"
class="commit-title table-mobile-content"> class="commit-title table-mobile-content">
No deployments yet {{s__("Environments|No deployments yet")}}
</div> </div>
</div> </div>
...@@ -531,7 +531,7 @@ export default { ...@@ -531,7 +531,7 @@ export default {
<div <div
role="rowheader" role="rowheader"
class="table-mobile-header"> class="table-mobile-header">
Updated {{s__("Environments|Updated")}}
</div> </div>
<span <span
v-if="canShowDate" v-if="canShowDate"
......
...@@ -34,6 +34,7 @@ export default { ...@@ -34,6 +34,7 @@ export default {
:aria-label="title"> :aria-label="title">
<i <i
class="fa fa-area-chart" class="fa fa-area-chart"
aria-hidden="true" /> aria-hidden="true"
/>
</a> </a>
</template> </template>
...@@ -48,10 +48,10 @@ export default { ...@@ -48,10 +48,10 @@ export default {
:disabled="isLoading"> :disabled="isLoading">
<span v-if="isLastDeployment"> <span v-if="isLastDeployment">
Re-deploy {{s__("Environments|Re-deploy")}}
</span> </span>
<span v-else> <span v-else>
Rollback {{s__("Environments|Rollback")}}
</span> </span>
<loading-icon v-if="isLoading" /> <loading-icon v-if="isLoading" />
......
<script>
import Flash from '../../flash';
import { s__ } from '../../locale';
import emptyState from './empty_state.vue';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default {
props: {
endpoint: {
type: String,
required: true,
},
canCreateEnvironment: {
type: Boolean,
required: true,
},
canCreateDeployment: {
type: Boolean,
required: true,
},
canReadEnvironment: {
type: Boolean,
required: true,
},
cssContainerClass: {
type: String,
required: true,
},
newEnvironmentPath: {
type: String,
required: true,
},
helpPagePath: {
type: String,
required: true,
},
},
components: {
emptyState,
},
mixins: [
CIPaginationMixin,
environmentsMixin,
],
created() {
eventHub.$on('toggleFolder', this.toggleFolder);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
},
methods: {
toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
this.fetchChildEnvironments(folder, true);
}
},
fetchChildEnvironments(folder, showLoader = false) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
this.service.getFolderContent(folder.folder_path)
.then(resp => resp.json())
.then(response => this.store.setfolderContent(folder, response.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
Flash(s__('Environments|An error occurred while fetching the environments.'));
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
successCallback(resp) {
this.saveData(resp);
// We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders();
if (openFolders.length) {
openFolders.forEach(folder => this.fetchChildEnvironments(folder));
}
},
},
};
</script>
<template>
<div :class="cssContainerClass">
<div class="top-area">
<tabs
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="environments"
/>
<div
v-if="canCreateEnvironment && !isLoading"
class="nav-controls">
<a
:href="newEnvironmentPath"
class="btn btn-create">
{{s__("Environments|New environment")}}
</a>
</div>
</div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
>
<empty-state
slot="emptyState"
v-if="!isLoading && state.environments.length === 0"
:new-path="newEnvironmentPath"
:help-path="helpPagePath"
:can-create-environment="canCreateEnvironment"
/>
</container>
</div>
</template>
...@@ -2,12 +2,12 @@ ...@@ -2,12 +2,12 @@
/** /**
* Render environments table. * Render environments table.
*/ */
import EnvironmentTableRowComponent from './environment_item.vue'; import environmentItem from './environment_item.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default { export default {
components: { components: {
'environment-item': EnvironmentTableRowComponent, environmentItem,
loadingIcon, loadingIcon,
}, },
...@@ -42,19 +42,19 @@ export default { ...@@ -42,19 +42,19 @@ export default {
<div class="ci-table" role="grid"> <div class="ci-table" role="grid">
<div class="gl-responsive-table-row table-row-header" role="row"> <div class="gl-responsive-table-row table-row-header" role="row">
<div class="table-section section-10 environments-name" role="columnheader"> <div class="table-section section-10 environments-name" role="columnheader">
Environment {{s__("Environments|Environment")}}
</div> </div>
<div class="table-section section-10 environments-deploy" role="columnheader"> <div class="table-section section-10 environments-deploy" role="columnheader">
Deployment {{s__("Environments|Deployment")}}
</div> </div>
<div class="table-section section-15 environments-build" role="columnheader"> <div class="table-section section-15 environments-build" role="columnheader">
Job {{s__("Environments|Job")}}
</div> </div>
<div class="table-section section-25 environments-commit" role="columnheader"> <div class="table-section section-25 environments-commit" role="columnheader">
Commit {{s__("Environments|Commit")}}
</div> </div>
<div class="table-section section-10 environments-date" role="columnheader"> <div class="table-section section-10 environments-date" role="columnheader">
Updated {{s__("Environments|Updated")}}
</div> </div>
</div> </div>
<template <template
...@@ -86,7 +86,7 @@ export default { ...@@ -86,7 +86,7 @@ export default {
<a <a
:href="folderUrl(model)" :href="folderUrl(model)"
class="btn btn-default"> class="btn btn-default">
Show all {{s__("Environments|Show all")}}
</a> </a>
</div> </div>
</div> </div>
......
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsComponent from './components/environment.vue'; import environmentsComponent from './components/environments_app.vue';
import { convertPermissionToBoolean } from '../lib/utils/common_utils';
import Translate from '../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-list-view', el: '#environments-list-view',
components: { components: {
'environments-table-app': EnvironmentsComponent, environmentsComponent,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return {
endpoint: environmentsData.environmentsDataEndpoint,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
cssContainerClass: environmentsData.cssClass,
canCreateEnvironment: convertPermissionToBoolean(environmentsData.canCreateEnvironment),
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
return createElement('environments-component', {
props: {
endpoint: this.endpoint,
newEnvironmentPath: this.newEnvironmentPath,
helpPagePath: this.helpPagePath,
cssContainerClass: this.cssContainerClass,
canCreateEnvironment: this.canCreateEnvironment,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
}, },
render: createElement => createElement('environments-table-app'),
})); }));
import Vue from 'vue'; import Vue from 'vue';
import EnvironmentsFolderComponent from './environments_folder_view.vue'; import environmentsFolderApp from './environments_folder_view.vue';
import { convertPermissionToBoolean } from '../../lib/utils/common_utils';
import Translate from '../../vue_shared/translate';
Vue.use(Translate);
document.addEventListener('DOMContentLoaded', () => new Vue({ document.addEventListener('DOMContentLoaded', () => new Vue({
el: '#environments-folder-list-view', el: '#environments-folder-list-view',
components: { components: {
'environments-folder-app': EnvironmentsFolderComponent, environmentsFolderApp,
},
data() {
const environmentsData = document.querySelector(this.$options.el).dataset;
return {
endpoint: environmentsData.endpoint,
folderName: environmentsData.folderName,
cssContainerClass: environmentsData.cssClass,
canCreateDeployment: convertPermissionToBoolean(environmentsData.canCreateDeployment),
canReadEnvironment: convertPermissionToBoolean(environmentsData.canReadEnvironment),
};
},
render(createElement) {
return createElement('environments-folder-app', {
props: {
endpoint: this.endpoint,
folderName: this.folderName,
cssContainerClass: this.cssContainerClass,
canCreateDeployment: this.canCreateDeployment,
canReadEnvironment: this.canReadEnvironment,
},
});
}, },
render: createElement => createElement('environments-folder-app'),
})); }));
<script> <script>
import Visibility from 'visibilityjs'; import environmentsMixin from '../mixins/environments_mixin';
import Flash from '../../flash'; import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue'; export default {
import EnvironmentsStore from '../stores/environments_store'; props: {
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; endpoint: {
import tablePagination from '../../vue_shared/components/table_pagination.vue'; type: String,
import Poll from '../../lib/utils/poll'; required: true,
import eventHub from '../event_hub'; },
import environmentsMixin from '../mixins/environments_mixin'; folderName: {
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils'; type: String,
required: true,
export default { },
components: { cssContainerClass: {
environmentTable, type: String,
tablePagination, required: true,
loadingIcon, },
}, canCreateDeployment: {
type: Boolean,
mixins: [ required: true,
environmentsMixin, },
], canReadEnvironment: {
type: Boolean,
data() { required: true,
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,
// Pagination Properties,
paginationInformation: {},
pageNumber: 1,
};
},
computed: {
scope() {
return getParameterByName('scope');
},
canReadEnvironmentParsed() {
return convertPermissionToBoolean(this.canReadEnvironment);
},
canCreateDeploymentParsed() {
return 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 = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.service = new EnvironmentsService(this.endpoint);
const poll = new Poll({
resource: this.service,
method: 'get',
data: { scope, page },
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
}, },
});
if (!Visibility.hidden()) {
this.isLoading = true;
poll.makeRequest();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
poll.restart();
} else {
poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('postAction');
},
methods: {
/**
* Will change the page number and update the URL.
*
* @param {Number} pageNumber desired page to go to.
*/
changePage(pageNumber) {
const param = setParamInURL('page', pageNumber);
gl.utils.visitUrl(param);
return param;
},
fetchEnvironments() {
const scope = getParameterByName('scope') || this.visibility;
const page = getParameterByName('page') || this.pageNumber;
this.isLoading = true;
return this.service.get({ scope, page })
.then(this.successCallback)
.catch(this.errorCallback);
},
successCallback(resp) {
this.saveData(resp);
},
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
}, },
mixins: [
postAction(endpoint) { environmentsMixin,
if (!this.isMakingRequest) { CIPaginationMixin,
this.isLoading = true; ],
methods: {
this.service.postAction(endpoint) successCallback(resp) {
.then(() => this.fetchEnvironments()) this.saveData(resp);
.catch(() => new Flash('An error occurred while making the request.')); },
}
}, },
}, };
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
...@@ -171,56 +43,23 @@ export default { ...@@ -171,56 +43,23 @@ export default {
v-if="!isLoading"> v-if="!isLoading">
<h4 class="js-folder-name environments-folder-name"> <h4 class="js-folder-name environments-folder-name">
Environments / <b>{{folderName}}</b> {{s__("Environments|Environments")}} / <b>{{folderName}}</b>
</h4> </h4>
<ul class="nav-links"> <tabs
<li :class="{ active: scope === null || scope === 'available' }"> :tabs="tabs"
<a @onChangeTab="onChangeTab"
:href="availablePath" scope="environments"
class="js-available-environments-folder-tab">
Available
<span class="badge js-available-environments-count">
{{state.availableCounter}}
</span>
</a>
</li>
<li :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">
<loading-icon
label="Loading environments"
v-if="isLoading"
size="3"
/> />
<div
class="table-holder"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed"
:can-read-environment="canReadEnvironmentParsed"
/>
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
</div> </div>
<container
:is-loading="isLoading"
:environments="state.environments"
:pagination="state.paginationInformation"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
@onChangePage="onChangePage"
/>
</div> </div>
</template> </template>
/**
* Common code between environmets app and folder view
*/
import Visibility from 'visibilityjs';
import Poll from '../../lib/utils/poll';
import {
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
import { s__ } from '../../locale';
import Flash from '../../flash';
import eventHub from '../event_hub';
import EnvironmentsStore from '../stores/environments_store';
import EnvironmentsService from '../services/environments_service';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import environmentTable from '../components/environments_table.vue';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import container from '../components/container.vue';
export default { export default {
components: {
environmentTable,
container,
loadingIcon,
tabs,
tablePagination,
},
data() {
const store = new EnvironmentsStore();
return {
store,
state: store.state,
isLoading: false,
isMakingRequest: false,
scope: getParameterByName('scope') || 'available',
page: getParameterByName('page') || '1',
requestData: {},
};
},
methods: { methods: {
saveData(resp) { saveData(resp) {
const headers = resp.headers; const headers = resp.headers;
return resp.json().then((response) => { return resp.json().then((response) => {
this.isLoading = false; this.isLoading = false;
this.store.storeAvailableCount(response.available_count); if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeStoppedCount(response.stopped_count); this.store.storeAvailableCount(response.available_count);
this.store.storeEnvironments(response.environments); this.store.storeStoppedCount(response.stopped_count);
this.store.setPagination(headers); this.store.storeEnvironments(response.environments);
this.store.setPagination(headers);
}
}); });
}, },
/**
* Handles URL and query parameter changes.
* When the user uses the pagination or the tabs,
* - update URL
* - Make API request to the server with new parameters
* - Update the polling function
* - Update the internal state
*/
updateContent(parameters) {
this.updateInternalState(parameters);
// fetch new data
return this.service.get(this.requestData)
.then(response => this.successCallback(response))
.then(() => {
// restart polling
this.poll.restart({ data: this.requestData });
})
.catch(() => {
this.errorCallback();
// restart polling
this.poll.restart();
});
},
errorCallback() {
this.isLoading = false;
Flash(s__('Environments|An error occurred while fetching the environments.'));
},
postAction(endpoint) {
if (!this.isMakingRequest) {
this.isLoading = true;
this.service.postAction(endpoint)
.then(() => this.fetchEnvironments())
.catch(() => {
this.isLoading = false;
Flash(s__('Environments|An error occurred while making the request.'));
});
}
},
fetchEnvironments() {
this.isLoading = true;
return this.service.get(this.requestData)
.then(this.successCallback)
.catch(this.errorCallback);
},
},
computed: {
tabs() {
return [
{
name: s__('Available'),
scope: 'available',
count: this.state.availableCounter,
isActive: this.scope === 'available',
},
{
name: s__('Stopped'),
scope: 'stopped',
count: this.state.stoppedCounter,
isActive: this.scope === 'stopped',
},
];
},
},
/**
* Fetches all the environments and stores them.
* Toggles loading property.
*/
created() {
this.service = new EnvironmentsService(this.endpoint);
this.requestData = { page: this.page, scope: this.scope };
this.poll = new Poll({
resource: this.service,
method: 'get',
data: this.requestData,
successCallback: this.successCallback,
errorCallback: this.errorCallback,
notificationCallback: (isMakingRequest) => {
this.isMakingRequest = isMakingRequest;
},
});
if (!Visibility.hidden()) {
this.isLoading = true;
this.poll.makeRequest();
} else {
this.fetchEnvironments();
}
Visibility.change(() => {
if (!Visibility.hidden()) {
this.poll.restart();
} else {
this.poll.stop();
}
});
eventHub.$on('postAction', this.postAction);
},
beforeDestroyed() {
eventHub.$off('postAction');
}, },
}; };
...@@ -36,7 +36,12 @@ export default class EnvironmentsStore { ...@@ -36,7 +36,12 @@ export default class EnvironmentsStore {
storeEnvironments(environments = []) { storeEnvironments(environments = []) {
const filteredEnvironments = environments.map((env) => { const filteredEnvironments = environments.map((env) => {
const oldEnvironmentState = this.state.environments const oldEnvironmentState = this.state.environments
.find(element => element.id === env.latest.id) || {}; .find((element) => {
if (env.latest) {
return element.id === env.latest.id;
}
return element.id === env.id;
}) || {};
let filtered = {}; let filtered = {};
......
...@@ -269,46 +269,6 @@ export const parseIntPagination = paginationInformation => ({ ...@@ -269,46 +269,6 @@ export const parseIntPagination = paginationInformation => ({
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10), previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
}); });
/**
* Updates the search parameter of a URL given the parameter and value 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}
*/
export const setParamInURL = (param, value) => {
let search;
const locationSearch = window.location.search;
if (locationSearch.length) {
const parameters = locationSearch.substring(1, locationSearch.length)
.split('&')
.reduce((acc, element) => {
const val = element.split('=');
// eslint-disable-next-line no-param-reassign
acc[val[0]] = decodeURIComponent(val[1]);
return acc;
}, {});
parameters[param] = value;
const toString = Object.keys(parameters)
.map(val => `${val}=${encodeURIComponent(parameters[val])}`)
.join('&');
search = `?${toString}`;
} else {
search = `?${param}=${value}`;
}
return search;
};
/** /**
* Given a string of query parameters creates an object. * Given a string of query parameters creates an object.
* *
......
...@@ -3,15 +3,14 @@ ...@@ -3,15 +3,14 @@
import PipelinesService from '../services/pipelines_service'; import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines'; import pipelinesMixin from '../mixins/pipelines';
import tablePagination from '../../vue_shared/components/table_pagination.vue'; import tablePagination from '../../vue_shared/components/table_pagination.vue';
import navigationTabs from './navigation_tabs.vue'; import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
import navigationControls from './nav_controls.vue'; import navigationControls from './nav_controls.vue';
import { import {
convertPermissionToBoolean, convertPermissionToBoolean,
getParameterByName, getParameterByName,
historyPushState,
buildUrlWithCurrentLocation,
parseQueryStringIntoObject, parseQueryStringIntoObject,
} from '../../lib/utils/common_utils'; } from '../../lib/utils/common_utils';
import CIPaginationMixin from '../../vue_shared/mixins/ci_pagination_api_mixin';
export default { export default {
props: { props: {
...@@ -36,6 +35,7 @@ ...@@ -36,6 +35,7 @@
}, },
mixins: [ mixins: [
pipelinesMixin, pipelinesMixin,
CIPaginationMixin,
], ],
data() { data() {
const pipelinesData = document.querySelector('#pipelines-list-vue').dataset; const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
...@@ -170,22 +170,8 @@ ...@@ -170,22 +170,8 @@
* - Update the internal state * - Update the internal state
*/ */
updateContent(parameters) { updateContent(parameters) {
// stop polling this.updateInternalState(parameters);
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
// fetch new data // fetch new data
return this.service.getPipelines(this.requestData) return this.service.getPipelines(this.requestData)
.then((response) => { .then((response) => {
...@@ -203,14 +189,6 @@ ...@@ -203,14 +189,6 @@
this.poll.restart(); this.poll.restart();
}); });
}, },
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
}, },
}; };
</script> </script>
...@@ -235,6 +213,7 @@ ...@@ -235,6 +213,7 @@
<navigation-tabs <navigation-tabs
:tabs="tabs" :tabs="tabs"
@onChangeTab="onChangeTab" @onChangeTab="onChangeTab"
scope="pipelines"
/> />
<navigation-controls <navigation-controls
......
<script> <script>
/**
* Given an array of tabs, renders non linked bootstrap tabs.
* When a tab is clicked it will trigger an event and provide the clicked scope.
*
* This component is used in apps that handle the API call.
* If you only need to change the URL this component should not be used.
*
* @example
* <navigation-tabs
* :tabs="[
* {
* name: String,
* scope: String,
* count: Number || Undefined,
* isActive: Boolean,
* },
* ]"
* @onChangeTab="onChangeTab"
* />
*/
export default { export default {
name: 'PipelineNavigationTabs', name: 'NavigationTabs',
props: { props: {
tabs: { tabs: {
type: Array, type: Array,
required: true, required: true,
}, },
scope: {
type: String,
required: false,
default: '',
},
}, },
mounted() { mounted() {
$(document).trigger('init.scrolling-tabs'); $(document).trigger('init.scrolling-tabs');
...@@ -34,7 +59,7 @@ ...@@ -34,7 +59,7 @@
<a <a
role="button" role="button"
@click="onTabClick(tab)" @click="onTabClick(tab)"
:class="`js-pipelines-tab-${tab.scope}`" :class="`js-${scope}-tab-${tab.scope}`"
> >
{{ tab.name }} {{ tab.name }}
......
/**
* API callbacks for pagination and tabs
* shared between Pipelines and Environments table.
*
* Components need to have `scope`, `page` and `requestData`
*/
import {
historyPushState,
buildUrlWithCurrentLocation,
} from '../../lib/utils/common_utils';
export default {
methods: {
onChangeTab(scope) {
this.updateContent({ scope, page: '1' });
},
onChangePage(page) {
/* URLS parameters are strings, we need to parse to match types */
this.updateContent({ scope: this.scope, page: Number(page).toString() });
},
updateInternalState(parameters) {
// stop polling
this.poll.stop();
const queryString = Object.keys(parameters).map((parameter) => {
const value = parameters[parameter];
// update internal state for UI
this[parameter] = value;
return `${parameter}=${encodeURIComponent(value)}`;
}).join('&');
// update polling parameters
this.requestData = parameters;
historyPushState(buildUrlWithCurrentLocation(`?${queryString}`));
this.isLoading = true;
},
},
};
...@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -34,6 +34,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
folder_environments = project.environments.where(environment_type: params[:id]) folder_environments = project.environments.where(environment_type: params[:id])
@environments = folder_environments.with_state(params[:scope] || :available) @environments = folder_environments.with_state(params[:scope] || :available)
.order(:name) .order(:name)
@folder = params[:id]
respond_to do |format| respond_to do |format|
format.html format.html
......
...@@ -5,6 +5,8 @@ ...@@ -5,6 +5,8 @@
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag('common_vue')
= page_specific_javascript_bundle_tag("environments_folder") = page_specific_javascript_bundle_tag("environments_folder")
#environments-folder-list-view{ data: { "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, #environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"css-class" => container_class } } "css-class" => container_class } }
...@@ -3,15 +3,13 @@ ...@@ -3,15 +3,13 @@
- add_to_breadcrumbs("Pipelines", project_pipelines_path(@project)) - add_to_breadcrumbs("Pipelines", project_pipelines_path(@project))
- content_for :page_specific_javascripts do - content_for :page_specific_javascripts do
= page_specific_javascript_bundle_tag('common_vue') = page_specific_javascript_bundle_tag("common_vue")
= page_specific_javascript_bundle_tag("environments") = page_specific_javascript_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data, #environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s, "can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
"can-read-environment" => can?(current_user, :read_environment, @project).to_s, "can-read-environment" => can?(current_user, :read_environment, @project).to_s,
"can-create-environment" => can?(current_user, :create_environment, @project).to_s, "can-create-environment" => can?(current_user, :create_environment, @project).to_s,
"project-environments-path" => project_environments_path(@project),
"project-stopped-environments-path" => project_environments_path(@project, scope: :stopped),
"new-environment-path" => new_project_environment_path(@project), "new-environment-path" => new_project_environment_path(@project),
"help-page-path" => help_page_path("ci/environments"), "help-page-path" => help_page_path("ci/environments"),
"css-class" => container_class } } "css-class" => container_class } }
...@@ -14,8 +14,10 @@ feature 'Environments page', :js do ...@@ -14,8 +14,10 @@ feature 'Environments page', :js do
it 'shows "Available" and "Stopped" tab with links' do it 'shows "Available" and "Stopped" tab with links' do
visit_environments(project) visit_environments(project)
expect(page).to have_link('Available') expect(page).to have_selector('.js-environments-tab-available')
expect(page).to have_link('Stopped') expect(page).to have_content('Available')
expect(page).to have_selector('.js-environments-tab-stopped')
expect(page).to have_content('Stopped')
end end
describe 'with one available environment' do describe 'with one available environment' do
...@@ -75,8 +77,8 @@ feature 'Environments page', :js do ...@@ -75,8 +77,8 @@ feature 'Environments page', :js do
it 'does not show environments and counters are set to zero' do it 'does not show environments and counters are set to zero' do
expect(page).to have_content('You don\'t have any environments right now.') expect(page).to have_content('You don\'t have any environments right now.')
expect(page.find('.js-available-environments-count').text).to eq('0') expect(page.find('.js-environments-tab-available .badge').text).to eq('0')
expect(page.find('.js-stopped-environments-count').text).to eq('0') expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
end end
end end
...@@ -93,8 +95,8 @@ feature 'Environments page', :js do ...@@ -93,8 +95,8 @@ feature 'Environments page', :js do
it 'shows environments names and counters' do it 'shows environments names and counters' do
expect(page).to have_link(environment.name) expect(page).to have_link(environment.name)
expect(page.find('.js-available-environments-count').text).to eq('1') expect(page.find('.js-environments-tab-available .badge').text).to eq('1')
expect(page.find('.js-stopped-environments-count').text).to eq('0') expect(page.find('.js-environments-tab-stopped .badge').text).to eq('0')
end end
it 'does not show deployments' do it 'does not show deployments' do
...@@ -294,11 +296,32 @@ feature 'Environments page', :js do ...@@ -294,11 +296,32 @@ feature 'Environments page', :js do
end end
end end
describe 'environments folders view' do
before do
create(:environment, project: project,
name: 'staging.review/review-1',
state: :available)
create(:environment, project: project,
name: 'staging.review/review-2',
state: :available)
end
scenario 'user opens folder view' do
visit folder_project_environments_path(project, 'staging.review')
wait_for_requests
expect(page).to have_content('Environments / staging.review')
expect(page).to have_content('review-1')
expect(page).to have_content('review-2')
end
end
def have_terminal_button def have_terminal_button
have_link(nil, href: terminal_project_environment_path(project, environment)) have_link(nil, href: terminal_project_environment_path(project, environment))
end end
def visit_environments(project, **opts) def visit_environments(project, **opts)
visit project_environments_path(project, **opts) visit project_environments_path(project, **opts)
wait_for_requests
end end
end end
import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('environments empty state', () => {
let vm;
let Component;
beforeEach(() => {
Component = Vue.extend(emptyState);
});
afterEach(() => {
vm.$destroy();
});
describe('With permissions', () => {
beforeEach(() => {
vm = mountComponent(Component, {
newPath: 'foo',
canCreateEnvironment: true,
helpPath: 'bar',
});
});
it('renders empty state and new environment button', () => {
expect(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
).toEqual('You don\'t have any environments right now.');
expect(
vm.$el.querySelector('.js-new-environment-button').getAttribute('href'),
).toEqual('foo');
});
});
describe('Without permission', () => {
beforeEach(() => {
vm = mountComponent(Component, {
newPath: 'foo',
canCreateEnvironment: false,
helpPath: 'bar',
});
});
it('renders empty state without new button', () => {
expect(
vm.$el.querySelector('.js-blank-state-title').textContent.trim(),
).toEqual('You don\'t have any environments right now.');
expect(
vm.$el.querySelector('.js-new-environment-button'),
).toBeNull();
});
});
});
import Vue from 'vue'; import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table.vue'; import environmentTableComp from '~/environments/components/environments_table.vue';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Environment table', () => {
let Component;
let vm;
describe('Environment item', () => {
preloadFixtures('static/environments/element.html.raw');
beforeEach(() => { beforeEach(() => {
loadFixtures('static/environments/element.html.raw'); Component = Vue.extend(environmentTableComp);
});
afterEach(() => {
vm.$destroy();
}); });
it('Should render a table', () => { it('Should render a table', () => {
...@@ -17,18 +24,12 @@ describe('Environment item', () => { ...@@ -17,18 +24,12 @@ describe('Environment item', () => {
}, },
}; };
const EnvironmentTable = Vue.extend(environmentTableComp); vm = mountComponent(Component, {
environments: [mockItem],
const component = new EnvironmentTable({ canCreateDeployment: false,
el: document.querySelector('.test-dom-element'), canReadEnvironment: true,
propsData: { });
environments: [{ mockItem }],
canCreateDeployment: false,
canReadEnvironment: true,
service: {},
},
}).$mount();
expect(component.$el.getAttribute('class')).toContain('ci-table'); expect(vm.$el.getAttribute('class')).toContain('ci-table');
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import '~/flash'; import environmentsComponent from '~/environments/components/environments_app.vue';
import environmentsComponent from '~/environments/components/environment.vue';
import { environment, folder } from './mock_data'; import { environment, folder } from './mock_data';
import { headersInterceptor } from '../helpers/vue_resource_helper'; import { headersInterceptor } from '../helpers/vue_resource_helper';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Environment', () => { describe('Environment', () => {
preloadFixtures('static/environments/environments.html.raw'); const mockData = {
endpoint: 'environments.json',
canCreateEnvironment: true,
canCreateDeployment: true,
canReadEnvironment: true,
cssContainerClass: 'container',
newEnvironmentPath: 'environments/new',
helpPagePath: 'help',
};
let EnvironmentsComponent; let EnvironmentsComponent;
let component; let component;
beforeEach(() => { beforeEach(() => {
loadFixtures('static/environments/environments.html.raw');
EnvironmentsComponent = Vue.extend(environmentsComponent); EnvironmentsComponent = Vue.extend(environmentsComponent);
}); });
...@@ -37,9 +43,7 @@ describe('Environment', () => { ...@@ -37,9 +43,7 @@ describe('Environment', () => {
}); });
it('should render the empty state', (done) => { it('should render the empty state', (done) => {
component = new EnvironmentsComponent({ component = mountComponent(EnvironmentsComponent, mockData);
el: document.querySelector('#environments-list-view'),
});
setTimeout(() => { setTimeout(() => {
expect( expect(
...@@ -81,9 +85,7 @@ describe('Environment', () => { ...@@ -81,9 +85,7 @@ describe('Environment', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(environmentsResponseInterceptor);
Vue.http.interceptors.push(headersInterceptor); Vue.http.interceptors.push(headersInterceptor);
component = new EnvironmentsComponent({ component = mountComponent(EnvironmentsComponent, mockData);
el: document.querySelector('#environments-list-view'),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -95,7 +97,7 @@ describe('Environment', () => { ...@@ -95,7 +97,7 @@ describe('Environment', () => {
it('should render a table with environments', (done) => { it('should render a table with environments', (done) => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelectorAll('table')).toBeDefined(); expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect( expect(
component.$el.querySelector('.environment-name').textContent.trim(), component.$el.querySelector('.environment-name').textContent.trim(),
).toEqual(environment.name); ).toEqual(environment.name);
...@@ -104,10 +106,6 @@ describe('Environment', () => { ...@@ -104,10 +106,6 @@ describe('Environment', () => {
}); });
describe('pagination', () => { describe('pagination', () => {
afterEach(() => {
window.history.pushState({}, null, '');
});
it('should render pagination', (done) => { it('should render pagination', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
...@@ -117,46 +115,23 @@ describe('Environment', () => { ...@@ -117,46 +115,23 @@ describe('Environment', () => {
}, 0); }, 0);
}); });
it('should update url when no search params are present', (done) => { it('should make an API request when page is clicked', (done) => {
spyOn(gl.utils, 'visitUrl'); spyOn(component, 'updateContent');
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); expect(component.updateContent).toHaveBeenCalledWith({ scope: 'available', page: '2' });
done(); done();
}, 0); }, 0);
}); });
it('should update url when page is already present', (done) => { it('should make an API request when using tabs', (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(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); spyOn(component, 'updateContent');
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); component.$el.querySelector('.js-environments-tab-stopped').click();
done();
}, 0);
});
it('should update url when page and scope are already present and page is first param', (done) => { expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
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(); done();
}, 0); });
}); });
}); });
}); });
...@@ -180,9 +155,7 @@ describe('Environment', () => { ...@@ -180,9 +155,7 @@ describe('Environment', () => {
}); });
it('should render empty state', (done) => { it('should render empty state', (done) => {
component = new EnvironmentsComponent({ component = mountComponent(EnvironmentsComponent, mockData);
el: document.querySelector('#environments-list-view'),
});
setTimeout(() => { setTimeout(() => {
expect( expect(
...@@ -214,9 +187,7 @@ describe('Environment', () => { ...@@ -214,9 +187,7 @@ describe('Environment', () => {
beforeEach(() => { beforeEach(() => {
Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(environmentsResponseInterceptor);
component = new EnvironmentsComponent({ component = mountComponent(EnvironmentsComponent, mockData);
el: document.querySelector('#environments-list-view'),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -289,4 +260,59 @@ describe('Environment', () => { ...@@ -289,4 +260,59 @@ describe('Environment', () => {
}); });
}); });
}); });
describe('methods', () => {
const environmentsEmptyResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
Vue.http.interceptors.push(headersInterceptor);
component = mountComponent(EnvironmentsComponent, mockData);
spyOn(history, 'pushState').and.stub();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsEmptyResponseInterceptor,
);
Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
});
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '3' })
.then(() => {
expect(component.page).toEqual('3');
expect(component.scope).toEqual('stopped');
expect(component.requestData.scope).toEqual('stopped');
expect(component.requestData.page).toEqual('3');
done();
})
.catch(done.fail);
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
spyOn(component, 'updateContent');
component.onChangeTab('stopped');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
spyOn(component, 'updateContent');
component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
});
});
}); });
import Vue from 'vue'; import Vue from 'vue';
import '~/flash';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue'; import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import { environmentsList } from '../mock_data'; import { environmentsList } from '../mock_data';
import { headersInterceptor } from '../../helpers/vue_resource_helper'; import { headersInterceptor } from '../../helpers/vue_resource_helper';
import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments Folder View', () => { describe('Environments Folder View', () => {
preloadFixtures('static/environments/environments_folder_view.html.raw'); let Component;
let EnvironmentsFolderViewComponent; let component;
const mockData = {
endpoint: 'environments.json',
folderName: 'review',
canCreateDeployment: true,
canReadEnvironment: true,
cssContainerClass: 'container',
};
beforeEach(() => { beforeEach(() => {
loadFixtures('static/environments/environments_folder_view.html.raw'); Component = Vue.extend(environmentsFolderViewComponent);
EnvironmentsFolderViewComponent = Vue.extend(environmentsFolderViewComponent);
window.history.pushState({}, null, 'environments/folders/build');
}); });
afterEach(() => { afterEach(() => {
window.history.pushState({}, null, '/'); component.$destroy();
}); });
let component;
describe('successfull request', () => { describe('successfull request', () => {
const environmentsResponseInterceptor = (request, next) => { const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({ next(request.respondWith(JSON.stringify({
...@@ -31,10 +34,10 @@ describe('Environments Folder View', () => { ...@@ -31,10 +34,10 @@ describe('Environments Folder View', () => {
headers: { headers: {
'X-nExt-pAge': '2', 'X-nExt-pAge': '2',
'x-page': '1', 'x-page': '1',
'X-Per-Page': '1', 'X-Per-Page': '2',
'X-Prev-Page': '', 'X-Prev-Page': '',
'X-TOTAL': '37', 'X-TOTAL': '20',
'X-Total-Pages': '2', 'X-Total-Pages': '10',
}, },
})); }));
}; };
...@@ -43,9 +46,7 @@ describe('Environments Folder View', () => { ...@@ -43,9 +46,7 @@ describe('Environments Folder View', () => {
Vue.http.interceptors.push(environmentsResponseInterceptor); Vue.http.interceptors.push(environmentsResponseInterceptor);
Vue.http.interceptors.push(headersInterceptor); Vue.http.interceptors.push(headersInterceptor);
component = new EnvironmentsFolderViewComponent({ component = mountComponent(Component, mockData);
el: document.querySelector('#environments-folder-list-view'),
});
}); });
afterEach(() => { afterEach(() => {
...@@ -57,7 +58,7 @@ describe('Environments Folder View', () => { ...@@ -57,7 +58,7 @@ describe('Environments Folder View', () => {
it('should render a table with environments', (done) => { it('should render a table with environments', (done) => {
setTimeout(() => { setTimeout(() => {
expect(component.$el.querySelectorAll('table')).toBeDefined(); expect(component.$el.querySelectorAll('table')).not.toBeNull();
expect( expect(
component.$el.querySelector('.environment-name').textContent.trim(), component.$el.querySelector('.environment-name').textContent.trim(),
).toEqual(environmentsList[0].name); ).toEqual(environmentsList[0].name);
...@@ -68,11 +69,11 @@ describe('Environments Folder View', () => { ...@@ -68,11 +69,11 @@ describe('Environments Folder View', () => {
it('should render available tab with count', (done) => { it('should render available tab with count', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent, component.$el.querySelector('.js-environments-tab-available').textContent,
).toContain('Available'); ).toContain('Available');
expect( expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, component.$el.querySelector('.js-environments-tab-available .badge').textContent,
).toContain('0'); ).toContain('0');
done(); done();
}, 0); }, 0);
...@@ -81,11 +82,11 @@ describe('Environments Folder View', () => { ...@@ -81,11 +82,11 @@ describe('Environments Folder View', () => {
it('should render stopped tab with count', (done) => { it('should render stopped tab with count', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, component.$el.querySelector('.js-environments-tab-stopped').textContent,
).toContain('Stopped'); ).toContain('Stopped');
expect( expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
).toContain('1'); ).toContain('1');
done(); done();
}, 0); }, 0);
...@@ -94,8 +95,8 @@ describe('Environments Folder View', () => { ...@@ -94,8 +95,8 @@ describe('Environments Folder View', () => {
it('should render parent folder name', (done) => { it('should render parent folder name', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-folder-name').textContent, component.$el.querySelector('.js-folder-name').textContent.trim(),
).toContain('Environments / build'); ).toContain('Environments / review');
done(); done();
}, 0); }, 0);
}); });
...@@ -104,52 +105,30 @@ describe('Environments Folder View', () => { ...@@ -104,52 +105,30 @@ describe('Environments Folder View', () => {
it('should render pagination', (done) => { it('should render pagination', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelectorAll('.gl-pagination li').length, component.$el.querySelectorAll('.gl-pagination'),
).toEqual(5); ).not.toBeNull();
done(); done();
}, 0); }, 0);
}); });
it('should update url when no search params are present', (done) => { it('should make an API request when changing page', (done) => {
spyOn(gl.utils, 'visitUrl'); spyOn(component, 'updateContent');
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); component.$el.querySelector('.gl-pagination .js-last-button 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(() => { expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' });
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2');
done(); done();
}, 0); }, 0);
}); });
it('should update url when page and scope are already present', (done) => { it('should make an API request when using tabs', (done) => {
spyOn(gl.utils, 'visitUrl');
window.history.pushState({}, null, '?scope=all&page=1');
setTimeout(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); spyOn(component, 'updateContent');
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?scope=all&page=2'); component.$el.querySelector('.js-environments-tab-stopped').click();
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(() => { expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click();
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2&scope=all');
done(); done();
}, 0); });
}); });
}); });
}); });
...@@ -172,9 +151,7 @@ describe('Environments Folder View', () => { ...@@ -172,9 +151,7 @@ describe('Environments Folder View', () => {
}); });
it('should not render a table', (done) => { it('should not render a table', (done) => {
component = new EnvironmentsFolderViewComponent({ component = mountComponent(Component, mockData);
el: document.querySelector('#environments-folder-list-view'),
});
setTimeout(() => { setTimeout(() => {
expect( expect(
...@@ -187,11 +164,11 @@ describe('Environments Folder View', () => { ...@@ -187,11 +164,11 @@ describe('Environments Folder View', () => {
it('should render available tab with count 0', (done) => { it('should render available tab with count 0', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-available-environments-folder-tab').textContent, component.$el.querySelector('.js-environments-tab-available').textContent,
).toContain('Available'); ).toContain('Available');
expect( expect(
component.$el.querySelector('.js-available-environments-folder-tab .js-available-environments-count').textContent, component.$el.querySelector('.js-environments-tab-available .badge').textContent,
).toContain('0'); ).toContain('0');
done(); done();
}, 0); }, 0);
...@@ -200,14 +177,70 @@ describe('Environments Folder View', () => { ...@@ -200,14 +177,70 @@ describe('Environments Folder View', () => {
it('should render stopped tab with count 0', (done) => { it('should render stopped tab with count 0', (done) => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-stopped-environments-folder-tab').textContent, component.$el.querySelector('.js-environments-tab-stopped').textContent,
).toContain('Stopped'); ).toContain('Stopped');
expect( expect(
component.$el.querySelector('.js-stopped-environments-folder-tab .js-stopped-environments-count').textContent, component.$el.querySelector('.js-environments-tab-stopped .badge').textContent,
).toContain('0'); ).toContain('0');
done(); done();
}, 0); }, 0);
}); });
}); });
describe('methods', () => {
const environmentsEmptyResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify([]), {
status: 200,
}));
};
beforeEach(() => {
Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
Vue.http.interceptors.push(headersInterceptor);
component = mountComponent(Component, mockData);
spyOn(history, 'pushState').and.stub();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, environmentsEmptyResponseInterceptor,
);
Vue.http.interceptors = _.without(Vue.http.interceptors, headersInterceptor);
});
describe('updateContent', () => {
it('should set given parameters', (done) => {
component.updateContent({ scope: 'stopped', page: '4' })
.then(() => {
expect(component.page).toEqual('4');
expect(component.scope).toEqual('stopped');
expect(component.requestData.scope).toEqual('stopped');
expect(component.requestData.page).toEqual('4');
done();
})
.catch(done.fail);
});
});
describe('onChangeTab', () => {
it('should set page to 1', () => {
spyOn(component, 'updateContent');
component.onChangeTab('stopped');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
});
});
describe('onChangePage', () => {
it('should update page and keep scope', () => {
spyOn(component, 'updateContent');
component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
});
});
}); });
%div
#environments-list-view{ data: { environments_data: "foo/environments",
"can-create-deployment" => "true",
"can-read-environment" => "true",
"can-create-environment" => "true",
"project-environments-path" => "https://gitlab.com/foo/environments",
"project-stopped-environments-path" => "https://gitlab.com/foo/environments?scope=stopped",
"new-environment-path" => "https://gitlab.com/foo/environments/new",
"help-page-path" => "https://gitlab.com/help_page"}}
%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") } }
...@@ -142,47 +142,6 @@ describe('common_utils', () => { ...@@ -142,47 +142,6 @@ describe('common_utils', () => {
}); });
}); });
describe('setParamInURL', () => {
afterEach(() => {
window.history.pushState({}, null, '');
});
it('should return the parameter', () => {
window.history.replaceState({}, null, '');
expect(commonUtils.setParamInURL('page', 156)).toBe('?page=156');
expect(commonUtils.setParamInURL('page', '156')).toBe('?page=156');
});
it('should update the existing parameter when its a number', () => {
window.history.pushState({}, null, '?page=15');
expect(commonUtils.setParamInURL('page', 16)).toBe('?page=16');
expect(commonUtils.setParamInURL('page', '16')).toBe('?page=16');
expect(commonUtils.setParamInURL('page', true)).toBe('?page=true');
});
it('should update the existing parameter when its a string', () => {
window.history.pushState({}, null, '?scope=all');
expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished');
});
it('should update the existing parameter when more than one parameter exists', () => {
window.history.pushState({}, null, '?scope=all&page=15');
expect(commonUtils.setParamInURL('scope', 'finished')).toBe('?scope=finished&page=15');
});
it('should add a new parameter to the end of the existing ones', () => {
window.history.pushState({}, null, '?scope=all');
expect(commonUtils.setParamInURL('page', 16)).toBe('?scope=all&page=16');
expect(commonUtils.setParamInURL('page', '16')).toBe('?scope=all&page=16');
expect(commonUtils.setParamInURL('page', true)).toBe('?scope=all&page=true');
});
});
describe('historyPushState', () => { describe('historyPushState', () => {
afterEach(() => { afterEach(() => {
window.history.replaceState({}, null, null); window.history.replaceState({}, null, null);
......
...@@ -176,45 +176,49 @@ describe('Pipelines', () => { ...@@ -176,45 +176,49 @@ describe('Pipelines', () => {
}); });
}); });
describe('updateContent', () => { describe('methods', () => {
it('should set given parameters', () => { beforeEach(() => {
component = mountComponent(PipelinesComponent, { spyOn(history, 'pushState').and.stub();
store: new Store(),
});
component.updateContent({ scope: 'finished', page: '4' });
expect(component.page).toEqual('4');
expect(component.scope).toEqual('finished');
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4');
}); });
});
describe('onChangeTab', () => { describe('updateContent', () => {
it('should set page to 1', () => { it('should set given parameters', () => {
component = mountComponent(PipelinesComponent, { component = mountComponent(PipelinesComponent, {
store: new Store(), store: new Store(),
}); });
component.updateContent({ scope: 'finished', page: '4' });
spyOn(component, 'updateContent'); expect(component.page).toEqual('4');
expect(component.scope).toEqual('finished');
expect(component.requestData.scope).toEqual('finished');
expect(component.requestData.page).toEqual('4');
});
});
component.onChangeTab('running'); describe('onChangeTab', () => {
it('should set page to 1', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
spyOn(component, 'updateContent');
expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' }); component.onChangeTab('running');
});
});
describe('onChangePage', () => { expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
it('should update page and keep scope', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
}); });
});
spyOn(component, 'updateContent'); describe('onChangePage', () => {
it('should update page and keep scope', () => {
component = mountComponent(PipelinesComponent, {
store: new Store(),
});
spyOn(component, 'updateContent');
component.onChangePage(4); component.onChangePage(4);
expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' }); expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
});
}); });
}); });
}); });
import Vue from 'vue'; import Vue from 'vue';
import navigationTabs from '~/pipelines/components/navigation_tabs.vue'; import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import mountComponent from '../helpers/vue_mount_component_helper'; import mountComponent from '../../helpers/vue_mount_component_helper';
describe('navigation tabs pipeline component', () => { describe('navigation tabs component', () => {
let vm; let vm;
let Component; let Component;
let data; let data;
...@@ -29,7 +29,7 @@ describe('navigation tabs pipeline component', () => { ...@@ -29,7 +29,7 @@ describe('navigation tabs pipeline component', () => {
]; ];
Component = Vue.extend(navigationTabs); Component = Vue.extend(navigationTabs);
vm = mountComponent(Component, { tabs: data }); vm = mountComponent(Component, { tabs: data, scope: 'pipelines' });
}); });
afterEach(() => { afterEach(() => {
...@@ -52,4 +52,10 @@ describe('navigation tabs pipeline component', () => { ...@@ -52,4 +52,10 @@ describe('navigation tabs pipeline component', () => {
it('should not render badge', () => { it('should not render badge', () => {
expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null); expect(vm.$el.querySelector('.js-pipelines-tab-running .badge')).toEqual(null);
}); });
it('should trigger onTabClick', () => {
spyOn(vm, '$emit');
vm.$el.querySelector('.js-pipelines-tab-pending').click();
expect(vm.$emit).toHaveBeenCalledWith('onChangeTab', 'pending');
});
}); });
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