Merge branch '25507-handle-errors-environment-list' into 'master'
Resolve "Error handling in environments list" Closes #25507 See merge request !8461
This commit is contained in:
commit
6669bfd3d1
6 changed files with 218 additions and 48 deletions
|
@ -1,6 +1,7 @@
|
|||
/* eslint-disable no-param-reassign */
|
||||
/* eslint-disable no-param-reassign, no-new */
|
||||
/* global Vue */
|
||||
/* global EnvironmentsService */
|
||||
/* global Flash */
|
||||
|
||||
//= require vue
|
||||
//= require vue-resource
|
||||
|
@ -10,41 +11,6 @@
|
|||
(() => {
|
||||
window.gl = window.gl || {};
|
||||
|
||||
/**
|
||||
* Given the visibility prop provided by the url query parameter and which
|
||||
* changes according to the active tab we need to filter which environments
|
||||
* should be visible.
|
||||
*
|
||||
* The environments array is a recursive tree structure and we need to filter
|
||||
* both root level environments and children environments.
|
||||
*
|
||||
* In order to acomplish that, both `filterState` and `filterEnvironmentsByState`
|
||||
* functions work together.
|
||||
* The first one works as the filter that verifies if the given environment matches
|
||||
* the given state.
|
||||
* The second guarantees both root level and children elements are filtered as well.
|
||||
*/
|
||||
|
||||
const filterState = state => environment => environment.state === state && environment;
|
||||
/**
|
||||
* Given the filter function and the array of environments will return only
|
||||
* the environments that match the state provided to the filter function.
|
||||
*
|
||||
* @param {Function} fn
|
||||
* @param {Array} array
|
||||
* @return {Array}
|
||||
*/
|
||||
const filterEnvironmentsByState = (fn, arr) => arr.map((item) => {
|
||||
if (item.children) {
|
||||
const filteredChildren = filterEnvironmentsByState(fn, item.children).filter(Boolean);
|
||||
if (filteredChildren.length) {
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return fn(item);
|
||||
}).filter(Boolean);
|
||||
|
||||
gl.environmentsList.EnvironmentsComponent = Vue.component('environment-component', {
|
||||
props: {
|
||||
store: {
|
||||
|
@ -81,10 +47,6 @@
|
|||
},
|
||||
|
||||
computed: {
|
||||
filteredEnvironments() {
|
||||
return filterEnvironmentsByState(filterState(this.visibility), this.state.environments);
|
||||
},
|
||||
|
||||
scope() {
|
||||
return this.$options.getQueryParameter('scope');
|
||||
},
|
||||
|
@ -111,7 +73,7 @@
|
|||
|
||||
const scope = this.$options.getQueryParameter('scope');
|
||||
if (scope) {
|
||||
this.visibility = scope;
|
||||
this.store.storeVisibility(scope);
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
@ -121,6 +83,10 @@
|
|||
.then((json) => {
|
||||
this.store.storeEnvironments(json);
|
||||
this.isLoading = false;
|
||||
})
|
||||
.catch(() => {
|
||||
this.isLoading = false;
|
||||
new Flash('An error occurred while fetching the environments.', 'alert');
|
||||
});
|
||||
},
|
||||
|
||||
|
@ -188,7 +154,7 @@
|
|||
|
||||
<div class="blank-state blank-state-no-icon"
|
||||
v-if="!isLoading && state.environments.length === 0">
|
||||
<h2 class="blank-state-title">
|
||||
<h2 class="blank-state-title js-blank-state-title">
|
||||
You don't have any environments right now.
|
||||
</h2>
|
||||
<p class="blank-state-text">
|
||||
|
@ -202,13 +168,13 @@
|
|||
<a
|
||||
v-if="canCreateEnvironmentParsed"
|
||||
:href="newEnvironmentPath"
|
||||
class="btn btn-create">
|
||||
class="btn btn-create js-new-environment-button">
|
||||
New Environment
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="table-holder"
|
||||
v-if="!isLoading && state.environments.length > 0">
|
||||
v-if="!isLoading && state.filteredEnvironments.length > 0">
|
||||
<table class="table ci-table environments">
|
||||
<thead>
|
||||
<tr>
|
||||
|
@ -221,7 +187,7 @@
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="model in filteredEnvironments"
|
||||
<template v-for="model in state.filteredEnvironments"
|
||||
v-bind:model="model">
|
||||
|
||||
<tr
|
||||
|
|
|
@ -10,6 +10,8 @@
|
|||
this.state.environments = [];
|
||||
this.state.stoppedCounter = 0;
|
||||
this.state.availableCounter = 0;
|
||||
this.state.visibility = 'available';
|
||||
this.state.filteredEnvironments = [];
|
||||
|
||||
return this;
|
||||
},
|
||||
|
@ -59,7 +61,7 @@
|
|||
|
||||
if (occurs.length) {
|
||||
acc[acc.indexOf(occurs[0])].children.push(environment);
|
||||
acc[acc.indexOf(occurs[0])].children.sort(this.sortByName);
|
||||
acc[acc.indexOf(occurs[0])].children.slice().sort(this.sortByName);
|
||||
} else {
|
||||
acc.push({
|
||||
name: environment.environment_type,
|
||||
|
@ -73,13 +75,70 @@
|
|||
}
|
||||
|
||||
return acc;
|
||||
}, []).sort(this.sortByName);
|
||||
}, []).slice().sort(this.sortByName);
|
||||
|
||||
this.state.environments = environmentsTree;
|
||||
|
||||
this.filterEnvironmentsByVisibility(this.state.environments);
|
||||
|
||||
return environmentsTree;
|
||||
},
|
||||
|
||||
storeVisibility(visibility) {
|
||||
this.state.visibility = visibility;
|
||||
},
|
||||
/**
|
||||
* Given the visibility prop provided by the url query parameter and which
|
||||
* changes according to the active tab we need to filter which environments
|
||||
* should be visible.
|
||||
*
|
||||
* The environments array is a recursive tree structure and we need to filter
|
||||
* both root level environments and children environments.
|
||||
*
|
||||
* In order to acomplish that, both `filterState` and `filterEnvironmentsByVisibility`
|
||||
* functions work together.
|
||||
* The first one works as the filter that verifies if the given environment matches
|
||||
* the given state.
|
||||
* The second guarantees both root level and children elements are filtered as well.
|
||||
*
|
||||
* Given array of environments will return only
|
||||
* the environments that match the state stored.
|
||||
*
|
||||
* @param {Array} array
|
||||
* @return {Array}
|
||||
*/
|
||||
filterEnvironmentsByVisibility(arr) {
|
||||
const filteredEnvironments = arr.map((item) => {
|
||||
if (item.children) {
|
||||
const filteredChildren = this.filterEnvironmentsByVisibility(
|
||||
item.children,
|
||||
).filter(Boolean);
|
||||
|
||||
if (filteredChildren.length) {
|
||||
item.children = filteredChildren;
|
||||
return item;
|
||||
}
|
||||
}
|
||||
|
||||
return this.filterState(this.state.visibility, item);
|
||||
}).filter(Boolean);
|
||||
|
||||
this.state.filteredEnvironments = filteredEnvironments;
|
||||
return filteredEnvironments;
|
||||
},
|
||||
|
||||
/**
|
||||
* Given the state and the environment,
|
||||
* returns only if the environment state matches the one provided.
|
||||
*
|
||||
* @param {String} state
|
||||
* @param {Object} environment
|
||||
* @return {Object}
|
||||
*/
|
||||
filterState(state, environment) {
|
||||
return environment.state === state && environment;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggles folder open property given the environment type.
|
||||
*
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
---
|
||||
title: Handle HTTP errors in environment list
|
||||
merge_request:
|
||||
author:
|
127
spec/javascripts/environments/environment_spec.js.es6
Normal file
127
spec/javascripts/environments/environment_spec.js.es6
Normal file
|
@ -0,0 +1,127 @@
|
|||
/* global Vue, environment */
|
||||
|
||||
//= require vue
|
||||
//= require vue-resource
|
||||
//= require flash
|
||||
//= require environments/stores/environments_store
|
||||
//= require environments/components/environment
|
||||
//= require ./mock_data
|
||||
|
||||
describe('Environment', () => {
|
||||
preloadFixtures('environments/environments');
|
||||
|
||||
let component;
|
||||
|
||||
beforeEach(() => {
|
||||
loadFixtures('environments/environments');
|
||||
});
|
||||
|
||||
describe('successfull request', () => {
|
||||
describe('without environments', () => {
|
||||
const environmentsEmptyResponseInterceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify([]), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(environmentsEmptyResponseInterceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(
|
||||
Vue.http.interceptors, environmentsEmptyResponseInterceptor,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render the empty state', (done) => {
|
||||
component = new gl.environmentsList.EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
propsData: {
|
||||
store: gl.environmentsList.EnvironmentsStore.create(),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-new-environment-button').textContent,
|
||||
).toContain('New Environment');
|
||||
|
||||
expect(
|
||||
component.$el.querySelector('.js-blank-state-title').textContent,
|
||||
).toContain('You don\'t have any environments right now.');
|
||||
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('with environments', () => {
|
||||
const environmentsResponseInterceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify([environment]), {
|
||||
status: 200,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(environmentsResponseInterceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(
|
||||
Vue.http.interceptors, environmentsResponseInterceptor,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render a table with environments', (done) => {
|
||||
component = new gl.environmentsList.EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
propsData: {
|
||||
store: gl.environmentsList.EnvironmentsStore.create(),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
component.$el.querySelectorAll('table tbody tr').length,
|
||||
).toEqual(1);
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('unsuccessfull request', () => {
|
||||
const environmentsErrorResponseInterceptor = (request, next) => {
|
||||
next(request.respondWith(JSON.stringify([]), {
|
||||
status: 500,
|
||||
}));
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Vue.http.interceptors.push(environmentsErrorResponseInterceptor);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
Vue.http.interceptors = _.without(
|
||||
Vue.http.interceptors, environmentsErrorResponseInterceptor,
|
||||
);
|
||||
});
|
||||
|
||||
it('should render empty state', (done) => {
|
||||
component = new gl.environmentsList.EnvironmentsComponent({
|
||||
el: document.querySelector('#environments-list-view'),
|
||||
propsData: {
|
||||
store: gl.environmentsList.EnvironmentsStore.create(),
|
||||
},
|
||||
});
|
||||
|
||||
setTimeout(() => {
|
||||
expect(
|
||||
component.$el.querySelector('.js-blank-state-title').textContent,
|
||||
).toContain('You don\'t have any environments right now.');
|
||||
done();
|
||||
}, 0);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -133,3 +133,17 @@ const environmentsList = [
|
|||
updated_at: '2016-11-07T11:11:16.525Z',
|
||||
},
|
||||
];
|
||||
|
||||
const environment = {
|
||||
id: 4,
|
||||
name: 'production',
|
||||
state: 'available',
|
||||
external_url: 'http://production.',
|
||||
environment_type: null,
|
||||
last_deployment: {},
|
||||
'stoppable?': false,
|
||||
environment_path: '/root/review-app/environments/4',
|
||||
stop_path: '/root/review-app/environments/4/stop',
|
||||
created_at: '2016-12-16T11:51:04.690Z',
|
||||
updated_at: '2016-12-16T12:04:51.133Z',
|
||||
};
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
%div
|
||||
#environments-list-view{ data: { environments_data: "https://gitlab.com/foo/environments",
|
||||
#environments-list-view{ data: { environments_data: "foo/environments",
|
||||
"can-create-deployment" => "true",
|
||||
"can-read-environment" => "true",
|
||||
"can-create-environment" => "true",
|
||||
|
|
Loading…
Reference in a new issue