diff --git a/app/assets/javascripts/ci/common/pipelines_table.vue b/app/assets/javascripts/ci/common/pipelines_table.vue
index 807128d23415a4f89e339bffc6d1b2b774a7073f..0c73c0c412fbac77865c3eae731ec39bd2482e0f 100644
--- a/app/assets/javascripts/ci/common/pipelines_table.vue
+++ b/app/assets/javascripts/ci/common/pipelines_table.vue
@@ -8,9 +8,7 @@ import { TRACKING_CATEGORIES } from '~/ci/constants';
 import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
 import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
 import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
-import eventHub from '~/ci/event_hub';
 import PipelineOperations from '../pipelines_page/components/pipeline_operations.vue';
-import PipelineStopModal from '../pipelines_page/components/pipeline_stop_modal.vue';
 import PipelineTriggerer from '../pipelines_page/components/pipeline_triggerer.vue';
 import PipelineUrl from '../pipelines_page/components/pipeline_url.vue';
 import PipelinesStatusBadge from '../pipelines_page/components/pipelines_status_badge.vue';
@@ -19,6 +17,23 @@ const HIDE_TD_ON_MOBILE = 'gl-display-none! gl-lg-display-table-cell!';
 const DEFAULT_TH_CLASSES =
   'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
 
+/**
+ * Pipelines Table
+ *
+ * Presentational component of a table of pipelines. This component does not
+ * fetch the list of pipelines and instead expects it as a prop.
+ * GraphQL actions for pipelines, such as retrying, canceling, etc.
+ * are handled within this component.
+ *
+ * Use this `legacy_pipelines_table_wrapper` if you need a fully functional REST component.
+ *
+ * IMPORTANT: When using this component, make sure to handle the following events:
+ * 1- @refresh-pipeline-table
+ * 2- @cancel-pipeline
+ * 3- @retry-pipeline
+ *
+ */
+
 export default {
   components: {
     GlTableLite,
@@ -26,7 +41,6 @@ export default {
     PipelineFailedJobsWidget,
     PipelineOperations,
     PipelinesStatusBadge,
-    PipelineStopModal,
     PipelineTriggerer,
     PipelineUrl,
   },
@@ -63,14 +77,6 @@ export default {
       required: true,
     },
   },
-  data() {
-    return {
-      pipelineId: 0,
-      pipeline: {},
-      endpoint: '',
-      cancelingPipeline: null,
-    };
-  },
   computed: {
     showFailedJobsWidget() {
       return this.glFeatures.ciJobFailuresInMr;
@@ -131,17 +137,6 @@ export default {
       return this.pipelines;
     },
   },
-  watch: {
-    pipelines() {
-      this.cancelingPipeline = null;
-    },
-  },
-  created() {
-    eventHub.$on('openConfirmationModal', this.setModalData);
-  },
-  beforeDestroy() {
-    eventHub.$off('openConfirmationModal', this.setModalData);
-  },
   methods: {
     getDownstreamPipelines(pipeline) {
       const downstream = pipeline.triggered;
@@ -153,14 +148,16 @@ export default {
     failedJobsCount(pipeline) {
       return pipeline?.failed_builds?.length || 0;
     },
-    setModalData(data) {
-      this.pipelineId = data.pipeline.id;
-      this.pipeline = data.pipeline;
-      this.endpoint = data.endpoint;
+    onRefreshPipelinesTable() {
+      this.$emit('refresh-pipelines-table');
+    },
+    onRetryPipeline(pipeline) {
+      // This emit is only used by the `legacy_pipelines_table_wrapper`.
+      this.$emit('retry-pipeline', pipeline);
     },
-    onSubmit() {
-      eventHub.$emit('postAction', this.endpoint);
-      this.cancelingPipeline = this.pipelineId;
+    onCancelPipeline(pipeline) {
+      // This emit is only used by the `legacy_pipelines_table_wrapper`.
+      this.$emit('cancel-pipeline', pipeline);
     },
     trackPipelineMiniGraph() {
       this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
@@ -219,7 +216,12 @@ export default {
       </template>
 
       <template #cell(actions)="{ item }">
-        <pipeline-operations :pipeline="item" :canceling-pipeline="cancelingPipeline" />
+        <pipeline-operations
+          :pipeline="item"
+          @cancel-pipeline="onCancelPipeline"
+          @refresh-pipelines-table="onRefreshPipelinesTable"
+          @retry-pipeline="onRetryPipeline"
+        />
       </template>
 
       <template #row-details="{ item }">
@@ -234,7 +236,5 @@ export default {
         />
       </template>
     </gl-table-lite>
-
-    <pipeline-stop-modal :pipeline="pipeline" @submit="onSubmit" />
   </div>
 </template>
diff --git a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
index 53f755fda370bd92a4c10096a63522c89812d8f7..5d1f1ac770cbf6ec72901aaeab5f127cc5a8af0f 100644
--- a/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/ci/pipeline_details/mixins/pipelines_mixin.js
@@ -52,14 +52,12 @@ export default {
     });
 
     eventHub.$on('postAction', this.postAction);
-    eventHub.$on('retryPipeline', this.postAction);
     eventHub.$on('clickedDropdown', this.updateTable);
     eventHub.$on('updateTable', this.updateTable);
     eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
   },
   beforeDestroy() {
     eventHub.$off('postAction', this.postAction);
-    eventHub.$off('retryPipeline', this.postAction);
     eventHub.$off('clickedDropdown', this.updateTable);
     eventHub.$off('updateTable', this.updateTable);
     eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
@@ -68,6 +66,15 @@ export default {
     this.poll.stop();
   },
   methods: {
+    onCancelPipeline(pipeline) {
+      this.postAction(pipeline.cancel_path);
+    },
+    onRefreshPipelinesTable() {
+      this.updateTable();
+    },
+    onRetryPipeline(pipeline) {
+      this.postAction(pipeline.retry_path);
+    },
     updateInternalState(parameters) {
       this.poll.stop();
 
diff --git a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
index 235126fea0c46b34a8c4be7bf665991a616963a3..ddcc566af1361a183ceabcf400d65d9e5eff89c0 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/nav_controls.vue
@@ -7,28 +7,25 @@ export default {
     GlButton,
   },
   props: {
-    newPipelinePath: {
+    ciLintPath: {
       type: String,
       required: false,
       default: null,
     },
-
-    resetCachePath: {
-      type: String,
+    isResetCacheButtonLoading: {
+      type: Boolean,
       required: false,
-      default: null,
+      default: false,
     },
-
-    ciLintPath: {
+    newPipelinePath: {
       type: String,
       required: false,
       default: null,
     },
-
-    isResetCacheButtonLoading: {
-      type: Boolean,
+    resetCachePath: {
+      type: String,
       required: false,
-      default: false,
+      default: null,
     },
   },
   methods: {
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
index b05bdae65c4ff617a3d35959a9777ca6fcd04274..746d605d852548949d9cd160ed004a8f6f34f6cd 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_operations.vue
@@ -1,22 +1,22 @@
 <script>
-import { GlButton, GlTooltipDirective, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
 import Tracking from '~/tracking';
 import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL, TRACKING_CATEGORIES } from '~/ci/constants';
-import eventHub from '../../event_hub';
 import PipelineMultiActions from './pipeline_multi_actions.vue';
 import PipelinesManualActions from './pipelines_manual_actions.vue';
+import PipelineStopModal from './pipeline_stop_modal.vue';
 
 export default {
   BUTTON_TOOLTIP_RETRY,
   BUTTON_TOOLTIP_CANCEL,
   directives: {
     GlTooltip: GlTooltipDirective,
-    GlModalDirective,
   },
   components: {
     GlButton,
     PipelineMultiActions,
     PipelinesManualActions,
+    PipelineStopModal,
   },
   mixins: [Tracking.mixin()],
   props: {
@@ -24,15 +24,12 @@ export default {
       type: Object,
       required: true,
     },
-    cancelingPipeline: {
-      type: Number,
-      required: false,
-      default: null,
-    },
   },
   data() {
     return {
+      isCanceling: false,
       isRetrying: false,
+      showConfirmationModal: false,
     };
   },
   computed: {
@@ -41,27 +38,36 @@ export default {
         this.pipeline?.details?.has_manual_actions || this.pipeline?.details?.has_scheduled_actions
       );
     },
-    isCancelling() {
-      return this.cancelingPipeline === this.pipeline.id;
-    },
   },
   watch: {
     pipeline() {
-      this.isRetrying = false;
+      if (this.isCanceling || this.isRetrying) {
+        this.isCanceling = false;
+        this.isRetrying = false;
+      }
     },
   },
   methods: {
+    onCloseModal() {
+      this.showConfirmationModal = false;
+    },
+    onConfirmCancelPipeline() {
+      this.isCanceling = true;
+      this.showConfirmationModal = false;
+
+      this.$emit('cancel-pipeline', this.pipeline);
+    },
     handleCancelClick() {
+      this.showConfirmationModal = true;
+
       this.trackClick('click_cancel_button');
-      eventHub.$emit('openConfirmationModal', {
-        pipeline: this.pipeline,
-        endpoint: this.pipeline.cancel_path,
-      });
     },
     handleRetryClick() {
       this.isRetrying = true;
+
       this.trackClick('click_retry_button');
-      eventHub.$emit('retryPipeline', this.pipeline.retry_path);
+
+      this.$emit('retry-pipeline', this.pipeline);
     },
     trackClick(action) {
       this.track(action, { label: TRACKING_CATEGORIES.table });
@@ -72,8 +78,19 @@ export default {
 
 <template>
   <div class="gl-text-right">
+    <pipeline-stop-modal
+      :pipeline="pipeline"
+      :show-confirmation-modal="showConfirmationModal"
+      @submit="onConfirmCancelPipeline"
+      @close-modal="onCloseModal"
+    />
+
     <div class="btn-group">
-      <pipelines-manual-actions v-if="hasActions" :iid="pipeline.iid" />
+      <pipelines-manual-actions
+        v-if="hasActions"
+        :iid="pipeline.iid"
+        @refresh-pipeline-table="$emit('refresh-pipelines-table')"
+      />
 
       <gl-button
         v-if="pipeline.flags.retryable"
@@ -94,11 +111,10 @@ export default {
       <gl-button
         v-if="pipeline.flags.cancelable"
         v-gl-tooltip.hover
-        v-gl-modal-directive="'confirmation-modal'"
         :aria-label="$options.BUTTON_TOOLTIP_CANCEL"
         :title="$options.BUTTON_TOOLTIP_CANCEL"
-        :loading="isCancelling"
-        :disabled="isCancelling"
+        :loading="isCanceling"
+        :disabled="isCanceling"
         icon="cancel"
         variant="danger"
         category="primary"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index 9f38be668f2b09ca64a966e298360a5c381cdab0..d62a68f0dcc1b2517bcd9b83dfdec75f00cd6fd7 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
@@ -7,7 +7,7 @@ import CiIcon from '~/vue_shared/components/ci_icon.vue';
 /**
  * Pipeline Stop Modal.
  *
- * Renders the modal used to confirm stopping a pipeline.
+ * Renders the modal used to confirm cancelling a pipeline.
  */
 export default {
   components: {
@@ -22,8 +22,15 @@ export default {
       required: true,
       deep: true,
     },
+    showConfirmationModal: {
+      type: Boolean,
+      required: true,
+    },
   },
   computed: {
+    hasRef() {
+      return !isEmpty(this.pipeline.ref);
+    },
     modalTitle() {
       return sprintf(
         s__('Pipeline|Stop pipeline #%{pipelineId}?'),
@@ -34,10 +41,7 @@ export default {
       );
     },
     modalText() {
-      return s__(`Pipeline|You’re about to stop pipeline #%{pipelineId}.`);
-    },
-    hasRef() {
-      return !isEmpty(this.pipeline.ref);
+      return s__(`Pipeline|You're about to stop pipeline #%{pipelineId}.`);
     },
     primaryProps() {
       return {
@@ -45,10 +49,13 @@ export default {
         attributes: { variant: 'danger' },
       };
     },
-    cancelProps() {
-      return {
-        text: __('Cancel'),
-      };
+    showModal: {
+      get() {
+        return this.showConfirmationModal;
+      },
+      set() {
+        this.$emit('close-modal');
+      },
     },
   },
   methods: {
@@ -56,14 +63,16 @@ export default {
       this.$emit('submit', event);
     },
   },
+  cancelProps: { text: __('Cancel') },
 };
 </script>
 <template>
   <gl-modal
+    v-model="showModal"
     modal-id="confirmation-modal"
     :title="modalTitle"
     :action-primary="primaryProps"
-    :action-cancel="cancelProps"
+    :action-cancel="$options.cancelProps"
     @primary="emitSubmit($event)"
   >
     <p>
@@ -74,7 +83,7 @@ export default {
       </gl-sprintf>
     </p>
 
-    <p v-if="pipeline">
+    <p>
       <ci-icon
         v-if="pipeline.details"
         :status="pipeline.details.status"
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
index 4dacd474bdec607e240c445a7d5ef10d0b62432d..ebf1744aee215944d800401e138368aec07628ee 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_manual_actions.vue
@@ -6,7 +6,6 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
 import { s__, __, sprintf } from '~/locale';
 import Tracking from '~/tracking';
 import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import eventHub from '../../event_hub';
 import { TRACKING_CATEGORIES } from '../../constants';
 import getPipelineActionsQuery from '../graphql/queries/get_pipeline_actions.query.graphql';
 
@@ -94,7 +93,7 @@ export default {
         .post(`${action.playPath}.json`)
         .then(() => {
           this.isLoading = false;
-          eventHub.$emit('updateTable');
+          this.$emit('refresh-pipeline-table');
         })
         .catch(() => {
           this.isLoading = false;
diff --git a/app/assets/javascripts/ci/pipelines_page/pipelines.vue b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
index 87ee5463bb0744029100fd086ab883b3d2c4abee..f4105040f3149f55e3097c1c1934a43ad29b5931 100644
--- a/app/assets/javascripts/ci/pipelines_page/pipelines.vue
+++ b/app/assets/javascripts/ci/pipelines_page/pipelines.vue
@@ -13,7 +13,7 @@ import {
   RAW_TEXT_WARNING,
   TRACKING_CATEGORIES,
 } from '~/ci/constants';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
 import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
 import { validateParams } from '~/ci/pipeline_details/utils';
 import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -37,7 +37,7 @@ export default {
     NavigationTabs,
     NavigationControls,
     PipelinesFilteredSearch,
-    PipelinesTableComponent,
+    PipelinesTable,
     TablePagination,
   },
   mixins: [PipelinesMixin, Tracking.mixin()],
@@ -431,12 +431,15 @@ export default {
       />
 
       <div v-else-if="stateToRender === $options.stateMap.tableList">
-        <pipelines-table-component
+        <pipelines-table
           :pipelines="state.pipelines"
           :pipeline-schedule-url="pipelineScheduleUrl"
           :update-graph-dropdown="updateGraphDropdown"
           :view-type="viewType"
           :pipeline-key-option="selectedPipelineKeyOption"
+          @cancel-pipeline="onCancelPipeline"
+          @refresh-pipelines-table="onRefreshPipelinesTable"
+          @retry-pipeline="onRetryPipeline"
         />
       </div>
 
diff --git a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
index 5e84dcbe48ee4bbec376b4ee5e7e62ce08099e80..5586032233c53318f8be5df60d34660c03b531b5 100644
--- a/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
+++ b/app/assets/javascripts/commit/pipelines/legacy_pipelines_table_wrapper.vue
@@ -2,7 +2,7 @@
 import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink, GlSprintf } from '@gitlab/ui';
 import { helpPagePath } from '~/helpers/help_page_helper';
 import { getParameterByName } from '~/lib/utils/url_utility';
-import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
 import { PipelineKeyOptions } from '~/ci/constants';
 import eventHub from '~/ci/event_hub';
 import PipelinesMixin from '~/ci/pipeline_details/mixins/pipelines_mixin';
@@ -21,7 +21,7 @@ export default {
     GlLoadingIcon,
     GlModal,
     GlSprintf,
-    PipelinesTableComponent,
+    PipelinesTable,
     TablePagination,
   },
   mixins: [PipelinesMixin, glFeatureFlagMixin()],
@@ -279,11 +279,14 @@ export default {
         {{ $options.i18n.runPipelineText }}
       </gl-button>
 
-      <pipelines-table-component
+      <pipelines-table
         :pipelines="state.pipelines"
         :update-graph-dropdown="updateGraphDropdown"
         :view-type="viewType"
         :pipeline-key-option="$options.PipelineKeyOptions[0]"
+        @cancel-pipeline="onCancelPipeline"
+        @refresh-pipelines-table="onRefreshPipelinesTable"
+        @retry-pipeline="onRetryPipeline"
       >
         <template #table-header-actions>
           <div v-if="canRenderPipelineButton" class="gl-text-right">
@@ -296,7 +299,7 @@ export default {
             </gl-button>
           </div>
         </template>
-      </pipelines-table-component>
+      </pipelines-table>
     </div>
 
     <gl-modal
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index beeb9b9ada451a60e83fc8e9ab710d5dc5ca9481..6ca59f634a2a88424fbb97d26d5ec0f4aeee71ce 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -11,7 +11,7 @@ const apolloProvider = new VueApollo({
 
 /**
  * Used in:
- *  - Project Pipelines List (projects:pipelines:index)
+ *  - Project Pipelines List (projects:pipelines)
  *  - Commit details View > Pipelines Tab > Pipelines Table (projects:commit:pipelines)
  *  - Merge request details View > Pipelines Tab > Pipelines Table (projects:merge_requests:show)
  *  - New merge request View > Pipelines Tab > Pipelines Table (projects:merge_requests:creations:new)
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 1bc67522e82828259b61a2cbe51e7a78b7877153..03518d5fdd10e46c927ff16579045c21e43db54d 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -93,7 +93,7 @@ function mountPipelines() {
   const { mrWidgetData } = gl;
   const table = new Vue({
     components: {
-      CommitPipelinesTable: () => {
+      MergeRequestPipelinesTable: () => {
         return gon.features.mrPipelinesGraphql
           ? import('~/ci/merge_requests/components/pipelines_table_wrapper.vue')
           : import('~/commit/pipelines/legacy_pipelines_table_wrapper.vue');
@@ -112,7 +112,7 @@ function mountPipelines() {
       withFailedJobsDetails: true,
     },
     render(createElement) {
-      return createElement('commit-pipelines-table', {
+      return createElement('merge-request-pipelines-table', {
         props: {
           endpoint: pipelineTableViewEl.dataset.endpoint,
           emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
@@ -347,11 +347,11 @@ export default class MergeRequestTabs {
         }
         // this.hideSidebar();
         this.resetViewContainer();
-        this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+        this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
       } else if (action === 'new') {
         this.expandView();
         this.resetViewContainer();
-        this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+        this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
       } else if (this.isDiffAction(action)) {
         if (!isInVueNoteablePage()) {
           /*
@@ -366,7 +366,7 @@ export default class MergeRequestTabs {
         }
         // this.hideSidebar();
         this.expandViewContainer();
-        this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+        this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
         this.commitsTab.classList.remove('active');
       } else if (action === 'pipelines') {
         // this.hideSidebar();
@@ -384,7 +384,7 @@ export default class MergeRequestTabs {
 
         // this.showSidebar();
         this.resetViewContainer();
-        this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
+        this.mergeRequestPipelinesTable = destroyPipelines(this.mergeRequestPipelinesTable);
       }
 
       renderGFM(document.querySelector('.detail-page-description'));
@@ -522,7 +522,7 @@ export default class MergeRequestTabs {
   }
 
   mountPipelinesView() {
-    this.commitPipelinesTable = mountPipelines();
+    this.mergeRequestPipelinesTable = mountPipelines();
   }
 
   // load the diff tab content from the backend
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 79099414dc909a8dd267940d742a8902899a7441..94383cd8b3b36038b3960e3e7fa42e00e414bfaf 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -34952,7 +34952,7 @@ msgstr ""
 msgid "Pipeline|We are currently unable to fetch pipeline data"
 msgstr ""
 
-msgid "Pipeline|You’re about to stop pipeline #%{pipelineId}."
+msgid "Pipeline|You're about to stop pipeline #%{pipelineId}."
 msgstr ""
 
 msgid "Pipeline|for"
diff --git a/spec/frontend/ci/common/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 26dd1a2fcc56ab387200ef5e907cabf58f4a7eb6..1ddeb901e59411cb910f64ade1066a33c019e6c9 100644
--- a/spec/frontend/ci/common/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -1,9 +1,7 @@
-import '~/commons';
 import { GlTableLite } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
 import fixture from 'test_fixtures/pipelines/pipelines.json';
 import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
 import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
 import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
 import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
@@ -20,14 +18,12 @@ import {
 
 import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
 
-jest.mock('~/ci/event_hub');
-
 describe('Pipelines Table', () => {
-  let pipeline;
   let wrapper;
   let trackingSpy;
 
   const defaultProvide = {
+    fullPath: '/my-project/',
     glFeatures: {},
     withFailedJobsDetails: false,
   };
@@ -39,32 +35,31 @@ describe('Pipelines Table', () => {
     withFailedJobsDetails: true,
   };
 
+  const { pipelines } = fixture;
+
   const defaultProps = {
-    pipelines: [],
+    pipelines,
     viewType: 'root',
     pipelineKeyOption: PipelineKeyOptions[0],
   };
 
-  const createMockPipeline = () => {
-    // Clone fixture as it could be modified by tests
-    const { pipelines } = JSON.parse(JSON.stringify(fixture));
-    return pipelines.find((p) => p.user !== null && p.commit !== null);
-  };
-
-  const createComponent = (props = {}, provide = {}) => {
-    wrapper = extendedWrapper(
-      mount(PipelinesTable, {
-        propsData: {
-          ...defaultProps,
-          ...props,
-        },
-        provide: {
-          ...defaultProvide,
-          ...provide,
-        },
-        stubs: ['PipelineFailedJobsWidget'],
-      }),
-    );
+  const [firstPipeline] = pipelines;
+
+  const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
+    wrapper = mountExtended(PipelinesTable, {
+      propsData: {
+        ...defaultProps,
+        ...props,
+      },
+      provide: {
+        ...defaultProvide,
+        ...provide,
+      },
+      stubs: {
+        PipelineOperations: true,
+        ...stubs,
+      },
+    });
   };
 
   const findGlTableLite = () => wrapper.findComponent(GlTableLite);
@@ -84,13 +79,9 @@ describe('Pipelines Table', () => {
   const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
   const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
 
-  beforeEach(() => {
-    pipeline = createMockPipeline();
-  });
-
   describe('Pipelines Table', () => {
     beforeEach(() => {
-      createComponent({ pipelines: [pipeline], viewType: 'root' });
+      createComponent({ props: { viewType: 'root' } });
     });
 
     it('displays table', () => {
@@ -105,7 +96,7 @@ describe('Pipelines Table', () => {
     });
 
     it('should display a table row', () => {
-      expect(findTableRows()).toHaveLength(1);
+      expect(findTableRows()).toHaveLength(pipelines.length);
     });
 
     describe('status cell', () => {
@@ -120,7 +111,7 @@ describe('Pipelines Table', () => {
       });
 
       it('should display the pipeline id', () => {
-        expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
+        expect(findPipelineInfo().text()).toContain(`#${firstPipeline.id}`);
       });
     });
 
@@ -130,24 +121,33 @@ describe('Pipelines Table', () => {
       });
 
       it('should render the right number of stages', () => {
-        const stagesLength = pipeline.details.stages.length;
-        expect(findLegacyPipelineMiniGraph().props('stages').length).toBe(stagesLength);
+        const stagesLength = firstPipeline.details.stages.length;
+        expect(findLegacyPipelineMiniGraph().props('stages')).toHaveLength(stagesLength);
       });
 
       it('should render the latest downstream pipelines only', () => {
         // component receives two downstream pipelines. one of them is already outdated
         // because we retried the trigger job, so the mini pipeline graph will only
         // render the newly created downstream pipeline instead
-        expect(pipeline.triggered).toHaveLength(2);
+        expect(firstPipeline.triggered).toHaveLength(2);
         expect(findLegacyPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
       });
 
       describe('when pipeline does not have stages', () => {
         beforeEach(() => {
-          pipeline = createMockPipeline();
-          pipeline.details.stages = [];
-
-          createComponent({ pipelines: [pipeline] });
+          createComponent({
+            props: {
+              pipelines: [
+                {
+                  ...firstPipeline,
+                  details: {
+                    ...firstPipeline.details,
+                    stages: [],
+                  },
+                },
+              ],
+            },
+          });
         });
 
         it('stages are not rendered', () => {
@@ -163,6 +163,10 @@ describe('Pipelines Table', () => {
     });
 
     describe('operations cell', () => {
+      beforeEach(() => {
+        createComponent({ stubs: { PipelineOperations } });
+      });
+
       it('should render pipeline operations', () => {
         expect(findActions().exists()).toBe(true);
       });
@@ -186,11 +190,11 @@ describe('Pipelines Table', () => {
       describe('row', () => {
         describe('when the FF is disabled', () => {
           beforeEach(() => {
-            createComponent({ pipelines: [pipeline] });
+            createComponent();
           });
 
           it('does not render', () => {
-            expect(findTableRows()).toHaveLength(1);
+            expect(findTableRows()).toHaveLength(pipelines.length);
             expect(findPipelineFailureWidget().exists()).toBe(false);
           });
         });
@@ -198,20 +202,21 @@ describe('Pipelines Table', () => {
         describe('when the FF is enabled', () => {
           describe('and `withFailedJobsDetails` value is provided', () => {
             beforeEach(() => {
-              createComponent({ pipelines: [pipeline] }, provideWithDetails);
+              createComponent({ provide: provideWithDetails });
             });
 
             it('renders', () => {
-              expect(findTableRows()).toHaveLength(2);
+              // We have 2 rows per pipeline with the widget
+              expect(findTableRows()).toHaveLength(pipelines.length * 2);
               expect(findPipelineFailureWidget().exists()).toBe(true);
             });
 
             it('passes the expected props', () => {
               expect(findPipelineFailureWidget().props()).toStrictEqual({
-                failedJobsCount: pipeline.failed_builds.length,
-                isPipelineActive: pipeline.active,
-                pipelineIid: pipeline.iid,
-                pipelinePath: pipeline.path,
+                failedJobsCount: firstPipeline.failed_builds.length,
+                isPipelineActive: firstPipeline.active,
+                pipelineIid: firstPipeline.iid,
+                pipelinePath: firstPipeline.path,
                 // Make sure the forward slash was removed
                 projectPath: 'frontend-fixtures/pipelines-project',
               });
@@ -220,14 +225,13 @@ describe('Pipelines Table', () => {
 
           describe('and `withFailedJobsDetails` value is not provided', () => {
             beforeEach(() => {
-              createComponent(
-                { pipelines: [pipeline] },
-                { glFeatures: { ciJobFailuresInMr: true } },
-              );
+              createComponent({
+                provide: { glFeatures: { ciJobFailuresInMr: true } },
+              });
             });
 
             it('does not render', () => {
-              expect(findTableRows()).toHaveLength(1);
+              expect(findTableRows()).toHaveLength(pipelines.length);
               expect(findPipelineFailureWidget().exists()).toBe(false);
             });
           });
@@ -235,35 +239,55 @@ describe('Pipelines Table', () => {
       });
     });
 
-    describe('tracking', () => {
+    describe('events', () => {
       beforeEach(() => {
-        trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+        createComponent();
       });
 
-      afterEach(() => {
-        unmockTracking();
+      describe('when confirming to cancel a pipeline', () => {
+        beforeEach(async () => {
+          await findActions().vm.$emit('cancel-pipeline', firstPipeline);
+        });
+
+        it('emits the `cancel-pipeline` event', () => {
+          expect(wrapper.emitted('cancel-pipeline')).toEqual([[firstPipeline]]);
+        });
       });
 
-      it('tracks status badge click', () => {
-        findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+      describe('when retrying a pipeline', () => {
+        beforeEach(() => {
+          findActions().vm.$emit('retry-pipeline', firstPipeline);
+        });
 
-        expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
-          label: TRACKING_CATEGORIES.table,
+        it('emits the `retry-pipeline` event', () => {
+          expect(wrapper.emitted('retry-pipeline')).toEqual([[firstPipeline]]);
         });
       });
 
-      it('tracks retry pipeline button click', () => {
-        findRetryBtn().vm.$emit('click');
+      describe('when refreshing pipelines', () => {
+        beforeEach(() => {
+          findActions().vm.$emit('refresh-pipelines-table');
+        });
 
-        expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
-          label: TRACKING_CATEGORIES.table,
+        it('emits the `refresh-pipelines-table` event', () => {
+          expect(wrapper.emitted('refresh-pipelines-table')).toEqual([[]]);
         });
       });
+    });
 
-      it('tracks cancel pipeline button click', () => {
-        findCancelBtn().vm.$emit('click');
+    describe('tracking', () => {
+      beforeEach(() => {
+        trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+      });
+
+      afterEach(() => {
+        unmockTracking();
+      });
 
-        expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
+      it('tracks status badge click', () => {
+        findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+
+        expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
           label: TRACKING_CATEGORIES.table,
         });
       });
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
index d2eab64b317a54e59adf3b7b4df94067fe57d418..6205a37e291c6606747ae11efba9bebb044d529b 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
@@ -1,10 +1,13 @@
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
 import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
 import PipelineMultiActions from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
 import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
-import eventHub from '~/ci/event_hub';
+import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
 
 describe('Pipeline operations', () => {
+  let trackingSpy;
   let wrapper;
 
   const defaultProps = {
@@ -36,6 +39,7 @@ describe('Pipeline operations', () => {
   const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
   const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
   const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
+  const findPipelineStopModal = () => wrapper.findComponent(PipelineStopModal);
 
   it('should display pipeline manual actions', () => {
     createComponent();
@@ -49,28 +53,71 @@ describe('Pipeline operations', () => {
     expect(findMultiActions().exists()).toBe(true);
   });
 
+  it('does not show the confirmation modal', () => {
+    createComponent();
+
+    expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+  });
+
+  describe('when cancelling a pipeline', () => {
+    beforeEach(async () => {
+      createComponent();
+      await findCancelBtn().vm.$emit('click');
+    });
+
+    it('should show a confirmation modal', () => {
+      expect(findPipelineStopModal().props().showConfirmationModal).toBe(true);
+    });
+
+    it('should emit cancel-pipeline event when confirming', async () => {
+      await findPipelineStopModal().vm.$emit('submit');
+
+      expect(wrapper.emitted('cancel-pipeline')).toEqual([[defaultProps.pipeline]]);
+      expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+    });
+
+    it('should hide the modal when closing', async () => {
+      await findPipelineStopModal().vm.$emit('close-modal');
+
+      expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+    });
+  });
+
   describe('events', () => {
     beforeEach(() => {
       createComponent();
-
-      jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
     });
 
     it('should emit retryPipeline event', () => {
       findRetryBtn().vm.$emit('click');
 
-      expect(eventHub.$emit).toHaveBeenCalledWith(
-        'retryPipeline',
-        defaultProps.pipeline.retry_path,
-      );
+      expect(wrapper.emitted('retry-pipeline')).toEqual([[defaultProps.pipeline]]);
+    });
+  });
+
+  describe('tracking', () => {
+    beforeEach(() => {
+      createComponent();
+      trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+    });
+
+    afterEach(() => {
+      unmockTracking();
+    });
+
+    it('tracks retry pipeline button click', () => {
+      findRetryBtn().vm.$emit('click');
+
+      expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
+        label: TRACKING_CATEGORIES.table,
+      });
     });
 
-    it('should emit openConfirmationModal event', () => {
+    it('tracks cancel pipeline button click', () => {
       findCancelBtn().vm.$emit('click');
 
-      expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
-        pipeline: defaultProps.pipeline,
-        endpoint: defaultProps.pipeline.cancel_path,
+      expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
+        label: TRACKING_CATEGORIES.table,
       });
     });
   });
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
index 4d78a92354238361a20c4bc79681c087c2d80a60..1e276840c07e72ef3926610725aaf6308e7fef3a 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
@@ -1,15 +1,17 @@
 import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
 import { mockPipelineHeader } from 'jest/ci/pipeline_details/mock_data';
 import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
 
 describe('PipelineStopModal', () => {
   let wrapper;
 
-  const createComponent = () => {
+  const createComponent = ({ props = {} } = {}) => {
     wrapper = shallowMount(PipelineStopModal, {
       propsData: {
         pipeline: mockPipelineHeader,
+        showConfirmationModal: false,
+        ...props,
       },
       stubs: {
         GlSprintf,
@@ -17,11 +19,43 @@ describe('PipelineStopModal', () => {
     });
   };
 
+  const findModal = () => wrapper.findComponent(GlModal);
+
   beforeEach(() => {
     createComponent();
   });
 
-  it('should render "stop pipeline" warning', () => {
-    expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`);
+  describe('when `showConfirmationModal` is false', () => {
+    it('passes the visiblity value to the modal', () => {
+      expect(findModal().props().visible).toBe(false);
+    });
+  });
+
+  describe('when `showConfirmationModal` is true', () => {
+    beforeEach(() => {
+      createComponent({ props: { showConfirmationModal: true } });
+    });
+
+    it('passes the visiblity value to the modal', () => {
+      expect(findModal().props().visible).toBe(true);
+    });
+
+    it('renders "stop pipeline" warning', () => {
+      expect(wrapper.text()).toMatch(`You're about to stop pipeline #${mockPipelineHeader.id}.`);
+    });
+  });
+
+  describe('events', () => {
+    beforeEach(() => {
+      createComponent({ props: { showConfirmationModal: true } });
+    });
+
+    it('emits the close-modal event when the visiblity changes', async () => {
+      expect(wrapper.emitted('close-modal')).toBeUndefined();
+
+      await findModal().vm.$emit('change', false);
+
+      expect(wrapper.emitted('close-modal')).toEqual([[]]);
+    });
   });
 });
diff --git a/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
index 4af292e35880ce57d7c64a74abc5ceb5895e4ea6..d58b139dae35a54cdc16f00ed71829c31e4a1964 100644
--- a/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
+++ b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
@@ -1,13 +1,13 @@
 import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
 import MockAdapter from 'axios-mock-adapter';
 import { nextTick } from 'vue';
 import fixture from 'test_fixtures/pipelines/pipelines.json';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
 import { stubComponent } from 'helpers/stub_component';
 import waitForPromises from 'helpers/wait_for_promises';
 import Api from '~/api';
-import LegacyPipelinesTableWraper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
+import LegacyPipelinesTableWrapper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
 import {
   HTTP_STATUS_BAD_REQUEST,
   HTTP_STATUS_INTERNAL_SERVER_ERROR,
@@ -39,27 +39,26 @@ describe('Pipelines table in Commits and Merge requests', () => {
   const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
   const findModal = () => wrapper.findComponent(GlModal);
   const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
-
-  const createComponent = ({ props = {} } = {}) => {
-    wrapper = extendedWrapper(
-      mount(LegacyPipelinesTableWraper, {
-        propsData: {
-          endpoint: 'endpoint.json',
-          emptyStateSvgPath: 'foo',
-          errorStateSvgPath: 'foo',
-          ...props,
-        },
-        mocks: {
-          $toast,
-        },
-        stubs: {
-          GlModal: stubComponent(GlModal, {
-            template: '<div />',
-            methods: { show: showMock },
-          }),
-        },
-      }),
-    );
+  const findPipelinesTable = () => wrapper.findComponent(PipelinesTable);
+
+  const createComponent = ({ props = {}, mountFn = mountExtended } = {}) => {
+    wrapper = mountFn(LegacyPipelinesTableWrapper, {
+      propsData: {
+        endpoint: 'endpoint.json',
+        emptyStateSvgPath: 'foo',
+        errorStateSvgPath: 'foo',
+        ...props,
+      },
+      mocks: {
+        $toast,
+      },
+      stubs: {
+        GlModal: stubComponent(GlModal, {
+          template: '<div />',
+          methods: { show: showMock },
+        }),
+      },
+    });
   };
 
   beforeEach(() => {
@@ -116,7 +115,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
 
       it('should make an API request when using pagination', async () => {
         expect(mock.history.get).toHaveLength(1);
-        expect(mock.history.get[0].params.page).toBe('1');
 
         wrapper.find('.next-page-item').trigger('click');
 
@@ -359,4 +357,53 @@ describe('Pipelines table in Commits and Merge requests', () => {
       );
     });
   });
+
+  describe('events', () => {
+    beforeEach(async () => {
+      mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline]);
+
+      createComponent({ mountFn: shallowMountExtended });
+
+      await waitForPromises();
+    });
+
+    describe('When cancelling a pipeline', () => {
+      it('sends the cancel action', async () => {
+        expect(mock.history.post).toHaveLength(0);
+
+        findPipelinesTable().vm.$emit('cancel-pipeline', pipeline);
+
+        await waitForPromises();
+
+        expect(mock.history.post).toHaveLength(1);
+        expect(mock.history.post[0].url).toContain('cancel.json');
+      });
+    });
+
+    describe('When retrying a pipeline', () => {
+      it('sends the retry action', async () => {
+        expect(mock.history.post).toHaveLength(0);
+
+        findPipelinesTable().vm.$emit('retry-pipeline', pipeline);
+
+        await waitForPromises();
+
+        expect(mock.history.post).toHaveLength(1);
+        expect(mock.history.post[0].url).toContain('retry.json');
+      });
+    });
+
+    describe('When refreshing a pipeline', () => {
+      it('calls the pipelines endpoint again', async () => {
+        expect(mock.history.get).toHaveLength(1);
+
+        findPipelinesTable().vm.$emit('refresh-pipelines-table');
+
+        await waitForPromises();
+
+        expect(mock.history.get).toHaveLength(2);
+        expect(mock.history.get[1].url).toContain('endpoint.json');
+      });
+    });
+  });
 });