summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFatih Acet <acetfatih@gmail.com>2017-01-19 20:46:04 +0000
committerFatih Acet <acetfatih@gmail.com>2017-01-19 20:46:04 +0000
commit6669bfd3d19bcab435fc477d097172fbf488d6fb (patch)
tree52b15da3c8862b89d8e0702a26663cc771487610
parent1c81452a9955bb06515faf26cedbe8e2b28791d5 (diff)
parent725b16543d75b82fbcd858ce9d2c88fbe92cc450 (diff)
downloadgitlab-ce-6669bfd3d19bcab435fc477d097172fbf488d6fb.tar.gz
Merge branch '25507-handle-errors-environment-list' into 'master'
Resolve "Error handling in environments list" Closes #25507 See merge request !8461
-rw-r--r--app/assets/javascripts/environments/components/environment.js.es656
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js.es663
-rw-r--r--changelogs/unreleased/25507-handle-errors-environment-list.yml4
-rw-r--r--spec/javascripts/environments/environment_spec.js.es6127
-rw-r--r--spec/javascripts/environments/mock_data.js.es614
-rw-r--r--spec/javascripts/fixtures/environments/environments.html.haml2
6 files changed, 218 insertions, 48 deletions
diff --git a/app/assets/javascripts/environments/components/environment.js.es6 b/app/assets/javascripts/environments/components/environment.js.es6
index 8b6fafb6104..fea642467fa 100644
--- a/app/assets/javascripts/environments/components/environment.js.es6
+++ b/app/assets/javascripts/environments/components/environment.js.es6
@@ -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
diff --git a/app/assets/javascripts/environments/stores/environments_store.js.es6 b/app/assets/javascripts/environments/stores/environments_store.js.es6
index 0204a903ab5..9b4090100da 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js.es6
+++ b/app/assets/javascripts/environments/stores/environments_store.js.es6
@@ -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.
*
diff --git a/changelogs/unreleased/25507-handle-errors-environment-list.yml b/changelogs/unreleased/25507-handle-errors-environment-list.yml
new file mode 100644
index 00000000000..4e9794f7917
--- /dev/null
+++ b/changelogs/unreleased/25507-handle-errors-environment-list.yml
@@ -0,0 +1,4 @@
+---
+title: Handle HTTP errors in environment list
+merge_request:
+author:
diff --git a/spec/javascripts/environments/environment_spec.js.es6 b/spec/javascripts/environments/environment_spec.js.es6
new file mode 100644
index 00000000000..20e11ca3738
--- /dev/null
+++ b/spec/javascripts/environments/environment_spec.js.es6
@@ -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);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/mock_data.js.es6 b/spec/javascripts/environments/mock_data.js.es6
index 9e16bc3e6a5..8ecd01f9a83 100644
--- a/spec/javascripts/environments/mock_data.js.es6
+++ b/spec/javascripts/environments/mock_data.js.es6
@@ -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',
+};
diff --git a/spec/javascripts/fixtures/environments/environments.html.haml b/spec/javascripts/fixtures/environments/environments.html.haml
index d89bc50c1f0..e6000fbb553 100644
--- a/spec/javascripts/fixtures/environments/environments.html.haml
+++ b/spec/javascripts/fixtures/environments/environments.html.haml
@@ -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",