Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBryce Johnson <bryce@gitlab.com>2017-04-19 23:26:52 +0300
committerBryce Johnson <bryce@gitlab.com>2017-04-25 18:09:11 +0300
commitb29090ef871b0c45e4a05da9421e027846657658 (patch)
tree8391794b40d2f31aee837be6b9096dea4f2a08f0
parent5a95bab45b55a25ba5c57f3d0a3487e863ec8405 (diff)
Ensure pipeline action buttons are updated after action
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js5
-rw-r--r--app/assets/javascripts/pipelines/components/async_button.vue63
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js56
-rw-r--r--spec/javascripts/pipelines/async_button_spec.js64
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js121
6 files changed, 167 insertions, 143 deletions
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 68a1c1de1df..981ffbe3791 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -43,7 +43,7 @@ export default Vue.component('pipelines-table', {
helpPagePath: null,
store,
state: store.state,
- isLoading: false,
+ isLoading: true,
hasError: false,
isMakingRequest: false,
};
@@ -91,7 +91,6 @@ export default Vue.component('pipelines-table', {
});
if (!Visibility.hidden()) {
- this.isLoading = true;
this.poll.makeRequest();
}
@@ -125,8 +124,6 @@ export default Vue.component('pipelines-table', {
methods: {
fetchPipelines() {
- this.isLoading = true;
-
return this.service.getPipelines()
.then(response => this.successCallback(response))
.catch(() => this.errorCallback());
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
index d1c60b570de..1c7bd3aea42 100644
--- a/app/assets/javascripts/pipelines/components/async_button.vue
+++ b/app/assets/javascripts/pipelines/components/async_button.vue
@@ -1,82 +1,30 @@
<script>
-/* eslint-disable no-new, no-alert */
-/* global Flash */
-import '~/flash';
-import eventHub from '../event_hub';
-
export default {
props: {
- endpoint: {
- type: String,
- required: true,
- },
-
- service: {
- type: Object,
- required: true,
- },
-
title: {
type: String,
required: true,
},
-
icon: {
type: String,
required: true,
},
-
- cssClass: {
- type: String,
+ isLoading: {
+ type: Boolean,
required: true,
},
-
- confirmActionMessage: {
- type: String,
- required: false,
- },
- },
-
- data() {
- return {
- isLoading: false,
- };
},
computed: {
iconClass() {
return `fa fa-${this.icon}`;
},
-
buttonClass() {
- return `btn has-tooltip ${this.cssClass}`;
+ return `btn has-tooltip`;
},
},
-
- methods: {
- onClick() {
- if (this.confirmActionMessage && confirm(this.confirmActionMessage)) {
- this.makeRequest();
- } else if (!this.confirmActionMessage) {
- this.makeRequest();
- }
- },
-
- makeRequest() {
- this.isLoading = true;
-
- $(this.$el).tooltip('destroy');
-
- this.service.postAction(this.endpoint)
- .then(() => {
- this.isLoading = false;
- eventHub.$emit('refreshPipelines');
- })
- .catch(() => {
- this.isLoading = false;
- new Flash('An error occured while making the request.');
- });
- },
+ updated() {
+ $(this.$el).tooltip('destroy');
},
};
</script>
@@ -84,7 +32,6 @@ export default {
<template>
<button
type="button"
- @click="onClick"
:class="buttonClass"
:title="title"
:aria-label="title"
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index 6c2174a3717..ab57701c85a 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -208,6 +208,7 @@ export default {
errorCallback() {
this.hasError = true;
+ this.isLoading = false;
},
setIsMakingRequest(isMakingRequest) {
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index 62b7131de51..ab00bf33011 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -1,5 +1,6 @@
-/* eslint-disable no-param-reassign */
-
+/* eslint-disable no-param-reassign, no-alert */
+import Flash from '~/flash';
+import eventHub from '../../pipelines/event_hub';
import AsyncButtonComponent from '../../pipelines/components/async_button.vue';
import PipelinesActionsComponent from '../../pipelines/components/pipelines_actions';
import PipelinesArtifactsComponent from '../../pipelines/components/pipelines_artifacts';
@@ -26,7 +27,12 @@ export default {
required: true,
},
},
-
+ data() {
+ return {
+ isRetrying: false,
+ isCancelling: false,
+ };
+ },
components: {
'async-button-component': AsyncButtonComponent,
'pipelines-actions-component': PipelinesActionsComponent,
@@ -167,7 +173,34 @@ export default {
return undefined;
},
},
+ watch: {
+ // we watch pipeline update bc we don't know when refreshPipelines is finished
+ pipeline: 'resetButtonLoadingState',
+ },
+ methods: {
+ resetButtonLoadingState() {
+ this.isCancelling = false;
+ this.isRetrying = false;
+ },
+ cancelPipeline() {
+ this.isCancelling = true;
+ const confirmCancelMessage = 'Are you sure you want to cancel this pipeline?';
+ return this.makeRequest(this.pipeline.cancel_path, confirmCancelMessage);
+ },
+ retryPipeline() {
+ this.isRetrying = true;
+ return this.makeRequest(this.pipeline.retry_path);
+ },
+ makeRequest(endpoint, confirmMessage) {
+ if (confirmMessage && !confirm(confirmMessage)) {
+ return Promise.resolve();
+ }
+ return this.service.postAction(endpoint)
+ .then(() => eventHub.$emit('refreshPipelines'))
+ .catch(() => new Flash('An error occured while making the request.'));
+ },
+ },
template: `
<tr class="commit">
<status-scope :pipeline="pipeline"/>
@@ -207,20 +240,19 @@ export default {
<async-button-component
v-if="pipeline.flags.retryable"
- :service="service"
- :endpoint="pipeline.retry_path"
- css-class="js-pipelines-retry-button btn-default btn-retry"
+ class="js-pipelines-retry-button btn-default btn-retry"
+ @click.native="retryPipeline"
+ :is-loading="isRetrying"
title="Retry"
- icon="repeat" />
+ icon="repeat"/>
<async-button-component
v-if="pipeline.flags.cancelable"
- :service="service"
- :endpoint="pipeline.cancel_path"
- css-class="js-pipelines-cancel-button btn-remove"
+ class="js-pipelines-cancel-button btn-remove"
+ @click.native="cancelPipeline"
+ :is-loading="isCancelling"
title="Cancel"
- icon="remove"
- confirm-action-message="Are you sure you want to cancel this pipeline?" />
+ icon="remove"/>
</div>
</td>
</tr>
diff --git a/spec/javascripts/pipelines/async_button_spec.js b/spec/javascripts/pipelines/async_button_spec.js
index 28c9c7ab282..c1f8f671112 100644
--- a/spec/javascripts/pipelines/async_button_spec.js
+++ b/spec/javascripts/pipelines/async_button_spec.js
@@ -3,23 +3,16 @@ import asyncButtonComp from '~/pipelines/components/async_button.vue';
describe('Pipelines Async Button', () => {
let component;
- let spy;
let AsyncButtonComponent;
beforeEach(() => {
AsyncButtonComponent = Vue.extend(asyncButtonComp);
- spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
-
component = new AsyncButtonComponent({
propsData: {
- endpoint: '/foo',
title: 'Foo',
icon: 'fa fa-foo',
- cssClass: 'bar',
- service: {
- postAction: spy,
- },
+ isLoading: false,
},
}).$mount();
});
@@ -37,57 +30,16 @@ describe('Pipelines Async Button', () => {
expect(component.$el.getAttribute('aria-label')).toContain('Foo');
});
- it('should render the provided cssClass', () => {
- expect(component.$el.getAttribute('class')).toContain('bar');
- });
-
- it('should call the service when it is clicked with the provided endpoint', () => {
- component.$el.click();
- expect(spy).toHaveBeenCalledWith('/foo');
- });
-
- it('should hide loading if request fails', () => {
- spy = jasmine.createSpy('spy').and.returnValue(Promise.reject());
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'fa fa-foo',
- cssClass: 'bar',
- dataAttributes: {
- 'data-foo': 'foo',
- },
- service: {
- postAction: spy,
- },
- },
- }).$mount();
-
- component.$el.click();
- expect(component.$el.querySelector('.fa-spinner')).toBe(null);
+ it('should not render the spinner when not loading', () => {
+ expect(component.$el.querySelector('.fa-spinner')).toBeNull();
});
- describe('With confirm dialog', () => {
- it('should call the service when confimation is positive', () => {
- spyOn(window, 'confirm').and.returnValue(true);
- spy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
-
- component = new AsyncButtonComponent({
- propsData: {
- endpoint: '/foo',
- title: 'Foo',
- icon: 'fa fa-foo',
- cssClass: 'bar',
- service: {
- postAction: spy,
- },
- confirmActionMessage: 'bar',
- },
- }).$mount();
+ it('should render the spinner when loading state changes', (done) => {
+ component.isLoading = true;
- component.$el.click();
- expect(spy).toHaveBeenCalledWith('/foo');
+ Vue.nextTick(() => {
+ expect(component.$el.querySelector('.fa-spinner')).not.toBe(null);
+ done();
});
});
});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 699625cdbb7..cdb376492a6 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -3,34 +3,36 @@ import tableRowComp from '~/vue_shared/components/pipelines_table_row';
import pipeline from '../../commit/pipelines/mock_data';
describe('Pipelines Table Row', () => {
- let component;
+ const postActionSpy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
beforeEach(() => {
const PipelinesTableRowComponent = Vue.extend(tableRowComp);
- component = new PipelinesTableRowComponent({
+ this.component = new PipelinesTableRowComponent({
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
- service: {},
+ service: {
+ postAction: postActionSpy,
+ },
},
}).$mount();
});
it('should render a table row', () => {
- expect(component.$el).toEqual('TR');
+ expect(this.component.$el).toEqual('TR');
});
describe('status column', () => {
it('should render a pipeline link', () => {
expect(
- component.$el.querySelector('td.commit-link a').getAttribute('href'),
+ this.component.$el.querySelector('td.commit-link a').getAttribute('href'),
).toEqual(pipeline.path);
});
it('should render status text', () => {
expect(
- component.$el.querySelector('td.commit-link a').textContent,
+ this.component.$el.querySelector('td.commit-link a').textContent,
).toContain(pipeline.details.status.text);
});
});
@@ -38,24 +40,24 @@ describe('Pipelines Table Row', () => {
describe('information column', () => {
it('should render a pipeline link', () => {
expect(
- component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
+ this.component.$el.querySelector('td:nth-child(2) a').getAttribute('href'),
).toEqual(pipeline.path);
});
it('should render pipeline ID', () => {
expect(
- component.$el.querySelector('td:nth-child(2) a > span').textContent,
+ this.component.$el.querySelector('td:nth-child(2) a > span').textContent,
).toEqual(`#${pipeline.id}`);
});
describe('when a user is provided', () => {
it('should render user information', () => {
expect(
- component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
+ this.component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
).toEqual(pipeline.user.web_url);
expect(
- component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
+ this.component.$el.querySelector('td:nth-child(2) img').getAttribute('title'),
).toEqual(pipeline.user.name);
});
});
@@ -64,7 +66,7 @@ describe('Pipelines Table Row', () => {
describe('commit column', () => {
it('should render link to commit', () => {
expect(
- component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
+ this.component.$el.querySelector('td:nth-child(3) .commit-id').getAttribute('href'),
).toEqual(pipeline.commit.commit_path);
});
});
@@ -72,7 +74,7 @@ describe('Pipelines Table Row', () => {
describe('stages column', () => {
it('should render an icon for each stage', () => {
expect(
- component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
+ this.component.$el.querySelectorAll('td:nth-child(4) .js-builds-dropdown-button').length,
).toEqual(pipeline.details.stages.length);
});
});
@@ -80,8 +82,101 @@ describe('Pipelines Table Row', () => {
describe('actions column', () => {
it('should render the provided actions', () => {
expect(
- component.$el.querySelectorAll('td:nth-child(6) ul li').length,
+ this.component.$el.querySelectorAll('td:nth-child(6) ul li').length,
).toEqual(pipeline.details.manual_actions.length);
});
});
+
+ describe('async button action methods', () => {
+ beforeEach(() => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ });
+
+ it('#resetButtonLoadingState resets isCancelling', (done) => {
+ this.component.isCancelling = true;
+
+ this.component.resetButtonLoadingState();
+
+ Vue.nextTick(() => {
+ expect(this.component.isCancelling).toBe(false);
+ done();
+ });
+ });
+
+ it('#resetButtonLoadingState resets isRetrying', (done) => {
+ this.component.isRetrying = true;
+
+ this.component.resetButtonLoadingState();
+
+ Vue.nextTick(() => {
+ expect(this.component.isRetrying).toBe(false);
+ done();
+ });
+ });
+
+ it('#cancelPipeline sets isCancelling', (done) => {
+ spyOn(this.component, 'makeRequest');
+
+ this.component.cancelPipeline();
+
+ Vue.nextTick(() => {
+ expect(this.component.isCancelling).toBe(true);
+ done();
+ });
+ });
+
+ it('#cancelPipeline calls makeRequest', (done) => {
+ spyOn(this.component, 'makeRequest');
+
+ this.component.cancelPipeline();
+
+ Vue.nextTick(() => {
+ expect(this.component.makeRequest).toHaveBeenCalled();
+ done();
+ });
+ });
+
+ it('#retryPipeline sets isRetrying', (done) => {
+ spyOn(this.component, 'makeRequest');
+
+ this.component.retryPipeline();
+
+ Vue.nextTick(() => {
+ expect(this.component.isRetrying).toBe(true);
+ done();
+ });
+ });
+
+ it('#retryPipeline calls makeRequest', (done) => {
+ spyOn(this.component, 'makeRequest');
+
+ this.component.retryPipeline();
+
+ Vue.nextTick(() => {
+ expect(this.component.makeRequest).toHaveBeenCalled();
+ done();
+ });
+ });
+
+
+ it('pipeline update triggers watcher to reset isCancelling', (done) => {
+ this.isCancelling = true;
+ this.component.$props.pipeline = Object.assign({}, pipeline, { created_at: new Date() });
+
+ Vue.nextTick(() => {
+ expect(this.component.isCancelling).toBe(false);
+ done();
+ });
+ });
+
+ it('pipeline update triggers watcher to reset isRetrying', (done) => {
+ this.isRetrying = true;
+ this.component.$props.pipeline = Object.assign({}, pipeline, { created_at: new Date() });
+
+ Vue.nextTick(() => {
+ expect(this.component.isRetrying).toBe(false);
+ done();
+ });
+ });
+ });
});