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