diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
index f42152006d25222eeec77720dfc191c5af893066..a44a5b30e1e4e2b7bf357143f6f5cc1faa4dc706 100644
--- a/app/assets/javascripts/google_tag_manager/index.js
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -232,35 +232,40 @@ export const trackTransaction = (transactionDetails) => {
   pushEnhancedEcommerceEvent('EECtransactionSuccess', eventData);
 };
 
-export const trackAddToCartUsageTab = () => {
+export const pushEECproductAddToCartEvent = () => {
   if (!isSupported()) {
     return;
   }
 
-  const getStartedButton = document.querySelector('.js-buy-additional-minutes');
-  getStartedButton.addEventListener('click', () => {
-    window.dataLayer.push({
-      event: 'EECproductAddToCart',
-      ecommerce: {
-        currencyCode: 'USD',
-        add: {
-          products: [
-            {
-              name: 'CI/CD Minutes',
-              id: '0003',
-              price: '10',
-              brand: 'GitLab',
-              category: 'DevOps',
-              variant: 'add-on',
-              quantity: 1,
-            },
-          ],
-        },
+  window.dataLayer.push({
+    event: 'EECproductAddToCart',
+    ecommerce: {
+      currencyCode: 'USD',
+      add: {
+        products: [
+          {
+            name: 'CI/CD Minutes',
+            id: '0003',
+            price: '10',
+            brand: 'GitLab',
+            category: 'DevOps',
+            variant: 'add-on',
+            quantity: 1,
+          },
+        ],
       },
-    });
+    },
   });
 };
 
+export const trackAddToCartUsageTab = () => {
+  const getStartedButton = document.querySelector('.js-buy-additional-minutes');
+  if (!getStartedButton) {
+    return;
+  }
+  getStartedButton.addEventListener('click', pushEECproductAddToCartEvent);
+};
+
 export const trackCombinedGroupProjectForm = () => {
   if (!isSupported()) {
     return;
diff --git a/app/helpers/namespaces_helper.rb b/app/helpers/namespaces_helper.rb
index 64b58d28fc97426fb49ec99592470905b7ceb17f..cf386ee398ae753e7bd00f2001b40d1c97f40c50 100644
--- a/app/helpers/namespaces_helper.rb
+++ b/app/helpers/namespaces_helper.rb
@@ -88,6 +88,15 @@ def namespaces_as_json(selected = :current_user)
     }.to_json
   end
 
+  def pipeline_usage_quota_app_data(namespace)
+    {
+      namespace_actual_plan_name: namespace.actual_plan_name,
+      namespace_path: namespace.full_path,
+      namespace_id: namespace.id,
+      page_size: page_size
+    }
+  end
+
   private
 
   # Many importers create a temporary Group, so use the real
diff --git a/ee/app/assets/javascripts/usage_quotas/pipelines/components/app.vue b/ee/app/assets/javascripts/usage_quotas/pipelines/components/app.vue
new file mode 100644
index 0000000000000000000000000000000000000000..4b310610f577a3e00e683ef2c2e32b1a0ae8f150
--- /dev/null
+++ b/ee/app/assets/javascripts/usage_quotas/pipelines/components/app.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { pushEECproductAddToCartEvent } from '~/google_tag_manager';
+import { LABEL_BUY_ADDITIONAL_MINUTES } from '../constants';
+
+export default {
+  name: 'PipelineUsageApp',
+  components: { GlButton },
+  inject: ['namespaceActualPlanName', 'buyAdditionalMinutesPath', 'buyAdditionalMinutesTarget'],
+  methods: {
+    trackBuyAdditionalMinutesClick() {
+      pushEECproductAddToCartEvent();
+    },
+  },
+  LABEL_BUY_ADDITIONAL_MINUTES,
+};
+</script>
+
+<template>
+  <div>
+    <div
+      v-if="buyAdditionalMinutesPath && buyAdditionalMinutesTarget"
+      class="gl-display-flex gl-justify-content-end"
+    >
+      <gl-button
+        :href="buyAdditionalMinutesPath"
+        :target="buyAdditionalMinutesTarget"
+        :data-track-label="namespaceActualPlanName"
+        data-track-action="click_buy_ci_minutes"
+        data-track-property="pipeline_quota_page"
+        data-testid="buy-additional-minutes-button"
+        category="primary"
+        variant="confirm"
+        @click="trackBuyAdditionalMinutesClick"
+      >
+        {{ $options.LABEL_BUY_ADDITIONAL_MINUTES }}
+      </gl-button>
+    </div>
+  </div>
+</template>
diff --git a/ee/app/assets/javascripts/usage_quotas/pipelines/constants.js b/ee/app/assets/javascripts/usage_quotas/pipelines/constants.js
new file mode 100644
index 0000000000000000000000000000000000000000..a4fa01c9520fa4d90ef83fa668b32a4fe87e7710
--- /dev/null
+++ b/ee/app/assets/javascripts/usage_quotas/pipelines/constants.js
@@ -0,0 +1,3 @@
+import { s__ } from '~/locale';
+
+export const LABEL_BUY_ADDITIONAL_MINUTES = s__('UsageQuota|Buy additional minutes');
diff --git a/ee/app/assets/javascripts/usage_quotas/pipelines/index.js b/ee/app/assets/javascripts/usage_quotas/pipelines/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..7b1c880c9f59f89c71828a918c1d0ef428d1e6a4
--- /dev/null
+++ b/ee/app/assets/javascripts/usage_quotas/pipelines/index.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import PipelineUsageApp from './components/app.vue';
+
+export default () => {
+  const el = document.getElementById('js-pipeline-usage-app');
+
+  if (!el) {
+    return false;
+  }
+
+  const {
+    namespaceActualPlanName,
+    buyAdditionalMinutesPath,
+    buyAdditionalMinutesTarget,
+  } = el.dataset;
+
+  return new Vue({
+    el,
+    name: 'PipelinesUsageView',
+    provide: {
+      namespaceActualPlanName,
+      buyAdditionalMinutesPath,
+      buyAdditionalMinutesTarget,
+    },
+    render(createElement) {
+      return createElement(PipelineUsageApp);
+    },
+  });
+};
diff --git a/ee/app/helpers/ee/namespaces_helper.rb b/ee/app/helpers/ee/namespaces_helper.rb
index 04deff086296fdfeafe84ad4b8fa8c25e9540c7c..c311eedebd75418a6cf3f9fda3a632dfcff8356f 100644
--- a/ee/app/helpers/ee/namespaces_helper.rb
+++ b/ee/app/helpers/ee/namespaces_helper.rb
@@ -67,6 +67,16 @@ def show_minute_limit_banner?(namespace)
       namespace.root_ancestor.free_plan? && !minute_limit_banner_dismissed?
     end
 
+    override :pipeline_usage_quota_app_data
+    def pipeline_usage_quota_app_data(namespace)
+      return super unless ::Gitlab::CurrentSettings.should_check_namespace_plan?
+
+      super.merge(
+        buy_additional_minutes_path: buy_additional_minutes_path(namespace),
+        buy_additional_minutes_target: buy_addon_target_attr(namespace)
+      )
+    end
+
     private
 
     def use_customers_dot_for_addon_path?(namespace)
diff --git a/ee/spec/frontend/usage_quotas/pipelines/components/app_spec.js b/ee/spec/frontend/usage_quotas/pipelines/components/app_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..0f9e2e0a1ee6e032944bdcbfd1a9d908b22d5f9a
--- /dev/null
+++ b/ee/spec/frontend/usage_quotas/pipelines/components/app_spec.js
@@ -0,0 +1,64 @@
+import { GlButton } from '@gitlab/ui';
+import { pushEECproductAddToCartEvent } from '~/google_tag_manager';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import PipelineUsageApp from 'ee/usage_quotas/pipelines/components/app.vue';
+import { LABEL_BUY_ADDITIONAL_MINUTES } from 'ee/usage_quotas/pipelines/constants';
+import { defaultProvide } from '../mock_data';
+
+jest.mock('~/google_tag_manager');
+
+describe('PipelineUsageApp', () => {
+  let wrapper;
+
+  const findBuyAdditionalMinutesButton = () =>
+    wrapper.findByTestId('buy-additional-minutes-button');
+
+  const createComponent = ({ provide = {} } = {}) => {
+    wrapper = shallowMountExtended(PipelineUsageApp, {
+      provide: {
+        ...defaultProvide,
+        ...provide,
+      },
+      stubs: {
+        GlButton,
+      },
+    });
+  };
+
+  beforeEach(() => {
+    createComponent();
+  });
+
+  afterEach(() => {
+    wrapper.destroy();
+  });
+
+  describe('Buy additional minutes Button', () => {
+    it('calls pushEECproductAddToCartEvent on click', async () => {
+      findBuyAdditionalMinutesButton().trigger('click');
+      expect(pushEECproductAddToCartEvent).toHaveBeenCalledTimes(1);
+    });
+
+    describe('Gitlab SaaS: valid data for buyAdditionalMinutesPath and buyAdditionalMinutesTarget', () => {
+      it('renders the button to buy additional minutes', () => {
+        expect(findBuyAdditionalMinutesButton().exists()).toBe(true);
+        expect(findBuyAdditionalMinutesButton().text()).toBe(LABEL_BUY_ADDITIONAL_MINUTES);
+      });
+    });
+
+    describe('Gitlab Self-Managed: buyAdditionalMinutesPath and buyAdditionalMinutesTarget not provided', () => {
+      beforeEach(() => {
+        createComponent({
+          provide: {
+            buyAdditionalMinutesPath: undefined,
+            buyAdditionalMinutesTarget: undefined,
+          },
+        });
+      });
+
+      it('does not render the button to buy additional minutes', () => {
+        expect(findBuyAdditionalMinutesButton().exists()).toBe(false);
+      });
+    });
+  });
+});
diff --git a/ee/spec/frontend/usage_quotas/pipelines/mock_data.js b/ee/spec/frontend/usage_quotas/pipelines/mock_data.js
new file mode 100644
index 0000000000000000000000000000000000000000..2adce845dafb139647e9d9829e8f8843751baae4
--- /dev/null
+++ b/ee/spec/frontend/usage_quotas/pipelines/mock_data.js
@@ -0,0 +1,7 @@
+import { TEST_HOST } from 'helpers/test_constants';
+
+export const defaultProvide = {
+  namespaceActualPlanName: 'MyGroup',
+  buyAdditionalMinutesPath: `${TEST_HOST}/-/subscriptions/buy_minutes?selected_group=12345`,
+  buyAdditionalMinutesTarget: '_self',
+};
diff --git a/ee/spec/helpers/ee/namespaces_helper_spec.rb b/ee/spec/helpers/ee/namespaces_helper_spec.rb
index 68ee1f0ae54ef9508372a270239c9475d532c791..dd82c958aa873348ceacba92a21c9a33be1ce511 100644
--- a/ee/spec/helpers/ee/namespaces_helper_spec.rb
+++ b/ee/spec/helpers/ee/namespaces_helper_spec.rb
@@ -264,4 +264,34 @@
       end
     end
   end
+
+  describe '#pipeline_usage_quota_app_data' do
+    context 'Gitlab SaaS', :saas do
+      before do
+        stub_ee_application_setting(should_check_namespace_plan: true)
+      end
+
+      it 'returns a hash with buy_additional_minutes data' do
+        expect(helper.pipeline_usage_quota_app_data(user_group)).to eql({
+          namespace_actual_plan_name: user_group.actual_plan_name,
+          namespace_path: user_group.full_path,
+          namespace_id: user_group.id,
+          page_size: Kaminari.config.default_per_page,
+          buy_additional_minutes_path: EE::SUBSCRIPTIONS_MORE_MINUTES_URL,
+          buy_additional_minutes_target: '_blank'
+        })
+      end
+    end
+
+    context 'Gitlab Self-Managed' do
+      it 'returns a hash without buy_additional_minutes data' do
+        expect(helper.pipeline_usage_quota_app_data(user_group)).to eql({
+          namespace_actual_plan_name: user_group.actual_plan_name,
+          namespace_path: user_group.full_path,
+          namespace_id: user_group.id,
+          page_size: Kaminari.config.default_per_page
+        })
+      end
+    end
+  end
 end
diff --git a/spec/helpers/namespaces_helper_spec.rb b/spec/helpers/namespaces_helper_spec.rb
index 00aa0fd1cbaef3d714d0c371d29d83ef97caba66..52c1130e8183edccec1284f8ecee18a74f120f37 100644
--- a/spec/helpers/namespaces_helper_spec.rb
+++ b/spec/helpers/namespaces_helper_spec.rb
@@ -268,4 +268,15 @@
       end
     end
   end
+
+  describe '#pipeline_usage_quota_app_data' do
+    it 'returns a hash with necessary data for the frontend' do
+      expect(helper.pipeline_usage_quota_app_data(user_group)).to eql({
+        namespace_actual_plan_name: user_group.actual_plan_name,
+        namespace_path: user_group.full_path,
+        namespace_id: user_group.id,
+        page_size: Kaminari.config.default_per_page
+      })
+    end
+  end
 end