Commit 048aa1c7 authored by Doug Stull's avatar Doug Stull Committed by Dmytro Zaporozhets

Add logic to show buy ci minutes link

- needed for experiment
parent 20d1bda1
......@@ -26,7 +26,7 @@
- if current_user_menu?(:settings)
%li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
= render_if_exists 'layouts/header/buy_ci_minutes'
= render_if_exists 'layouts/header/buy_ci_minutes', project: @project, namespace: @group
- if current_user_menu?(:help)
%li.divider.d-md-none
......
# frozen_string_literal: true
module EE
module RunnersHelper
include ::Gitlab::Utils::StrongMemoize
def ci_usage_warning_message(namespace, project)
message = [ci_usage_base_message(namespace)]
......@@ -15,20 +17,30 @@ module EE
message.join(' ').html_safe
end
def show_buy_ci_minutes?
experiment_enabled?(:buy_ci_minutes_version_a)
def show_buy_ci_minutes?(project, namespace)
return false unless experiment_enabled?(:ci_notification_dot) || experiment_enabled?(:buy_ci_minutes_version_a)
show_out_of_ci_minutes_notification?(project, namespace)
end
def show_user_notification_dot?(project, namespace)
def show_ci_minutes_notification_dot?(project, namespace)
return false unless experiment_enabled?(:ci_notification_dot)
return false unless project&.persisted? || namespace&.persisted?
::Ci::MinutesNotificationService.call(current_user, project, namespace).show_notification? &&
current_user.pipelines.any?
show_out_of_ci_minutes_notification?(project, namespace)
end
private
def show_out_of_ci_minutes_notification?(project, namespace)
strong_memoize(:show_out_of_ci_minutes_notification) do
next unless project&.persisted? || namespace&.persisted?
context = ::Ci::Minutes::Context.new(project, namespace)
threshold = ::Ci::Minutes::Threshold.new(current_user, context.level)
threshold.warning_reached? && context.namespace.all_pipelines.for_user(current_user).any?
end
end
def purchase_shared_runner_minutes_link
link = link_to(_("Click here"), EE::SUBSCRIPTIONS_PLANS_URL, target: '_blank', rel: 'noopener')
......
# frozen_string_literal: true
module Ci
module Minutes
class Context
attr_reader :namespace, :level
delegate :full_path, to: :level
def initialize(project, namespace)
@project = project
@namespace = project&.shared_runners_limit_namespace || namespace
@level = project || namespace
end
private
attr_reader :project
end
end
end
# frozen_string_literal: true
module Ci
module Minutes
class Threshold
include Gitlab::Allowable
def initialize(user, context_level)
@context_level = context_level
@user = user
end
def warning_reached?
show_limit? && context_level.shared_runners_remaining_minutes_below_threshold?
end
def alert_reached?
show_limit? && context_level.shared_runners_minutes_used?
end
private
attr_reader :user, :context_level
def show_limit?
context_level.shared_runners_minutes_limit_enabled? && can_see_status?
end
def can_see_status?
context_level.is_a?(Namespace) || can?(user, :create_pipeline, context_level)
end
end
end
end
......@@ -155,7 +155,8 @@ module EE
to: :statistics, allow_nil: true
delegate :actual_shared_runners_minutes_limit,
:shared_runners_minutes_used?, to: :shared_runners_limit_namespace
:shared_runners_minutes_used?,
:shared_runners_remaining_minutes_below_threshold?, to: :shared_runners_limit_namespace
delegate :last_update_succeeded?, :last_update_failed?,
:ever_updated_successfully?, :hard_failed?,
......
# frozen_string_literal: true
module Ci
class MinutesNotificationService
include Gitlab::Allowable
attr_reader :namespace
def self.call(*args)
new(*args).call
end
def initialize(current_user, project, namespace)
@current_user = current_user
@project = project
@namespace = namespace
end
def call
calculate
self
end
def show_notification?
can_see_limit_reached? && namespace.shared_runners_remaining_minutes_below_threshold?
end
def show_alert?
can_see_limit_reached? && below_threshold?
end
def scope
level.full_path
end
private
attr_reader :project,
:can_see_status,
:has_limit,
:current_user,
:level
def calculate
if at_namespace_level?
calculate_from_namespace_level
else
calculate_from_project_level
end
@has_limit = level.shared_runners_minutes_limit_enabled?
end
def at_namespace_level?
namespace && !project
end
def calculate_from_namespace_level
@level = namespace
@can_see_status = true
end
def calculate_from_project_level
@level = project
@namespace = project.shared_runners_limit_namespace
@can_see_status = can?(current_user, :create_pipeline, project)
end
def can_see_limit_reached?
has_limit && can_see_status
end
def below_threshold?
namespace.shared_runners_minutes_used? || namespace.shared_runners_remaining_minutes_below_threshold?
end
end
end
- return unless show_buy_ci_minutes?
- return unless show_buy_ci_minutes?(project, namespace)
%li
= link_to profile_pipeline_quota_path,
......
- return unless show_user_notification_dot?(project, namespace)
- return unless show_ci_minutes_notification_dot?(project, namespace)
-# Tracking events from the template is not ideal and we are moving this to the client in https://gitlab.com/gitlab-org/gitlab/-/issues/213712
- track_event('show_buy_ci_minutes_notification', label: current_user.namespace.actual_plan_name, property: 'user_dropdown')
......
- notification_service = ::Ci::MinutesNotificationService.call(current_user, local_assigns.dig(:project), local_assigns.dig(:namespace))
- context = ::Ci::Minutes::Context.new(local_assigns.dig(:project), local_assigns.dig(:namespace))
- threshold = ::Ci::Minutes::Threshold.new(current_user, context.level)
- if notification_service.show_alert?
- if threshold.warning_reached? || threshold.alert_reached?
%div{ class: ["pt-2", (classes if defined? classes)] }
.bs-callout.shared-runner-quota-message.d-none.d-sm-block.bs-callout-danger{ data: { scope: notification_service.scope } }
.bs-callout.shared-runner-quota-message.d-none.d-sm-block.bs-callout-danger{ data: { scope: context.full_path } }
%p
= ci_usage_warning_message(notification_service.namespace, project)
= ci_usage_warning_message(context.namespace, project)
= link_to _('Purchase more minutes'), ::EE::SUBSCRIPTIONS_MORE_MINUTES_URL, class: "btn btn-danger btn-inverted"
......@@ -3,7 +3,11 @@
require "spec_helper"
describe EE::RunnersHelper do
let_it_be(:user) { create(:user) }
let_it_be(:user, reload: true) { create(:user) }
before do
allow(helper).to receive(:current_user).and_return(user)
end
describe '.ci_usage_warning_message' do
let(:project) { create(:project, namespace: namespace) }
......@@ -140,43 +144,23 @@ describe EE::RunnersHelper do
end
end
describe '.show_buy_ci_minutes?' do
subject { helper.show_buy_ci_minutes? }
context 'when experiment is disabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:buy_ci_minutes_version_a).and_return(false)
end
it { is_expected.to be_falsey }
end
shared_examples_for 'minutes notification' do
let_it_be(:namespace) { create(:namespace, owner: user) }
let_it_be(:project) { create(:project, namespace: namespace) }
let(:injected_project) { project }
let(:injected_namespace) { namespace }
let(:show_warning) { true }
let(:context_level) { project }
let(:context) { double('Ci::Minutes::Context', level: context_level, namespace: namespace) }
let(:threshold) { double('Ci::Minutes::Threshold', warning_reached?: show_warning) }
let!(:user_pipeline) { create(:ci_pipeline, user: user, project: project) }
context 'when experiment is enabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:buy_ci_minutes_version_a).and_return(true)
end
it { is_expected.to be_truthy }
end
end
describe '.show_user_notification_dot?' do
let(:experiment_status) { true }
let(:ci_minutes_show) { true }
let!(:user_pipelines) { create(:ci_pipeline, user: user, project: nil) }
subject { helper.show_user_notification_dot?(project, namespace) }
before do
allow(helper).to receive(:current_user).and_return(user)
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(experiment_status)
allow(::Ci::MinutesNotificationService).to receive_message_chain(:call, :show_notification?).and_return(ci_minutes_show)
allow(::Ci::Minutes::Context).to receive(:new).and_return(context)
allow(::Ci::Minutes::Threshold).to receive(:new).and_return(threshold)
end
context 'with a project and namespace' do
let_it_be(:project) { create(:project) }
let_it_be(:namespace) { create(:namespace) }
context 'when experiment is disabled' do
let(:experiment_status) { false }
......@@ -186,38 +170,97 @@ describe EE::RunnersHelper do
context 'when experiment is enabled with user pipelines' do
it { is_expected.to be_truthy }
context 'without a project' do
let(:project) { build(:project) }
context 'without a persisted project passed' do
let(:injected_project) { build(:project) }
let(:context_level) { namespace }
it { is_expected.to be_truthy }
end
context 'without a namespace' do
let(:namespace) { build(:namespace) }
context 'without a persisted namespace passed' do
let(:injected_namespace) { build(:namespace) }
it { is_expected.to be_truthy }
end
context 'with neither a project nor a namespace' do
let(:project) { build(:project) }
let(:namespace) { build(:namespace) }
let(:injected_project) { build(:project) }
let(:injected_namespace) { build(:namespace) }
it { is_expected.to be_falsey }
context 'when show_ci_minutes_notification_dot? has been called before' do
it 'does not do all the notification and query work again' do
expect(threshold).not_to receive(:warning_reached?)
expect(injected_project).to receive(:persisted?).once
helper.show_ci_minutes_notification_dot?(injected_project, injected_namespace)
expect(subject).to be_falsey
end
end
end
context 'when show notification is falsey' do
let(:ci_minutes_show) { false }
let(:show_warning) { false }
it { is_expected.to be_falsey }
end
context 'without user pipelines' do
before do
user.pipelines.clear
user.pipelines.clear # this forces us to reload user for let_it_be
end
it { is_expected.to be_falsey }
end
context 'when show_ci_minutes_notification_dot? has been called before' do
it 'does not do all the notification and query work again' do
expect(threshold).to receive(:warning_reached?).once
expect(project).to receive(:persisted?).once
helper.show_ci_minutes_notification_dot?(project, namespace)
expect(subject).to be_truthy
end
end
end
end
end
context 'with pipelines' do
let(:experiment_status) { true }
describe '.show_buy_ci_minutes?' do
subject { helper.show_buy_ci_minutes?(injected_project, injected_namespace) }
context 'when experiment is "ci_notification_dot"' do
it_behaves_like 'minutes notification' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(experiment_status)
allow(helper).to receive(:experiment_enabled?).with(:buy_ci_minutes_version_a).and_return(false)
end
end
end
context 'when experiment is "ci_minutes_version_a"' do
it_behaves_like 'minutes notification' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(false)
allow(helper).to receive(:experiment_enabled?).with(:buy_ci_minutes_version_a).and_return(experiment_status)
end
end
end
end
describe '.show_ci_minutes_notification_dot?' do
subject { helper.show_ci_minutes_notification_dot?(injected_project, injected_namespace) }
it_behaves_like 'minutes notification' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(experiment_status)
end
end
end
end
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::Minutes::Context do
let_it_be(:group) { create(:group) }
let(:project) { build(:project, namespace: group) }
shared_examples 'full path' do
describe '#full_path' do
it 'shows full path' do
expect(subject.full_path).to eq context.full_path
end
end
describe '#level' do
it 'assigns correct level of namespace or project' do
expect(subject.level).to eq context
end
end
end
shared_examples 'captures root namespace' do
describe '#namespace' do
it 'assigns the namespace' do
expect(subject.namespace).to eq group
end
end
end
context 'when at project level' do
subject { described_class.new(project, nil) }
it_behaves_like 'captures root namespace'
it_behaves_like 'full path' do
let(:context) { project }
end
end
context 'when at namespace level' do
subject { described_class.new(nil, group) }
it_behaves_like 'captures root namespace'
it_behaves_like 'full path' do
let(:context) { group }
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Ci::Minutes::Threshold do
let_it_be(:user) { create(:user) }
let(:shared_runners_enabled) { true }
let!(:project) { create(:project, :repository, namespace: group, shared_runners_enabled: shared_runners_enabled) }
let_it_be(:group) { create(:group) }
let(:injected_group) { group }
let(:injected_project) { project }
shared_examples 'queries for warning being reached' do
context 'without limit' do
it { is_expected.to be_falsey }
end
context 'when limit is defined' do
context 'when usage has reached a notification level' do
before do
group.shared_runners_minutes_limit = 10
allow(group).to receive(:shared_runners_remaining_minutes).and_return(2)
end
context 'when over the limit' do
before do
group.last_ci_minutes_usage_notification_level = 30
end
it { is_expected.to be_truthy }
end
context 'when right at the limit for notification' do
before do
group.last_ci_minutes_usage_notification_level = 20
end
it { is_expected.to be_truthy }
end
end
context 'when limit not yet exceeded' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
it { is_expected.to be_falsey }
end
context 'when minutes are not yet set' do
let(:group) { create(:group, :with_build_minutes_limit) }
it { is_expected.to be_falsey }
end
end
end
shared_examples 'queries for alert being reached' do
context 'without limit' do
it { is_expected.to be_falsey }
end
context 'when limit is defined' do
context 'when usage has exceeded the limit' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
it { is_expected.to be_truthy }
end
context 'when limit not yet exceeded' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
it { is_expected.to be_falsey }
end
context 'when minutes are not yet set' do
let(:group) { create(:group, :with_build_minutes_limit) }
it { is_expected.to be_falsey }
end
end
end
shared_examples 'cannot see if warning reached' do
before do
group.last_ci_minutes_usage_notification_level = 30
group.shared_runners_minutes_limit = 10
allow(group).to receive(:shared_runners_remaining_minutes).and_return(2)
end
context 'when usage has not reached a warning level' do
it { is_expected.to be_falsey }
end
end
shared_examples 'cannot see if alert reached' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
context 'when usage has reached an alert level' do
it { is_expected.to be_falsey }
end
end
context 'when at project level' do
describe '#warning_reached?' do
subject do
threshold = described_class.new(user, injected_project)
threshold.warning_reached?
end
context 'when project member' do
it_behaves_like 'queries for warning being reached' do
before do
group.add_developer(user)
end
end
end
context 'when not a project member' do
it_behaves_like 'cannot see if warning reached'
end
end
describe '#alert_reached?' do
subject do
threshold = described_class.new(user, injected_project)
threshold.alert_reached?
end
context 'when project member' do
it_behaves_like 'queries for alert being reached' do
before do
group.add_developer(user)
end
end
end
context 'when not a project member' do
it_behaves_like 'cannot see if alert reached'
end
end
end
context 'when at namespace level' do
describe '#warning_reached?' do
subject do
threshold = described_class.new(user, injected_group)
threshold.warning_reached?
end
context 'with a project that has runners enabled inside namespace' do
it_behaves_like 'queries for warning being reached'
end
context 'with no projects that have runners enabled inside namespace' do
it_behaves_like 'cannot see if warning reached' do
let(:shared_runners_enabled) { false }
end
end
end
describe '#alert_reached?' do
subject do
threshold = described_class.new(user, injected_group)
threshold.alert_reached?
end
context 'with a project that has runners enabled inside namespace' do
it_behaves_like 'queries for alert being reached'
end
context 'with no projects that have runners enabled inside namespace' do
it_behaves_like 'cannot see if alert reached' do
let(:shared_runners_enabled) { false }
end
end
end
end
end
......@@ -17,6 +17,7 @@ describe Project do
it { is_expected.to delegate_method(:actual_shared_runners_minutes_limit).to(:shared_runners_limit_namespace) }
it { is_expected.to delegate_method(:shared_runners_minutes_limit_enabled?).to(:shared_runners_limit_namespace) }
it { is_expected.to delegate_method(:shared_runners_minutes_used?).to(:shared_runners_limit_namespace) }
it { is_expected.to delegate_method(:shared_runners_remaining_minutes_below_threshold?).to(:shared_runners_limit_namespace) }
it { is_expected.to belong_to(:deleting_user) }
......
# frozen_string_literal: true
require 'spec_helper'
describe Ci::MinutesNotificationService do
describe '.call' do
let_it_be(:user) { create(:user) }
let(:shared_runners_enabled) { true }
let!(:project) { create(:project, :repository, namespace: group, shared_runners_enabled: shared_runners_enabled) }
let_it_be(:group) { create(:group) }
let(:namespace) { group }
let(:prj) { project }
subject { described_class.call(user, prj, namespace) }
shared_examples 'showing notification' do
context 'without limit' do
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
context 'when limit is defined' do
context 'when usage has reached a notification level' do
before do
group.last_ci_minutes_usage_notification_level = 30
group.shared_runners_minutes_limit = 10
allow_any_instance_of(EE::Namespace).to receive(:shared_runners_remaining_minutes).and_return(2)
end
it 'returns truthy' do
expect(subject.show_notification?).to be_truthy
end
end
context 'when limit not yet exceeded' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
context 'when minutes are not yet set' do
let(:group) { create(:group, :with_build_minutes_limit) }
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
end
end
shared_examples 'showing alert' do
context 'without limit' do
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
context 'when limit is defined' do
context 'when usage has reached a notification level' do
before do
group.last_ci_minutes_usage_notification_level = 30
group.shared_runners_minutes_limit = 10
allow_any_instance_of(EE::Namespace).to receive(:shared_runners_remaining_minutes).and_return(2)
end
it 'returns truthy' do
expect(subject.show_notification?).to be_truthy
end
end
context 'when usage has exceeded the limit' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
it 'returns truthy' do
expect(subject.show_notification?).to be_truthy
end
end
context 'when limit not yet exceeded' do
let(:group) { create(:group, :with_not_used_build_minutes_limit) }
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
context 'when minutes are not yet set' do
let(:group) { create(:group, :with_build_minutes_limit) }
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
end
end
shared_examples 'scoping' do
describe '#scope' do
it 'shows full path' do
expect(subject.scope).to eq level.full_path
end
end
end
shared_examples 'show notification project constraints' do
before do
group.last_ci_minutes_usage_notification_level = 30
group.shared_runners_minutes_limit = 10
allow_any_instance_of(EE::Namespace).to receive(:shared_runners_remaining_minutes).and_return(2)
end
context 'when usage has reached a notification level' do
it 'returns falsey' do
expect(subject.show_notification?).to be_falsey
end
end
end
shared_examples 'show alert project constraints' do
let(:group) { create(:group, :with_used_build_minutes_limit) }
context 'when usage has reached a notification level' do
it 'returns falsey' do
expect(subject.show_alert?).to be_falsey
end
end
end
shared_examples 'class level items' do
it 'assigns the namespace' do
expect(subject.namespace).to eq group
end
end
context 'when at project level' do
let(:namespace) { nil }
let(:prj) { project }
it_behaves_like 'class level items'
describe '#show_notification?' do
context 'when project member' do
it_behaves_like 'showing notification' do
before do
group.add_developer(user)
end
end
end
context 'when not a project member' do
it_behaves_like 'show notification project constraints'
end
end
describe '#show_alert?' do
context 'when project member' do
it_behaves_like 'showing alert' do
before do
group.add_developer(user)
end
end
end
context 'when not a project member' do
it_behaves_like 'show alert project constraints'
end
end
it_behaves_like 'scoping' do
let(:level) { project }
end
end
context 'when at namespace level' do
let(:prj) { nil }
it_behaves_like 'class level items'
describe '#show_notification?' do
context 'with a project that has runners enabled inside namespace' do
it_behaves_like 'showing notification'
end
context 'with no projects that have runners enabled inside namespace' do
it_behaves_like 'show notification project constraints' do
let(:shared_runners_enabled) { false }
end
end
end
describe '#show_alert?' do
context 'with a project that has runners enabled inside namespace' do
it_behaves_like 'showing alert'
end
context 'with no projects that have runners enabled inside namespace' do
it_behaves_like 'show alert project constraints' do
let(:shared_runners_enabled) { false }
end
end
end
it_behaves_like 'scoping' do
let(:level) { group }
end
end
end
end
......@@ -12,7 +12,7 @@ describe 'layouts/application' do
allow(view).to receive(:user_signed_in?).and_return(true)
allow(view).to receive(:current_user).and_return(user)
allow(view).to receive(:current_user_mode).and_return(Gitlab::Auth::CurrentUserMode.new(user))
allow(view).to receive(:show_user_notification_dot?).and_return(show_notification_dot)
allow(view).to receive(:show_ci_minutes_notification_dot?).and_return(show_notification_dot)
end
describe 'layouts/_user_notification_dot' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment