Commit 01b0617c authored by Filipa Lacerda's avatar Filipa Lacerda

Port of 32098-environments-reusable

Fix broken test

 Add end to end test for endpoint and folder name changes

Improve e2e test to use dot in folder name

Fix broken test
parent 73d0cd60
<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,
service: {},
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);
},
/**
* Pagination should only be rendered when we have information about it and when the
* number of total pages is bigger than 1.
*
* @return {Boolean}
*/
shouldRenderPagination() {
return this.state.paginationInformation && this.state.paginationInformation.totalPages > 1;
},
},
/**
* 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);
eventHub.$on('toggleDeployBoard', this.toggleDeployBoard);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
eventHub.$off('toggleDeployBoard');
},
methods: {
/**
* Toggles the visibility of the deploy boards of the clicked environment.
* @param {Object} model
*/
toggleDeployBoard(model) {
this.store.toggleDeployBoard(model.id);
},
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 blank-state-no-icon"
v-if="!isLoading && state.environments.length === 0">
<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
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');
}, },
}, },
}; };
......
...@@ -430,7 +430,7 @@ export default { ...@@ -430,7 +430,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>
<span <span
class="deploy-board-icon" class="deploy-board-icon"
...@@ -520,7 +520,7 @@ export default { ...@@ -520,7 +520,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"
...@@ -536,7 +536,7 @@ export default { ...@@ -536,7 +536,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>
...@@ -546,7 +546,7 @@ export default { ...@@ -546,7 +546,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);
eventHub.$on('toggleDeployBoard', this.toggleDeployBoard);
},
beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('toggleDeployBoard');
},
methods: {
/**
* Toggles the visibility of the deploy boards of the clicked environment.
* @param {Object} model
*/
toggleDeployBoard(model) {
this.store.toggleDeployBoard(model.id);
},
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,15 +2,15 @@ ...@@ -2,15 +2,15 @@
/** /**
* Render environments table. * Render environments table.
*/ */
import EnvironmentTableRowComponent from './environment_item.vue'; import environmentItem from './environment_item.vue';
import DeployBoard from './deploy_board_component.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue'; import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import deployBoard from './deploy_board_component.vue';
export default { export default {
components: { components: {
'environment-item': EnvironmentTableRowComponent, environmentItem,
DeployBoard,
loadingIcon, loadingIcon,
deployBoard,
}, },
props: { props: {
...@@ -44,19 +44,19 @@ export default { ...@@ -44,19 +44,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
...@@ -98,7 +98,7 @@ export default { ...@@ -98,7 +98,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';
document.addEventListener('DOMContentLoaded', () => { Vue.use(Translate);
// eslint-disable-next-line no-new
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';
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 Poll from '../../lib/utils/poll';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
import { convertPermissionToBoolean, getParameterByName, setParamInURL } from '../../lib/utils/common_utils';
export default { export default {
components: { props: {
environmentTable, endpoint: {
tablePagination, type: String,
loadingIcon, required: true,
}, },
folderName: {
mixins: [ type: String,
environmentsMixin, required: true,
],
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,
service: {},
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);
}, },
cssContainerClass: {
/** type: String,
* URL to link in the stopped tab. required: true,
*
* @return {String}
*/
stoppedPath() {
return `${window.location.pathname}?scope=stopped`;
}, },
canCreateDeployment: {
/** type: Boolean,
* URL to link in the available tab. required: true,
*
* @return {String}
*/
availablePath() {
return window.location.pathname;
}, },
canReadEnvironment: {
type: Boolean,
required: true,
}, },
/**
* 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); mixins: [
}, environmentsMixin,
CIPaginationMixin,
beforeDestroyed() { ],
eventHub.$off('postAction');
},
methods: { methods: {
/**
* Toggles the visibility of the deploy boards of the clicked environment.
*
* @param {Object} model
* @return {Object}
*/
toggleDeployBoard(model) {
return this.store.toggleDeployBoard(model.id);
},
/**
* 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) { successCallback(resp) {
this.saveData(resp); this.saveData(resp);
}, },
errorCallback() {
this.isLoading = false;
// eslint-disable-next-line no-new
new Flash('An error occurred while fetching the environments.');
}, },
};
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.'));
}
},
},
};
</script> </script>
<template> <template>
<div :class="cssContainerClass"> <div :class="cssContainerClass">
...@@ -183,59 +45,23 @@ export default { ...@@ -183,59 +45,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>
<div <container
class="table-holder" :is-loading="isLoading"
v-if="!isLoading && state.environments.length > 0">
<environment-table
:environments="state.environments" :environments="state.environments"
:can-create-deployment="canCreateDeploymentParsed" :pagination="state.paginationInformation"
:can-read-environment="canReadEnvironmentParsed" :can-create-deployment="canCreateDeployment"
:toggleDeployBoard="toggleDeployBoard" :can-read-environment="canReadEnvironment"
:store="store" @onChangePage="onChangePage"
:service="service"
/> />
<table-pagination
v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
:change="changePage"
:pageInfo="state.paginationInformation"/>
</div>
</div>
</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;
if (_.isEqual(parseQueryStringIntoObject(resp.url.split('?')[1]), this.requestData)) {
this.store.storeAvailableCount(response.available_count); this.store.storeAvailableCount(response.available_count);
this.store.storeStoppedCount(response.stopped_count); this.store.storeStoppedCount(response.stopped_count);
this.store.storeEnvironments(response.environments); this.store.storeEnvironments(response.environments);
this.store.setPagination(headers); 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');
}, },
}; };
...@@ -44,7 +44,12 @@ export default class EnvironmentsStore { ...@@ -44,7 +44,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,7 @@ ...@@ -170,22 +170,7 @@
* - 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 +188,6 @@ ...@@ -203,14 +188,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 +212,7 @@ ...@@ -235,6 +212,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;
},
},
};
...@@ -35,6 +35,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController ...@@ -35,6 +35,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,9 @@ ...@@ -5,6 +5,9 @@
= 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 } }
...@@ -32,9 +32,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -32,9 +32,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.text "description", null: false t.text "description", null: false
t.string "logo" t.string "logo"
t.integer "updated_by" t.integer "updated_by"
t.datetime_with_timezone "created_at", null: false
t.datetime_with_timezone "updated_at", null: false
t.string "header_logo" t.string "header_logo"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.text "description_html" t.text "description_html"
t.integer "cached_markdown_version" t.integer "cached_markdown_version"
end end
...@@ -306,8 +306,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -306,8 +306,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "ci_build_trace_sections", force: :cascade do |t| create_table "ci_build_trace_sections", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.datetime_with_timezone "date_start", null: false t.datetime "date_start", null: false
t.datetime_with_timezone "date_end", null: false t.datetime "date_end", null: false
t.integer "byte_start", limit: 8, null: false t.integer "byte_start", limit: 8, null: false
t.integer "byte_end", limit: 8, null: false t.integer "byte_end", limit: 8, null: false
t.integer "build_id", null: false t.integer "build_id", null: false
...@@ -386,8 +386,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -386,8 +386,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.string "encrypted_value_iv" t.string "encrypted_value_iv"
t.integer "group_id", null: false t.integer "group_id", null: false
t.boolean "protected", default: false, null: false t.boolean "protected", default: false, null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree add_index "ci_group_variables", ["group_id", "key"], name: "index_ci_group_variables_on_group_id_and_key", unique: true, using: :btree
...@@ -399,8 +399,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -399,8 +399,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.string "encrypted_value_salt" t.string "encrypted_value_salt"
t.string "encrypted_value_iv" t.string "encrypted_value_iv"
t.integer "pipeline_schedule_id", null: false t.integer "pipeline_schedule_id", null: false
t.datetime_with_timezone "created_at" t.datetime "created_at"
t.datetime_with_timezone "updated_at" t.datetime "updated_at"
end end
add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree add_index "ci_pipeline_schedule_variables", ["pipeline_schedule_id", "key"], name: "index_ci_pipeline_schedule_variables_on_schedule_id_and_key", unique: true, using: :btree
...@@ -452,8 +452,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -452,8 +452,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "auto_canceled_by_id" t.integer "auto_canceled_by_id"
t.integer "pipeline_schedule_id" t.integer "pipeline_schedule_id"
t.integer "source" t.integer "source"
t.boolean "protected"
t.integer "config_source" t.integer "config_source"
t.boolean "protected"
t.integer "failure_reason" t.integer "failure_reason"
end end
...@@ -564,8 +564,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -564,8 +564,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "cluster_platforms_kubernetes", force: :cascade do |t| create_table "cluster_platforms_kubernetes", force: :cascade do |t|
t.integer "cluster_id", null: false t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.text "api_url" t.text "api_url"
t.text "ca_cert" t.text "ca_cert"
t.string "namespace" t.string "namespace"
...@@ -581,8 +581,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -581,8 +581,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "cluster_projects", force: :cascade do |t| create_table "cluster_projects", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.integer "cluster_id", null: false t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree add_index "cluster_projects", ["cluster_id"], name: "index_cluster_projects_on_cluster_id", using: :btree
...@@ -592,8 +592,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -592,8 +592,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "cluster_id", null: false t.integer "cluster_id", null: false
t.integer "status" t.integer "status"
t.integer "num_nodes", null: false t.integer "num_nodes", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.text "status_reason" t.text "status_reason"
t.string "gcp_project_id", null: false t.string "gcp_project_id", null: false
t.string "zone", null: false t.string "zone", null: false
...@@ -610,8 +610,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -610,8 +610,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "user_id" t.integer "user_id"
t.integer "provider_type" t.integer "provider_type"
t.integer "platform_type" t.integer "platform_type"
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "enabled", default: true t.boolean "enabled", default: true
t.string "name", null: false t.string "name", null: false
end end
...@@ -621,8 +621,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -621,8 +621,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "clusters_applications_helm", force: :cascade do |t| create_table "clusters_applications_helm", force: :cascade do |t|
t.integer "cluster_id", null: false t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "status", null: false t.integer "status", null: false
t.string "version", null: false t.string "version", null: false
t.text "status_reason" t.text "status_reason"
...@@ -630,8 +630,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -630,8 +630,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "clusters_applications_ingress", force: :cascade do |t| create_table "clusters_applications_ingress", force: :cascade do |t|
t.integer "cluster_id", null: false t.integer "cluster_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "status", null: false t.integer "status", null: false
t.integer "ingress_type", null: false t.integer "ingress_type", null: false
t.string "version", null: false t.string "version", null: false
...@@ -719,8 +719,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -719,8 +719,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.datetime "created_at" t.datetime "created_at"
t.datetime "updated_at" t.datetime "updated_at"
t.string "confirmation_token" t.string "confirmation_token"
t.datetime_with_timezone "confirmed_at" t.datetime "confirmed_at"
t.datetime_with_timezone "confirmation_sent_at" t.datetime "confirmation_sent_at"
end end
add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree add_index "emails", ["confirmation_token"], name: "index_emails_on_confirmation_token", unique: true, using: :btree
...@@ -751,8 +751,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -751,8 +751,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "epic_metrics", force: :cascade do |t| create_table "epic_metrics", force: :cascade do |t|
t.integer "epic_id", null: false t.integer "epic_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "epic_metrics", ["epic_id"], name: "index_epic_metrics", using: :btree add_index "epic_metrics", ["epic_id"], name: "index_epic_metrics", using: :btree
...@@ -769,9 +769,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -769,9 +769,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "lock_version" t.integer "lock_version"
t.date "start_date" t.date "start_date"
t.date "end_date" t.date "end_date"
t.datetime_with_timezone "last_edited_at" t.datetime "last_edited_at"
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.string "title", null: false t.string "title", null: false
t.string "title_html", null: false t.string "title_html", null: false
t.text "description" t.text "description"
...@@ -788,8 +788,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -788,8 +788,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "project_id" t.integer "project_id"
t.integer "author_id", null: false t.integer "author_id", null: false
t.integer "target_id" t.integer "target_id"
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "action", limit: 2, null: false t.integer "action", limit: 2, null: false
t.string "target_type" t.string "target_type"
end end
...@@ -848,8 +848,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -848,8 +848,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "service_id" t.integer "service_id"
t.integer "status" t.integer "status"
t.integer "gcp_cluster_size", null: false t.integer "gcp_cluster_size", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "enabled", default: true t.boolean "enabled", default: true
t.text "status_reason" t.text "status_reason"
t.string "project_namespace" t.string "project_namespace"
...@@ -913,8 +913,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -913,8 +913,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "geo_node_namespace_links", force: :cascade do |t| create_table "geo_node_namespace_links", force: :cascade do |t|
t.integer "geo_node_id", null: false t.integer "geo_node_id", null: false
t.integer "namespace_id", null: false t.integer "namespace_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "geo_node_namespace_links", ["geo_node_id", "namespace_id"], name: "index_geo_node_namespace_links_on_geo_node_id_and_namespace_id", unique: true, using: :btree add_index "geo_node_namespace_links", ["geo_node_id", "namespace_id"], name: "index_geo_node_namespace_links_on_geo_node_id_and_namespace_id", unique: true, using: :btree
...@@ -933,12 +933,12 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -933,12 +933,12 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "attachments_synced_count" t.integer "attachments_synced_count"
t.integer "attachments_failed_count" t.integer "attachments_failed_count"
t.integer "last_event_id" t.integer "last_event_id"
t.datetime_with_timezone "last_event_date" t.datetime "last_event_date"
t.integer "cursor_last_event_id" t.integer "cursor_last_event_id"
t.datetime_with_timezone "cursor_last_event_date" t.datetime "cursor_last_event_date"
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.datetime_with_timezone "last_successful_status_check_at" t.datetime "last_successful_status_check_at"
t.string "status_message" t.string "status_message"
end end
...@@ -1032,8 +1032,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1032,8 +1032,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree add_index "gpg_key_subkeys", ["keyid"], name: "index_gpg_key_subkeys_on_keyid", unique: true, using: :btree
create_table "gpg_keys", force: :cascade do |t| create_table "gpg_keys", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "user_id" t.integer "user_id"
t.binary "primary_keyid" t.binary "primary_keyid"
t.binary "fingerprint" t.binary "fingerprint"
...@@ -1045,8 +1045,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1045,8 +1045,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree add_index "gpg_keys", ["user_id"], name: "index_gpg_keys_on_user_id", using: :btree
create_table "gpg_signatures", force: :cascade do |t| create_table "gpg_signatures", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "project_id" t.integer "project_id"
t.integer "gpg_key_id" t.integer "gpg_key_id"
t.binary "commit_sha" t.binary "commit_sha"
...@@ -1064,8 +1064,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1064,8 +1064,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree add_index "gpg_signatures", ["project_id"], name: "index_gpg_signatures_on_project_id", using: :btree
create_table "group_custom_attributes", force: :cascade do |t| create_table "group_custom_attributes", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "group_id", null: false t.integer "group_id", null: false
t.string "key", null: false t.string "key", null: false
t.string "value", null: false t.string "value", null: false
...@@ -1160,7 +1160,7 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1160,7 +1160,7 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.datetime "last_edited_at" t.datetime "last_edited_at"
t.integer "last_edited_by_id" t.integer "last_edited_by_id"
t.boolean "discussion_locked" t.boolean "discussion_locked"
t.datetime_with_timezone "closed_at" t.datetime "closed_at"
end end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
...@@ -1311,8 +1311,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1311,8 +1311,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree add_index "members", ["user_id"], name: "index_members_on_user_id", using: :btree
create_table "merge_request_diff_commits", id: false, force: :cascade do |t| create_table "merge_request_diff_commits", id: false, force: :cascade do |t|
t.datetime_with_timezone "authored_date" t.datetime "authored_date"
t.datetime_with_timezone "committed_date" t.datetime "committed_date"
t.integer "merge_request_diff_id", null: false t.integer "merge_request_diff_id", null: false
t.integer "relative_order", null: false t.integer "relative_order", null: false
t.binary "sha", null: false t.binary "sha", null: false
...@@ -1665,8 +1665,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1665,8 +1665,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree add_index "personal_access_tokens", ["user_id"], name: "index_personal_access_tokens_on_user_id", using: :btree
create_table "plans", force: :cascade do |t| create_table "plans", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.string "name" t.string "name"
t.string "title" t.string "title"
t.integer "active_pipelines_limit" t.integer "active_pipelines_limit"
...@@ -1686,8 +1686,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1686,8 +1686,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
create_table "project_auto_devops", force: :cascade do |t| create_table "project_auto_devops", force: :cascade do |t|
t.integer "project_id", null: false t.integer "project_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.boolean "enabled" t.boolean "enabled"
t.string "domain" t.string "domain"
end end
...@@ -1695,8 +1695,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1695,8 +1695,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
create_table "project_custom_attributes", force: :cascade do |t| create_table "project_custom_attributes", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "project_id", null: false t.integer "project_id", null: false
t.string "key", null: false t.string "key", null: false
t.string "value", null: false t.string "value", null: false
...@@ -1746,9 +1746,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1746,9 +1746,9 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.integer "retry_count", default: 0, null: false t.integer "retry_count", default: 0, null: false
t.datetime "last_update_started_at" t.datetime "last_update_started_at"
t.datetime "last_update_scheduled_at" t.datetime "last_update_scheduled_at"
t.datetime_with_timezone "next_execution_timestamp" t.datetime "next_execution_timestamp"
t.datetime_with_timezone "created_at" t.datetime "created_at"
t.datetime_with_timezone "updated_at" t.datetime "updated_at"
end end
add_index "project_mirror_data", ["next_execution_timestamp", "retry_count"], name: "index_mirror_data_on_next_execution_and_retry_count", using: :btree add_index "project_mirror_data", ["next_execution_timestamp", "retry_count"], name: "index_mirror_data_on_next_execution_and_retry_count", using: :btree
...@@ -1785,12 +1785,12 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -1785,12 +1785,12 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.string "import_status" t.string "import_status"
t.text "merge_requests_template" t.text "merge_requests_template"
t.integer "star_count", default: 0, null: false t.integer "star_count", default: 0, null: false
t.boolean "merge_requests_rebase_enabled", default: false, null: false t.boolean "merge_requests_rebase_enabled", default: false
t.string "import_type" t.string "import_type"
t.string "import_source" t.string "import_source"
t.integer "approvals_before_merge", default: 0, null: false t.integer "approvals_before_merge", default: 0, null: false
t.boolean "reset_approvals_on_push", default: true t.boolean "reset_approvals_on_push", default: true
t.boolean "merge_requests_ff_only_enabled", default: false, null: false t.boolean "merge_requests_ff_only_enabled", default: false
t.text "issues_template" t.text "issues_template"
t.boolean "mirror", default: false, null: false t.boolean "mirror", default: false, null: false
t.datetime "mirror_last_update_at" t.datetime "mirror_last_update_at"
...@@ -2045,8 +2045,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -2045,8 +2045,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.string "team_name", null: false t.string "team_name", null: false
t.string "alias", null: false t.string "alias", null: false
t.string "user_id", null: false t.string "user_id", null: false
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
end end
add_index "slack_integrations", ["service_id"], name: "index_slack_integrations_on_service_id", using: :btree add_index "slack_integrations", ["service_id"], name: "index_slack_integrations_on_service_id", using: :btree
...@@ -2139,7 +2139,7 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -2139,7 +2139,7 @@ ActiveRecord::Schema.define(version: 20171120145444) do
t.datetime "updated_at", null: false t.datetime "updated_at", null: false
t.integer "issue_id" t.integer "issue_id"
t.integer "merge_request_id" t.integer "merge_request_id"
t.datetime_with_timezone "spent_at" t.datetime "spent_at"
end end
add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree add_index "timelogs", ["issue_id"], name: "index_timelogs_on_issue_id", using: :btree
...@@ -2214,8 +2214,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do ...@@ -2214,8 +2214,8 @@ ActiveRecord::Schema.define(version: 20171120145444) do
add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree add_index "user_agent_details", ["subject_id", "subject_type"], name: "index_user_agent_details_on_subject_id_and_subject_type", using: :btree
create_table "user_custom_attributes", force: :cascade do |t| create_table "user_custom_attributes", force: :cascade do |t|
t.datetime_with_timezone "created_at", null: false t.datetime "created_at", null: false
t.datetime_with_timezone "updated_at", null: false t.datetime "updated_at", null: false
t.integer "user_id", null: false t.integer "user_id", null: false
t.string "key", null: false t.string "key", null: false
t.string "value", null: false t.string "value", null: false
......
...@@ -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();
});
});
});
...@@ -2,12 +2,18 @@ import Vue from 'vue'; ...@@ -2,12 +2,18 @@ import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table.vue'; import environmentTableComp from '~/environments/components/environments_table.vue';
import eventHub from '~/environments/event_hub'; import eventHub from '~/environments/event_hub';
import { deployBoardMockData } from './mock_data'; import { deployBoardMockData } from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
describe('Environment item', () => { describe('Environment table', () => {
let EnvironmentTable; let Component;
let vm;
beforeEach(() => { beforeEach(() => {
EnvironmentTable = Vue.extend(environmentTableComp); Component = Vue.extend(environmentTableComp);
});
afterEach(() => {
vm.$destroy();
}); });
it('Should render a table', () => { it('Should render a table', () => {
...@@ -19,16 +25,13 @@ describe('Environment item', () => { ...@@ -19,16 +25,13 @@ describe('Environment item', () => {
environment_path: 'url', environment_path: 'url',
}; };
const component = new EnvironmentTable({ vm = mountComponent(Component, {
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: false, canCreateDeployment: false,
canReadEnvironment: true, canReadEnvironment: true,
}, });
}).$mount();
expect(component.$el.getAttribute('class')).toContain('ci-table'); expect(vm.$el.getAttribute('class')).toContain('ci-table');
}); });
it('should render deploy board container when data is provided', () => { it('should render deploy board container when data is provided', () => {
...@@ -44,18 +47,15 @@ describe('Environment item', () => { ...@@ -44,18 +47,15 @@ describe('Environment item', () => {
isEmptyDeployBoard: false, isEmptyDeployBoard: false,
}; };
const component = new EnvironmentTable({ vm = mountComponent(Component, {
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: true, canCreateDeployment: false,
canReadEnvironment: true, canReadEnvironment: true,
}, });
}).$mount();
expect(component.$el.querySelector('.js-deploy-board-row')).toBeDefined(); expect(vm.$el.querySelector('.js-deploy-board-row')).toBeDefined();
expect( expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'), vm.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true); ).toEqual(true);
}); });
...@@ -82,15 +82,12 @@ describe('Environment item', () => { ...@@ -82,15 +82,12 @@ describe('Environment item', () => {
expect(env.id).toEqual(mockItem.id); expect(env.id).toEqual(mockItem.id);
}); });
const component = new EnvironmentTable({ vm = mountComponent(Component, {
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem], environments: [mockItem],
canCreateDeployment: true, canCreateDeployment: false,
canReadEnvironment: true, canReadEnvironment: true,
}, });
}).$mount();
component.$el.querySelector('.deploy-board-icon').click(); vm.$el.querySelector('.deploy-board-icon').click();
}); });
}); });
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,44 +115,20 @@ describe('Environment', () => { ...@@ -117,44 +115,20 @@ 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(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); spyOn(component, 'updateContent');
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); component.$el.querySelector('.js-environments-tab-stopped').click();
done(); expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
}, 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(); done();
}, 0); }, 0);
}); });
...@@ -191,9 +165,7 @@ describe('Environment', () => { ...@@ -191,9 +165,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(
...@@ -225,9 +197,7 @@ describe('Environment', () => { ...@@ -225,9 +197,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(() => {
...@@ -300,4 +270,59 @@ describe('Environment', () => { ...@@ -300,4 +270,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);
...@@ -95,7 +96,7 @@ describe('Environments Folder View', () => { ...@@ -95,7 +96,7 @@ describe('Environments Folder View', () => {
setTimeout(() => { setTimeout(() => {
expect( expect(
component.$el.querySelector('.js-folder-name').textContent, component.$el.querySelector('.js-folder-name').textContent,
).toContain('Environments / build'); ).toContain('Environments / review');
done(); done();
}, 0); }, 0);
}); });
...@@ -104,50 +105,26 @@ describe('Environments Folder View', () => { ...@@ -104,50 +105,26 @@ 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'); expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '10' });
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(() => { setTimeout(() => {
component.$el.querySelector('.gl-pagination li:nth-child(5) a').click(); spyOn(component, 'updateContent');
expect(gl.utils.visitUrl).toHaveBeenCalledWith('?page=2'); component.$el.querySelector('.js-environments-tab-stopped').click();
done(); expect(component.updateContent).toHaveBeenCalledWith({ scope: 'stopped', page: '1' });
}, 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(); done();
}, 0); }, 0);
}); });
...@@ -183,9 +160,7 @@ describe('Environments Folder View', () => { ...@@ -183,9 +160,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(
...@@ -198,11 +173,11 @@ describe('Environments Folder View', () => { ...@@ -198,11 +173,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);
...@@ -211,14 +186,70 @@ describe('Environments Folder View', () => { ...@@ -211,14 +186,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,6 +176,11 @@ describe('Pipelines', () => { ...@@ -176,6 +176,11 @@ describe('Pipelines', () => {
}); });
}); });
describe('methods', () => {
beforeEach(() => {
spyOn(history, 'pushState').and.stub();
});
describe('updateContent', () => { describe('updateContent', () => {
it('should set given parameters', () => { it('should set given parameters', () => {
component = mountComponent(PipelinesComponent, { component = mountComponent(PipelinesComponent, {
...@@ -217,4 +222,5 @@ describe('Pipelines', () => { ...@@ -217,4 +222,5 @@ describe('Pipelines', () => {
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