diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js index 1678991b1eaa3d6abcfc5310a2e28871ed942403..67b068f1c6b07e84c86f0ba0f72f2e2e06ae3d24 100644 --- a/app/assets/javascripts/header.js +++ b/app/assets/javascripts/header.js @@ -74,20 +74,27 @@ function initStatusTriggers() { } } +function trackShowUserDropdownLink(trackEvent, elToTrack, el) { + const { trackLabel, trackProperty } = elToTrack.dataset; + + $(el).on('shown.bs.dropdown', () => { + Tracking.event(document.body.dataset.page, trackEvent, { + label: trackLabel, + property: trackProperty, + }); + }); +} export function initNavUserDropdownTracking() { const el = document.querySelector('.js-nav-user-dropdown'); const buyEl = document.querySelector('.js-buy-ci-minutes-link'); + const upgradeEl = document.querySelector('.js-upgrade-plan-link'); if (el && buyEl) { - const { trackLabel, trackProperty } = buyEl.dataset; - const trackEvent = 'show_buy_ci_minutes'; + trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el); + } - $(el).on('shown.bs.dropdown', () => { - Tracking.event(undefined, trackEvent, { - label: trackLabel, - property: trackProperty, - }); - }); + if (el && upgradeEl) { + trackShowUserDropdownLink('show_upgrade_link', upgradeEl, el); } } diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss index a0a020ec548435a43653b24e77a28b91a5799b38..2c7e9428ef1a40d48750e3b70df8b660d910eb5d 100644 --- a/app/assets/stylesheets/framework/header.scss +++ b/app/assets/stylesheets/framework/header.scss @@ -553,6 +553,7 @@ vertical-align: text-top; } + a.upgrade-plan-link gl-emoji, a.ci-minutes-emoji gl-emoji, a.trial-link gl-emoji { font-size: $gl-font-size; diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml index 410b120396db3b310e60dc63280c1f300ebc21f2..7d9924719a2b231a1887505c5b384a491f4ed0d1 100644 --- a/app/views/layouts/header/_current_user_dropdown.html.haml +++ b/app/views/layouts/header/_current_user_dropdown.html.haml @@ -27,6 +27,7 @@ %li = link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' } = render_if_exists 'layouts/header/buy_ci_minutes', project: @project, namespace: @group + = render_if_exists 'layouts/header/upgrade' - if current_user_menu?(:help) %li.divider.d-md-none diff --git a/ee/app/helpers/ee/users_helper.rb b/ee/app/helpers/ee/users_helper.rb index 763577bdfcfe0e2206c5d4b0c23e1d52d7ee1c2c..7631b3a162ee32a9d5e6a969d75e52ce52b0da5f 100644 --- a/ee/app/helpers/ee/users_helper.rb +++ b/ee/app/helpers/ee/users_helper.rb @@ -20,6 +20,16 @@ def user_badges_in_admin_section(user) end end + def show_upgrade_link?(user) + return unless user + return unless ::Gitlab.com? + return unless experiment_enabled?(:upgrade_link_in_user_menu_a) + + Rails.cache.fetch(['users', user.id, 'show_upgrade_link?'], expires_in: 10.minutes) do + user.owns_upgradeable_namespace? + end + end + private def trials_allowed?(user) diff --git a/ee/app/models/ee/user.rb b/ee/app/models/ee/user.rb index e85d2892e2c024c25ceaedc248e2f3f90f8317cb..e8b290ba60df558a422110defd1cec7e410639da 100644 --- a/ee/app/models/ee/user.rb +++ b/ee/app/models/ee/user.rb @@ -243,17 +243,17 @@ def has_paid_namespace? ::Namespace .from("(#{namespace_union_for_reporter_developer_maintainer_owned}) #{::Namespace.table_name}") .include_gitlab_subscription - .where(gitlab_subscriptions: { hosted_plan: ::Plan.where(name: Plan::PAID_HOSTED_PLANS) }) + .where(gitlab_subscriptions: { hosted_plan: ::Plan.where(name: ::Plan::PAID_HOSTED_PLANS) }) .any? end # Returns true if the user is an Owner on any namespace currently on # a paid plan - def owns_paid_namespace? + def owns_paid_namespace?(plans: ::Plan::PAID_HOSTED_PLANS) ::Namespace .from("(#{namespace_union_for_owned}) #{::Namespace.table_name}") .include_gitlab_subscription - .where(gitlab_subscriptions: { hosted_plan: ::Plan.where(name: Plan::PAID_HOSTED_PLANS) }) + .where(gitlab_subscriptions: { hosted_plan: ::Plan.where(name: plans) }) .any? end @@ -363,6 +363,11 @@ def security_dashboard InstanceSecurityDashboard.new(self) end + def owns_upgradeable_namespace? + !owns_paid_namespace?(plans: [::Plan::GOLD]) && + owns_paid_namespace?(plans: [::Plan::BRONZE, ::Plan::SILVER]) + end + protected override :password_required? diff --git a/ee/app/views/layouts/header/_upgrade.html.haml b/ee/app/views/layouts/header/_upgrade.html.haml new file mode 100644 index 0000000000000000000000000000000000000000..356caa72d5f9aaa108d6b84dda80eabb2377d56e --- /dev/null +++ b/ee/app/views/layouts/header/_upgrade.html.haml @@ -0,0 +1,7 @@ +- if show_upgrade_link?(current_user) + %li + = link_to EE::SUBSCRIPTIONS_PLANS_URL, + class: 'upgrade-plan-link js-upgrade-plan-link', + data: { 'track-event': 'click_upgrade_link', 'track-label': current_user.namespace.actual_plan_name, 'track-property': 'user_dropdown' } do + = s_("CurrentUser|Upgrade") + = emoji_icon('rocket', 'aria-hidden': true) diff --git a/ee/spec/models/user_spec.rb b/ee/spec/models/user_spec.rb index accec07137676c00f0c575c83469b40790fd7929..ca3ab2cd152d14d01599bafb27f35174301047cf 100644 --- a/ee/spec/models/user_spec.rb +++ b/ee/spec/models/user_spec.rb @@ -1204,4 +1204,49 @@ expect(security_dashboard).to be_a(InstanceSecurityDashboard) end end + + describe '#owns_upgradeable_namespace?' do + let_it_be(:user) { create(:user) } + + subject { user.owns_upgradeable_namespace? } + + using RSpec::Parameterized::TableSyntax + + where(:hosted_plan, :result) do + :bronze_plan | true + :silver_plan | true + :gold_plan | false + :free_plan | false + :default_plan | false + end + + with_them do + it 'returns the correct result for each plan on a personal namespace' do + plan = create(hosted_plan) + create(:gitlab_subscription, namespace: user.namespace, hosted_plan: plan) + + expect(subject).to be result + end + + it 'returns the correct result for each plan on a group owned by the user' do + create(:group_with_plan, plan: hosted_plan).add_owner(user) + + expect(subject).to be result + end + end + + it 'returns false when there is no subscription for the personal namespace' do + expect(subject).to be false + end + + it 'returns false when the user has multiple groups and any group has gold' do + create(:group_with_plan, plan: :bronze_plan).add_owner(user) + create(:group_with_plan, plan: :silver_plan).add_owner(user) + create(:group_with_plan, plan: :gold_plan).add_owner(user) + + user.namespace.plans.reload + + expect(subject).to be false + end + end end diff --git a/ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb b/ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb index 2debe4d7abb2adc57cad099039639f8d82964787..f6bd8ca6a37ed7f752e122fc00146a3b9494f166 100644 --- a/ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb +++ b/ee/spec/views/layouts/header/_current_user_dropdown.html.haml_spec.rb @@ -4,31 +4,63 @@ describe 'layouts/header/_current_user_dropdown' do let_it_be(:user) { create(:user) } - let(:need_minutes) { true } - before do - allow(view).to receive(:current_user).and_return(user) - allow(view).to receive(:show_buy_ci_minutes?).and_return(need_minutes) + describe 'Buy CI Minutes link in user dropdown' do + let(:need_minutes) { true } - render - end + before do + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:show_upgrade_link?).and_return(false) + allow(view).to receive(:show_buy_ci_minutes?).and_return(need_minutes) + + render + end - subject { rendered } + subject { rendered } - context 'when ci minutes need bought' do - it 'has "Buy CI minutes" link with correct data properties', :aggregate_failures do - expect(subject).to have_selector('[data-track-event="click_buy_ci_minutes"]') - expect(subject).to have_selector("[data-track-label='#{user.namespace.actual_plan_name}']") - expect(subject).to have_selector('[data-track-property="user_dropdown"]') - expect(subject).to have_link('Buy CI minutes') + context 'when ci minutes need bought' do + it 'has "Buy CI minutes" link with correct data properties', :aggregate_failures do + expect(subject).to have_selector('[data-track-event="click_buy_ci_minutes"]') + expect(subject).to have_selector("[data-track-label='#{user.namespace.actual_plan_name}']") + expect(subject).to have_selector('[data-track-property="user_dropdown"]') + expect(subject).to have_link('Buy CI minutes') + end + end + + context 'when ci minutes do not need bought' do + let(:need_minutes) { false } + + it 'has no "Buy CI minutes" link' do + expect(subject).not_to have_link('Buy CI minutes') + end end end - context 'when ci minutes do not need bought' do - let(:need_minutes) { false } + describe 'Upgrade link in user dropdown' do + let(:on_upgradeable_plan) { true } + + before do + allow(view).to receive(:current_user).and_return(user) + allow(view).to receive(:show_buy_ci_minutes?).and_return(false) + allow(view).to receive(:show_upgrade_link?).and_return(on_upgradeable_plan) + + render + end + + subject { rendered } + + context 'when user is on an upgradeable plan' do + it 'displays the Upgrade link' do + expect(subject).to have_link('Upgrade') + end + end + + context 'when user is not on an upgradeable plan' do + let(:on_upgradeable_plan) { false } - it 'has no "Buy CI minutes" link' do - expect(subject).not_to have_link('Buy CI minutes') + it 'does not display the Upgrade link' do + expect(subject).not_to have_link('Upgrade') + end end end end diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index 0097961eed4515ee09af375a7827232b901920ab..3495b4a0b725f47201a57894b46c900b6ea749fc 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -42,6 +42,9 @@ module Experimentation }, buy_ci_minutes_version_a: { tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA' + }, + upgrade_link_in_user_menu_a: { + tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA' } }.freeze diff --git a/locale/gitlab.pot b/locale/gitlab.pot index ae4c454208be146d0894abaac3edd90c578c8979..50c131c991b45514a75ea7a27b7a72ecbfe09bda 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -6477,6 +6477,9 @@ msgstr "" msgid "CurrentUser|Start a Gold trial" msgstr "" +msgid "CurrentUser|Upgrade" +msgstr "" + msgid "Custom CI configuration path" msgstr "" diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js index 0a74799283a1627c6c2202a4d2b02b775aacceab..6d2d7976196afe3ed4c1070ac2f127c075acc9eb 100644 --- a/spec/frontend/header_spec.js +++ b/spec/frontend/header_spec.js @@ -60,8 +60,8 @@ describe('Header', () => { beforeEach(() => { setFixtures(` <li class="js-nav-user-dropdown"> - <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes - </a> + <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes</a> + <a class="js-upgrade-plan-link" data-track-event="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a> </li>`); trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn); @@ -77,8 +77,16 @@ describe('Header', () => { it('sends a tracking event when the dropdown is opened and contains Buy CI minutes link', () => { $('.js-nav-user-dropdown').trigger('shown.bs.dropdown'); - expect(trackingSpy).toHaveBeenCalledTimes(1); - expect(trackingSpy).toHaveBeenCalledWith(undefined, 'show_buy_ci_minutes', { + expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_buy_ci_minutes', { + label: 'free', + property: 'user_dropdown', + }); + }); + + it('sends a tracking event when the dropdown is opened and contains Upgrade link', () => { + $('.js-nav-user-dropdown').trigger('shown.bs.dropdown'); + + expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_upgrade_link', { label: 'free', property: 'user_dropdown', });