Commit 62b45631 authored by Jacob Schatz's avatar Jacob Schatz

Merge branch '1589-deploy-boards' into 'master'

Deploy boards

Closes #1589

See merge request !1233
parents f7f034ad 251d6eb9
/* eslint-disable no-new, import/first */
/**
* Renders a deploy board.
*
* A deploy board is composed by:
* - Information area with percentage of completion.
* - Instances with status.
* - Button Actions.
* [Mockup](https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png)
*
* The data of each deploy board needs to be fetched when we render the component.
*
* The endpoint response can sometimes be 204, in those cases we need to retry the request.
* This should be done using backoff pooling and we should make no more than 3 request
* for each deploy board.
* After the third request we need to show a message saying we can't fetch the data.
* Please refer to this [comment](https://gitlab.com/gitlab-org/gitlab-ee/issues/1589#note_23630610)
* for more information
*/
const instanceComponent = require('./deploy_board_instance_component.js.es6');
const statusCodes = require('~/lib/utils/http_status');
const Flash = require('~/flash');
require('~/lib/utils/common_utils.js.es6');
module.exports = {
components: {
instanceComponent,
},
props: {
store: {
type: Object,
required: true,
},
service: {
type: Object,
required: true,
},
deployBoardData: {
type: Object,
required: true,
},
environmentID: {
type: Number,
required: true,
},
},
data() {
return {
isLoading: false,
hasError: false,
backOffRequestCounter: 0,
};
},
created() {
this.isLoading = true;
const maxNumberOfRequests = 3;
// If the response is 204, we make 3 more requests.
gl.utils.backOff((next, stop) => {
this.service.getDeployBoard(this.environmentID)
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.backOffRequestCounter = this.backOffRequestCounter += 1;
if (this.backOffRequestCounter < maxNumberOfRequests) {
next();
} else {
stop(resp);
}
} else {
stop(resp);
}
})
.catch(stop);
})
.then((resp) => {
if (resp.status === statusCodes.NO_CONTENT) {
this.hasError = true;
return resp;
}
return resp.json();
})
.then((response) => {
this.store.storeDeployBoard(this.environmentID, response);
return response;
})
.then(() => {
this.isLoading = false;
})
.catch(() => {
this.isLoading = false;
new Flash('An error occurred while fetching the deploy board.', 'alert');
});
},
computed: {
canRenderDeployBoard() {
return !this.isLoading && !this.hasError && Object.keys(this.deployBoardData).length;
},
instanceTitle() {
let title;
if (this.deployBoardData.instances.length === 1) {
title = 'Instance';
} else {
title = 'Instances';
}
return title;
},
},
template: `
<div class="js-deploy-board deploy-board">
<div v-if="isLoading">
<i class="fa fa-spinner fa-spin" aria-hidden="true"></i>
</div>
<div v-if="canRenderDeployBoard">
<section class="deploy-board-information">
<span>
<span class="percentage">{{deployBoardData.completion}}%</span>
<span class="text">Complete</span>
</span>
</section>
<section class="deploy-board-instances">
<p class="text">{{instanceTitle}}</p>
<div class="deploy-board-instances-container">
<template v-for="instance in deployBoardData.instances">
<instance-component
:status="instance.status"
:tooltipText="instance.tooltip">
</instance-component>
</template>
</div>
</section>
<section class="deploy-board-actions">
<a class="btn"
data-method="post"
rel="nofollow"
v-if="deployBoardData.rollback_url"
:href="deployBoardData.rollback_url">
Rollback
</a>
<a class="btn btn-red btn-inverted"
data-method="post"
rel="nofollow"
v-if="deployBoardData.abort_url"
:href="deployBoardData.abort_url">
Abort
</a>
</section>
</div>
<div v-if="!isLoading && hasError" class="deploy-board-error-message">
We can't fetch the data right now. Please try again later.
</div>
</div>
`,
};
/**
* An instance in deploy board is represented by a square in this mockup:
* https://gitlab.com/gitlab-org/gitlab-ce/uploads/2f655655c0eadf655d0ae7467b53002a/environments__deploy-graphic.png
*
* Each instance has a state and a tooltip.
* The state needs to be represented in different colors,
* see more information about this in https://gitlab.com/gitlab-org/gitlab-ee/uploads/5fff049fd88336d9ee0c6ef77b1ba7e3/monitoring__deployboard--key.png
*
*/
module.exports = {
props: {
/**
* Represents the status of the pod. Each state is represented with a different
* color.
* It should be one of the following:
* finished || deploying || failed || ready || preparing || waiting
*/
status: {
type: String,
required: true,
default: 'finished',
},
tooltipText: {
type: String,
required: false,
default: '',
},
},
computed: {
cssClass() {
return `deploy-board-instance-${this.status}`;
},
},
template: `
<div
class="deploy-board-instance has-tooltip"
:class="cssClass"
:data-title="tooltipText"
data-toggle="tooltip"
data-placement="top">
</div>
`,
};
......@@ -3,12 +3,12 @@
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentsService = require('~/environments/services/environments_service');
const EnvironmentTable = require('./environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
const EnvironmentsStore = require('~/environments/stores/environments_store');
require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-component', {
......@@ -23,6 +23,7 @@ module.exports = Vue.component('environment-component', {
return {
store,
service: {},
state: store.state,
visibility: 'available',
isLoading: false,
......@@ -62,6 +63,16 @@ module.exports = Vue.component('environment-component', {
return gl.utils.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;
},
},
/**
......@@ -74,11 +85,11 @@ module.exports = Vue.component('environment-component', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
......@@ -99,8 +110,15 @@ module.exports = Vue.component('environment-component', {
},
methods: {
toggleRow(model) {
return this.store.toggleFolder(model.name);
/**
* Toggles the visibility of the deploy boards of the clicked environment.
*
* @param {Object} model
* @return {Object}
*/
toggleDeployBoard(model) {
return this.store.toggleDeployBoard(model.id);
},
/**
......@@ -179,10 +197,13 @@ module.exports = Vue.component('environment-component', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"
:store="store"
:service="service">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
<table-pagination v-if="shouldRenderPagination"
:change="changePage"
:pageInfo="state.paginationInformation">
</table-pagination>
......
/**
* Environment Item Component
*
* Renders a table row for each environment.
*/
const Vue = require('vue');
const Timeago = require('timeago.js');
......@@ -9,12 +15,6 @@ const StopComponent = require('./environment_stop');
const RollbackComponent = require('./environment_rollback');
const TerminalButtonComponent = require('./environment_terminal_button');
/**
* Envrionment Item Component
*
* Renders a table row for each environment.
*/
const timeagoInstance = new Timeago();
module.exports = Vue.component('environment-item', {
......@@ -61,11 +61,16 @@ module.exports = Vue.component('environment-item', {
type: String,
required: false,
},
toggleDeployBoard: {
type: Function,
required: false,
},
},
computed: {
/**
* Verifies if `last_deployment` key exists in the current Envrionment.
* Verifies if `last_deployment` key exists in the current Environment.
* This key is required to render most of the html - this method works has
* an helper.
*
......@@ -414,7 +419,6 @@ module.exports = Vue.component('environment-item', {
folderUrl() {
return `${window.location.pathname}/folders/${this.model.folderName}`;
},
},
/**
......@@ -435,11 +439,27 @@ module.exports = Vue.component('environment-item', {
template: `
<tr>
<td>
<span class="deploy-board-icon"
v-if="model.hasDeployBoard"
@click="toggleDeployBoard(model)">
<i v-show="!model.isDeployBoardVisible"
class="fa fa-caret-right"
aria-hidden="true">
</i>
<i v-show="model.isDeployBoardVisible"
class="fa fa-caret-down"
aria-hidden="true">
</i>
</span>
<a v-if="!model.isFolder"
class="environment-name"
:href="environmentPath">
{{model.name}}
</a>
<a v-else class="folder-name" :href="folderUrl">
<span class="folder-icon">
<i class="fa fa-folder" aria-hidden="true"></i>
......
/**
* Render environments table.
*
* Dumb component used to render top level environments and
* the folder view.
*/
const Vue = require('vue');
const EnvironmentItem = require('./environment_item');
const DeployBoard = require('./deploy_board_component');
module.exports = Vue.component('environment-table-component', {
components: {
'environment-item': EnvironmentItem,
EnvironmentItem,
DeployBoard,
},
props: {
......@@ -43,6 +48,24 @@ module.exports = Vue.component('environment-table-component', {
type: String,
required: false,
},
toggleDeployBoard: {
type: Function,
required: false,
default: () => {},
},
store: {
type: Object,
required: false,
default: () => ({}),
},
service: {
type: Object,
required: false,
default: () => ({}),
},
},
template: `
......@@ -60,13 +83,26 @@ module.exports = Vue.component('environment-table-component', {
<tbody>
<template v-for="model in environments"
v-bind:model="model">
<tr is="environment-item"
:model="model"
:can-create-deployment="canCreateDeployment"
:can-read-environment="canReadEnvironment"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg"></tr>
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"></tr>
<tr v-if="model.hasDeployBoard && model.isDeployBoardVisible" class="js-deploy-board-row">
<td colspan="6" class="deploy-board-container">
<deploy-board
:store="store"
:service="service"
:environmentID="model.id"
:deployBoardData="model.deployBoardData">
</deploy-board>
</td>
</tr>
</template>
</tbody>
</table>
......
/* eslint-disable no-param-reassign, no-new */
/* global Flash */
/* eslint-disable no-new */
const Vue = window.Vue = require('vue');
window.Vue.use(require('vue-resource'));
const EnvironmentsService = require('../services/environments_service');
const EnvironmentTable = require('../components/environments_table');
const EnvironmentsStore = require('../stores/environments_store');
require('../../vue_shared/components/table_pagination');
require('../../lib/utils/common_utils');
require('../../vue_shared/vue_resource_interceptor');
const EnvironmentsService = require('~/environments//services/environments_service');
const EnvironmentTable = require('~/environments/components/environments_table');
const EnvironmentsStore = require('~/environments//stores/environments_store');
const Flash = require('~/flash');
require('~/vue_shared/components/table_pagination');
require('~/lib/utils/common_utils');
require('~/vue_shared/vue_resource_interceptor');
module.exports = Vue.component('environment-folder-view', {
......@@ -26,6 +26,7 @@ module.exports = Vue.component('environment-folder-view', {
return {
store,
service: {},
folderName,
endpoint,
state: store.state,
......@@ -88,11 +89,11 @@ module.exports = Vue.component('environment-folder-view', {
const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
const service = new EnvironmentsService(endpoint);
this.service = new EnvironmentsService(endpoint);
this.isLoading = true;
return service.all()
return this.service.get()
.then(resp => ({
headers: resp.headers,
body: resp.json(),
......@@ -113,6 +114,17 @@ module.exports = Vue.component('environment-folder-view', {
},
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.
*
......@@ -168,7 +180,10 @@ module.exports = Vue.component('environment-folder-view', {
:can-read-environment="canReadEnvironmentParsed"
:play-icon-svg="playIconSvg"
:terminal-icon-svg="terminalIconSvg"
:commit-icon-svg="commitIconSvg">
:commit-icon-svg="commitIconSvg"
:toggleDeployBoard="toggleDeployBoard"
:store="store"
:service="service">
</environment-table>
<table-pagination v-if="state.paginationInformation && state.paginationInformation.totalPages > 1"
......
......@@ -3,11 +3,17 @@ const Vue = require('vue');
class EnvironmentsService {
constructor(endpoint) {
this.environments = Vue.resource(endpoint);
this.deployBoard = Vue.resource('environments/{id}/status.json');
}
all() {
get() {
return this.environments.get();
}
getDeployBoard(environmentID) {
return this.deployBoard.get({ id: environmentID });
}
}
module.exports = EnvironmentsService;
......@@ -30,6 +30,14 @@ class EnvironmentsStore {
* If the `size` is bigger than 1, it means it should be rendered as a folder.
* In those cases we add `isFolder` key in order to render it properly.
*
* Top level environments - when the size is 1 - with `rollout_status_path`
* can render a deploy board. We add `isDeployBoardVisible` and `deployBoardData`
* keys to those environments.
* The first key will let's us know if we should or not render the deploy board.
* It will be toggled when the user clicks to seee the deploy board.
*
* The second key will allow us to update the environment with the received deploy board data.
*
* @param {Array} environments
* @returns {Array}
*/
......@@ -37,15 +45,21 @@ class EnvironmentsStore {
const filteredEnvironments = environments.map((env) => {
let filtered = {};
if (env.size > 1) {
filtered = Object.assign({}, env, { isFolder: true, folderName: env.name });
}
if (env.latest) {
filtered = Object.assign(filtered, env, env.latest);
filtered = Object.assign({}, env, env.latest);
delete filtered.latest;
} else {
filtered = Object.assign(filtered, env);
filtered = Object.assign({}, env);
}
if (filtered.size > 1) {
filtered = Object.assign(filtered, env, { isFolder: true, folderName: env.name });
} else if (filtered.size === 1 && filtered.rollout_status_path) {
filtered = Object.assign(filtered, env, {
hasDeployBoard: true,
isDeployBoardVisible: false,
deployBoardData: {},
});
}
return filtered;
......@@ -56,6 +70,20 @@ class EnvironmentsStore {
return filteredEnvironments;
}
/**
* Stores the pagination information needed to render the pagination for the
* table.
*
* Normalizes the headers to uppercase since they can be provided either
* in uppercase or lowercase.
*
* Parses to an integer the normalized ones needed for the pagination component.
*
* Stores the normalized and parsed information.
*
* @param {Object} pagination = {}
* @return {Object}
*/
setPagination(pagination = {}) {
const normalizedHeaders = gl.utils.normalizeHeaders(pagination);
const paginationInformation = gl.utils.parseIntPagination(normalizedHeaders);
......@@ -85,6 +113,48 @@ class EnvironmentsStore {
this.state.stoppedCounter = count;
return count;
}
/**
* Toggles deploy board visibility for the provided environment ID.
*
* @param {Object} environment
* @return {Array}
*/
toggleDeployBoard(environmentID) {
const environments = this.state.environments.slice();
this.state.environments = environments.map((env) => {
let updated = Object.assign({}, env);
if (env.id === environmentID) {
updated = Object.assign({}, updated, { isDeployBoardVisible: !env.isDeployBoardVisible });
}
return updated;
});
return this.state.environments;
}
/**
* Store deploy board data for given environment.
*
* @param {Number} environmentID
* @param {Object} deployBoard
* @return {Array}
*/
storeDeployBoard(environmentID, deployBoard) {
const environments = Object.assign([], this.state.environments);
this.state.environments = environments.map((env) => {
let updated = Object.assign({}, env);
if (env.id === environmentID) {
updated = Object.assign({}, updated, { deployBoardData: deployBoard });
}
return updated;
});
return this.state.environments;
}
}
module.exports = EnvironmentsStore;
......@@ -296,5 +296,57 @@
* @returns {Boolean}
*/
w.gl.utils.convertPermissionToBoolean = permission => permission === 'true';
/**
* Back Off exponential algorithm
* backOff :: (Function<next, stop>, Number) -> Promise<Any, Error>
*
* @param {Function<next, stop>} fn function to be called
* @param {Number} timeout
* @return {Promise<Any, Error>}
* @example
* ```
* backOff(function (next, stop) {
* // Let's perform this function repeatedly for 60s or for the timeout provided.
*
* ourFunction()
* .then(function (result) {
* // continue if result is not what we need
* next();
*
* // when result is what we need let's stop with the repetions and jump out of the cycle
* stop(result);
* })
* .catch(function (error) {
* // if there is an error, we need to stop this with an error.
* stop(error);
* })
* }, 60000)
* .then(function (result) {})
* .catch(function (error) {
* // deal with errors passed to stop()
* })
* ```
*/
w.gl.utils.backOff = (fn, timeout = 60000) => {
let nextInterval = 2000;
const startTime = (+new Date());
return new Promise((resolve, reject) => {
const stop = arg => ((arg instanceof Error) ? reject(arg) : resolve(arg));
const next = () => {
if (new Date().getTime() - startTime < timeout) {
setTimeout(fn.bind(null, next, stop), nextInterval);
nextInterval *= 2;
} else {
reject(new Error('BACKOFF_TIMEOUT'));
}
};
fn(next, stop);
});
};
})(window);
}).call(window);
/**
* exports HTTP status codes
*/
const statusCodes = {
NO_CONTENT: 204,
OK: 200,
};
export default statusCodes;
......@@ -160,3 +160,117 @@
}
}
}
/**
* Deploy boards
*/
.deploy-board > div {
display: flex;
justify-content: space-between;
.deploy-board-information {
order: 1;
display: flex;
width: 70px;
flex-wrap: wrap;
justify-content: center;
margin: 20px 0 20px 10px;
> span {
text-align: center;
}
.percentage {
color: $gl-text-color;
}
.text {
color: $gl-text-color-secondary;
}
}
.deploy-board-instances {
order: 2;
width: 75%;
.text {
color: $gl-text-color-secondary;
font-size: 12px;
}
.deploy-board-instances-container {
display: flex;
flex-wrap: wrap;
flex-direction: row;
margin-top: -8px;
}
}
.deploy-board-actions {
order: 3;
align-self: center;
}
&.deploy-board-error-message {
justify-content: center;
}
}
.deploy-board-instance {
width: 15px;
height: 15px;
border-radius: 3px;
border-width: 1px;
border-style: solid;
margin: 1px;
&-finished {
background-color: lighten($green-light, 25%);
border-color: $green-light;
}
&-deploying {
background-color: lighten($green-light, 40%);
border-color: $green-light;
}
&-failed {
background-color: lighten($red-light, 20%);
border-color: $red-normal;
}
&-ready {
background-color: lighten($border-color, 1%);
border-color: $border-color;
}
&-preparing {
background-color: lighten($border-color, 5%);
border-color: $border-color;
}
&-waiting {
background-color: $white-light;
border-color: $border-color;
}
}
.deploy-board-icon i {
cursor: pointer;
color: $layout-link-gray;
padding-right: 10px;
}
.deploy-board {
padding: 10px;
background-color: $gray-light;
min-height: 20px;
.fa-spinner {
margin: 0 auto;
width: 20px;
display: block;
font-size: 20px;
}
}
......@@ -98,6 +98,10 @@
padding: 10px 0;
}
td.deploy-board-container {
padding: 0;
}
.commit-link {
padding: 9px 8px 10px;
}
......
---
title: Adds abitlity to render deploy boards in the frontend side
merge_request: 1233
author:
const Vue = require('vue');
const DeployBoard = require('~/environments/components/deploy_board_component');
const Service = require('~/environments/services/environments_service');
const { deployBoardMockData } = require('./mock_data');
describe('Deploy Board', () => {
let DeployBoardComponent;
beforeEach(() => {
DeployBoardComponent = Vue.extend(DeployBoard);
});
describe('successfull request', () => {
const deployBoardInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify(deployBoardMockData), {
status: 200,
}));
};
let component;
beforeEach(() => {
Vue.http.interceptors.push(deployBoardInterceptor);
this.service = new Service('environments');
component = new DeployBoardComponent({
propsData: {
store: {},
service: this.service,
deployBoardData: deployBoardMockData,
environmentID: 1,
},
}).$mount();
});
afterEach(() => {
Vue.http.interceptors = _.without(
Vue.http.interceptors, deployBoardInterceptor,
);
});
it('should render percentage with completion value provided', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-information .percentage').textContent,
).toEqual(`${deployBoardMockData.completion}%`);
done();
}, 0);
});
it('should render all instances', (done) => {
setTimeout(() => {
const instances = component.$el.querySelectorAll('.deploy-board-instances-container div');
expect(instances.length).toEqual(deployBoardMockData.instances.length);
expect(
instances[2].classList.contains(`deploy-board-instance-${deployBoardMockData.instances[2].status}`),
).toBe(true);
done();
}, 0);
});
it('should render an abort and a rollback button with the provided url', (done) => {
setTimeout(() => {
const buttons = component.$el.querySelectorAll('.deploy-board-actions a');
expect(buttons[0].getAttribute('href')).toEqual(deployBoardMockData.rollback_url);
expect(buttons[1].getAttribute('href')).toEqual(deployBoardMockData.abort_url);
done();
}, 0);
});
});
describe('unsuccessfull request', () => {
const deployBoardErrorInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({}), {
status: 500,
}));
};
let component;
beforeEach(() => {
Vue.http.interceptors.push(deployBoardErrorInterceptor);
this.service = new Service('environments');
component = new DeployBoardComponent({
propsData: {
store: {},
service: this.service,
deployBoardData: {},
environmentID: 1,
},
}).$mount();
});
afterEach(() => {
Vue.http.interceptors = _.without(Vue.http.interceptors, deployBoardErrorInterceptor);
});
it('should render empty state', (done) => {
setTimeout(() => {
expect(component.$el.children.length).toEqual(0);
done();
}, 0);
});
});
});
const Vue = require('vue');
const DeployBoardInstance = require('~/environments/components/deploy_board_instance_component');
describe('Deploy Board Instance', () => {
let DeployBoardInstanceComponent;
beforeEach(() => {
DeployBoardInstanceComponent = Vue.extend(DeployBoardInstance);
});
it('should render a div with the correct css status and tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'ready',
tooltipText: 'This is a pod',
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-ready')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('This is a pod');
});
it('should render a div without tooltip data', () => {
const component = new DeployBoardInstanceComponent({
propsData: {
status: 'deploying',
},
}).$mount();
expect(component.$el.classList.contains('deploy-board-instance-deploying')).toBe(true);
expect(component.$el.getAttribute('data-title')).toEqual('');
});
});
......@@ -26,6 +26,9 @@ describe('Environment item', () => {
model: mockItem,
canCreateDeployment: false,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
});
......@@ -114,6 +117,9 @@ describe('Environment item', () => {
model: environment,
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
});
......
......@@ -49,7 +49,7 @@ describe('Environment', () => {
});
});
describe('with paginated environments', () => {
describe('with environments', () => {
const environmentsResponseInterceptor = (request, next) => {
next(request.respondWith(JSON.stringify({
environments: [environment],
......@@ -142,6 +142,17 @@ describe('Environment', () => {
}, 0);
});
});
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
done();
}, 0);
});
});
});
});
......
......@@ -9,22 +9,101 @@ describe('Environment item', () => {
it('Should render a table', () => {
const mockItem = {
name: 'review',
folderName: 'review',
size: 3,
isFolder: true,
latest: {
environment_path: 'url',
},
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [{ mockItem }],
environments: [mockItem],
canCreateDeployment: false,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
expect(component.$el.tagName).toEqual('TABLE');
});
it('should render deploy board container when data is provided', () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
rollout_status_path: 'url',
hasDeployBoard: true,
deployBoardData: {
instances: [
{ status: 'ready', tooltip: 'foo' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
},
isDeployBoardVisible: true,
};
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem],
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: () => {},
store: {},
service: {},
},
});
expect(component.$el.querySelector('.js-deploy-board-row')).toBeDefined();
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
});
it('should toggle deploy board visibility when arrow is clicked', () => {
const mockItem = {
name: 'review',
size: 1,
environment_path: 'url',
id: 1,
rollout_status_path: 'url',
hasDeployBoard: true,
deployBoardData: {
instances: [
{ status: 'ready', tooltip: 'foo' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
},
isDeployBoardVisible: false,
};
const spy = jasmine.createSpy('spy');
const component = new EnvironmentTable({
el: document.querySelector('.test-dom-element'),
propsData: {
environments: [mockItem],
canCreateDeployment: true,
canReadEnvironment: true,
toggleDeployBoard: spy,
store: {},
service: {},
},
});
component.$el.querySelector('.deploy-board-icon').click();
expect(spy).toHaveBeenCalled();
});
});
const Store = require('~/environments/stores/environments_store');
const { environmentsList, serverData } = require('./mock_data');
const { serverData, deployBoardMockData } = require('./mock_data');
(() => {
describe('Store', () => {
describe('Environments Store', () => {
let store;
beforeEach(() => {
......@@ -16,10 +16,53 @@ const { environmentsList, serverData } = require('./mock_data');
expect(store.state.paginationInformation).toEqual({});
});
describe('store environments', () => {
it('should store environments', () => {
store.storeEnvironments(serverData);
expect(store.state.environments.length).toEqual(serverData.length);
expect(store.state.environments[0]).toEqual(environmentsList[0]);
});
it('should store a non folder environment with deploy board if rollout_status_path key is provided', () => {
const environment = {
name: 'foo',
size: 1,
id: 1,
rollout_status_path: 'url',
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].hasDeployBoard).toEqual(true);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(false);
expect(store.state.environments[0].deployBoardData).toEqual({});
});
it('should add folder keys when environment is a folder', () => {
const environment = {
name: 'bar',
size: 3,
id: 2,
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].isFolder).toEqual(true);
expect(store.state.environments[0].folderName).toEqual('bar');
});
it('should extract content of `latest` key when provided', () => {
const environment = {
name: 'bar',
size: 3,
id: 2,
latest: {
last_deployment: {},
isStoppable: true,
},
};
store.storeEnvironments([environment]);
expect(store.state.environments[0].last_deployment).toEqual({});
expect(store.state.environments[0].isStoppable).toEqual(true);
});
});
it('should store available count', () => {
......@@ -32,7 +75,8 @@ const { environmentsList, serverData } = require('./mock_data');
expect(store.state.stoppedCounter).toEqual(2);
});
it('should store pagination information', () => {
describe('store pagination', () => {
it('should store normalized and integer pagination information', () => {
const pagination = {
'X-nExt-pAge': '2',
'X-page': '1',
......@@ -55,4 +99,27 @@ const { environmentsList, serverData } = require('./mock_data');
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
describe('deploy boards', () => {
beforeEach(() => {
const environment = {
name: 'foo',
size: 1,
id: 1,
};
store.storeEnvironments([environment]);
});
it('should toggle deploy board property for given environment id', () => {
store.toggleDeployBoard(1);
expect(store.state.environments[0].isDeployBoardVisible).toEqual(true);
});
it('should store deploy board data for given environment id', () => {
store.storeDeployBoard(1, deployBoardMockData);
expect(store.state.environments[0].deployBoardData).toEqual(deployBoardMockData);
});
});
});
})();
......@@ -141,6 +141,17 @@ describe('Environments Folder View', () => {
}, 0);
});
});
describe('deploy boards', () => {
it('should render arrow to open deploy boards', (done) => {
setTimeout(() => {
expect(
component.$el.querySelector('.deploy-board-icon i').classList.contains('fa-caret-right'),
).toEqual(true);
done();
}, 0);
});
});
});
describe('unsuccessfull request', () => {
......
......@@ -12,6 +12,7 @@ const environmentsList = [
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path',
},
{
folderName: 'build',
......@@ -82,11 +83,49 @@ const environment = {
stop_path: '/root/review-app/environments/7/stop',
created_at: '2017-01-31T10:53:46.894Z',
updated_at: '2017-01-31T10:53:46.894Z',
rollout_status_path: '/path',
},
};
const deployBoardMockData = {
instances: [
{ status: 'finished', tooltip: 'tanuki-2334 Finished' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' },
{ status: 'finished', tooltip: 'tanuki-2334 Finished' },
{ status: 'finished', tooltip: 'tanuki-2335 Finished' },
{ status: 'finished', tooltip: 'tanuki-2336 Finished' },
{ status: 'finished', tooltip: 'tanuki-2337 Finished' },
{ status: 'finished', tooltip: 'tanuki-2338 Finished' },
{ status: 'finished', tooltip: 'tanuki-2339 Finished' },
{ status: 'finished', tooltip: 'tanuki-2340 Finished' },
{ status: 'deploying', tooltip: 'tanuki-2341 Deploying' },
{ status: 'deploying', tooltip: 'tanuki-2342 Deploying' },
{ status: 'deploying', tooltip: 'tanuki-2343 Deploying' },
{ status: 'failed', tooltip: 'tanuki-2344 Failed' },
{ status: 'ready', tooltip: 'tanuki-2345 Ready' },
{ status: 'ready', tooltip: 'tanuki-2346 Ready' },
{ status: 'preparing', tooltip: 'tanuki-2348 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2349 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2350 Preparing' },
{ status: 'preparing', tooltip: 'tanuki-2353 Preparing' },
{ status: 'waiting', tooltip: 'tanuki-2354 Waiting' },
{ status: 'waiting', tooltip: 'tanuki-2355 Waiting' },
{ status: 'waiting', tooltip: 'tanuki-2356 Waiting' },
],
abort_url: 'url',
rollback_url: 'url',
completion: 100,
is_completed: true,
};
module.exports = {
environmentsList,
environment,
serverData,
deployBoardMockData,
};
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