diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 68a1c1de1df804e0a1b38236e5cd7f843a449ae9..933aee2d7392dbd5ee541e41346c53ed8c064340 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -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());
@@ -172,7 +169,7 @@ export default Vue.component('pipelines-table', {
         v-if="shouldRenderTable">
         <pipelines-table-component
           :pipelines="state.pipelines"
-          :service="service" />
+          :service="service"/>
       </div>
     </div>
   `,
diff --git a/app/assets/javascripts/pipelines/components/async_button.vue b/app/assets/javascripts/pipelines/components/async_button.vue
index d1c60b570dec945c026a51d09d25f76d4bc283df..ced65b41c48309bf2564a6067d3e8f42d60c6406 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 6eea4812f335556a83086bba5caaa9e7a6d366cc..964efc5cc622debca076979e9eb8a735d44308c9 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -146,7 +146,6 @@ export default {
     });
 
     if (!Visibility.hidden()) {
-      this.isLoading = true;
       poll.makeRequest();
     }
 
@@ -189,8 +188,6 @@ export default {
 
     fetchPipelines() {
       if (!this.isMakingRequest) {
-        this.isLoading = true;
-
         this.service.getPipelines({ scope: this.scopeParameter, page: this.pageParameter })
           .then(response => this.successCallback(response))
           .catch(() => this.errorCallback());
@@ -206,7 +203,6 @@ export default {
       this.store.storeCount(response.body.count);
       this.store.storePipelines(response.body.pipelines);
       this.store.storePagination(response.headers);
-
       this.isLoading = false;
     },
 
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 62b7131de515248c15701e2e21f6819e52bdfc43..cf5161d65a626fc5f1b0eca0df0dfee182d3fd6e 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,
@@ -166,8 +172,48 @@ export default {
       }
       return undefined;
     },
+    /* These computed's allow us to track changes specifically to these values
+     and not update loading state everytime `pipeline` is updated */
+    isRetryable() {
+      return !!this.pipeline.flags.retryable;
+    },
+    isCancelable() {
+      return !!this.pipeline.flags.cancelable;
+    },
+  },
+  watch: {
+    /* When these values change, it's a signal that the retry/cancel
+    was successful **and** that all pipelines have been refreshed */
+    isRetryable() {
+      this.resetRequestingState();
+    },
+    isCancelable() {
+      this.resetRequestingState();
+    },
   },
+  methods: {
+    resetRequestingState() {
+      this.isRetrying = false;
+      this.isCancelling = false;
+    },
+    cancelPipeline() {
+      const confirmMessage = 'Are you sure you want to cancel this pipeline?';
 
+      if (confirmMessage && confirm(confirmMessage)) {
+        this.isCancelling = true;
+        this.makeRequest(this.pipeline.cancel_path);
+      }
+    },
+    retryPipeline() {
+      this.isRetrying = true;
+      this.makeRequest(this.pipeline.retry_path);
+    },
+    makeRequest(endpoint) {
+      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"/>
@@ -206,21 +252,20 @@ export default {
             :artifacts="pipeline.details.artifacts" />
 
           <async-button-component
-            v-if="pipeline.flags.retryable"
-            :service="service"
-            :endpoint="pipeline.retry_path"
-            css-class="js-pipelines-retry-button btn-default btn-retry"
+            v-if="isRetryable"
+            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"
+            v-if="isCancelable"
+            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 28c9c7ab2829c800e6adfa7101da62641702e690..02a7881552ae92c3b8d8c8310a9609024e50c2be 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,
-        },
+        icon: 'foo',
+        isLoading: false,
       },
     }).$mount();
   });
@@ -28,6 +21,10 @@ describe('Pipelines Async Button', () => {
     expect(component.$el.tagName).toEqual('BUTTON');
   });
 
+  it('#iconClass computed should return the provided icon', () => {
+    expect(component.iconClass).toBe('fa fa-foo');
+  });
+
   it('should render the provided icon', () => {
     expect(component.$el.querySelector('i').getAttribute('class')).toContain('fa fa-foo');
   });
@@ -37,57 +34,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 not render the spinner when not loading', () => {
+    expect(component.$el.querySelector('.fa-spinner')).toBeNull();
   });
 
-  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);
-  });
-
-  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 699625cdbb7e1ac3bdc1a14efff7988d136890db..7ec1e6b346901ef893a13741d91af06bb4c9218e 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,35 @@ import tableRowComp from '~/vue_shared/components/pipelines_table_row';
 import pipeline from '../../commit/pipelines/mock_data';
 
 describe('Pipelines Table Row', () => {
-  let component;
-
   beforeEach(() => {
+    const postActionSpy = jasmine.createSpy('spy').and.returnValue(Promise.resolve());
     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 +39,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 +65,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,16 +73,119 @@ 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);
     });
   });
 
   describe('actions column', () => {
     it('should render the provided actions', () => {
-      expect(
-        component.$el.querySelectorAll('td:nth-child(6) ul li').length,
-      ).toEqual(pipeline.details.manual_actions.length);
+      expect(this.component.$el.querySelectorAll('td:nth-child(6) ul li').length)
+        .toEqual(pipeline.details.manual_actions.length);
+    });
+
+    it('renders the cancel button', () => {
+      expect(this.component.$el.querySelectorAll('.js-pipelines-cancel-button')).not.toBeNull();
+    });
+
+    it('renders the retry button', () => {
+      expect(this.component.$el.querySelectorAll('.js-pipelines-retry-button')).not.toBeNull();
+    });
+  });
+
+  describe('async button action methods', () => {
+    beforeEach(() => {
+      spyOn(window, 'confirm').and.returnValue(true);
+    });
+
+    it('#resetRequestingState resets isCancelling', (done) => {
+      this.component.isCancelling = true;
+
+      this.component.resetRequestingState();
+
+      Vue.nextTick(() => {
+        expect(this.component.isCancelling).toBe(false);
+        done();
+      });
+    });
+
+    it('#resetRequestingState resets isRetrying', (done) => {
+      this.component.isRetrying = true;
+
+      this.component.resetRequestingState();
+
+      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 cancelable update triggers watcher to reset isCancelling', (done) => {
+      this.isCancelling = true;
+      this.component.$props.pipeline = Object.assign({}, pipeline, {
+        flags: { cancelable: false },
+      });
+
+      Vue.nextTick(() => {
+        expect(this.component.isCancelling).toBe(false);
+        done();
+      });
+    });
+
+    it('pipeline retryable update triggers watcher to reset isRetrying', (done) => {
+      this.isRetrying = true;
+      this.component.$props.pipeline = Object.assign({}, pipeline, {
+        flags: { retryable: false },
+      });
+
+      Vue.nextTick(() => {
+        expect(this.component.isRetrying).toBe(false);
+        done();
+      });
     });
   });
 });