Commit 4e6cfa11 authored by Ash McKenzie's avatar Ash McKenzie

Merge branch 'logic-to-show-notification-dot' into 'master'

Add logic for notification dot

Closes gitlab-org/growth/product#1498

See merge request gitlab-org/gitlab!27707
parents b2312db6 95d0aec5
...@@ -65,10 +65,6 @@ module NavHelper ...@@ -65,10 +65,6 @@ module NavHelper
%w(groups#issues labels#index milestones#index boards#index boards#show) %w(groups#issues labels#index milestones#index boards#index boards#show)
end end
def show_user_notification_dot?
experiment_enabled?(:ci_notification_dot)
end
private private
def get_header_links def get_header_links
......
...@@ -68,8 +68,7 @@ ...@@ -68,8 +68,7 @@
%li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) } %li.nav-item.header-user.dropdown{ data: { track_label: "profile_dropdown", track_event: "click_dropdown", track_value: "", qa_selector: 'user_menu' }, class: ('mr-0' if has_impersonation_link) }
= link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do = link_to current_user, class: user_dropdown_class, data: { toggle: "dropdown" } do
= image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar" = image_tag avatar_icon_for_user(current_user, 23), width: 23, height: 23, class: "header-user-avatar qa-user-avatar"
- if show_user_notification_dot? = render_if_exists 'layouts/header/user_notification_dot', project: project, namespace: group
%span.header-user-notification-dot.rounded-circle.position-relative
= sprite_icon('angle-down', css_class: 'caret-down') = sprite_icon('angle-down', css_class: 'caret-down')
.dropdown-menu.dropdown-menu-right .dropdown-menu.dropdown-menu-right
= render 'layouts/header/current_user_dropdown' = render 'layouts/header/current_user_dropdown'
......
...@@ -19,6 +19,14 @@ module EE ...@@ -19,6 +19,14 @@ module EE
experiment_enabled?(:buy_ci_minutes_version_a) experiment_enabled?(:buy_ci_minutes_version_a)
end end
def show_user_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?
end
private private
def purchase_shared_runner_minutes_link def purchase_shared_runner_minutes_link
......
# 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_user_notification_dot?(project, namespace)
%span.header-user-notification-dot.rounded-circle.position-relative
- project = local_assigns.fetch(:project, nil) - notification_service = ::Ci::MinutesNotificationService.call(current_user, local_assigns.dig(:project), local_assigns.dig(:namespace))
- namespace = local_assigns.fetch(:namespace, project && project.shared_runners_limit_namespace)
- scope = (project || namespace).full_path
- has_limit = (project || namespace).shared_runners_minutes_limit_enabled?
- can_see_status = project.nil? || can?(current_user, :create_pipeline, project)
- ci_warning_message = ci_usage_warning_message(namespace, project)
- if has_limit && can_see_status && ci_warning_message.present? - if notification_service.show_alert?
%div{ class: ["pt-2", (classes if defined? classes)] } %div{ class: ["pt-2", (classes if defined? classes)] }
.bs-callout.shared-runner-quota-message.d-none.d-sm-block.bs-callout-danger{ data: { scope: scope } } .bs-callout.shared-runner-quota-message.d-none.d-sm-block.bs-callout-danger{ data: { scope: notification_service.scope } }
%p %p
= ci_warning_message = ci_usage_warning_message(notification_service.namespace, project)
= link_to _('Purchase more minutes'), ::EE::SUBSCRIPTIONS_MORE_MINUTES_URL, class: "btn btn-danger btn-inverted" = link_to _('Purchase more minutes'), ::EE::SUBSCRIPTIONS_MORE_MINUTES_URL, class: "btn btn-danger btn-inverted"
...@@ -120,7 +120,12 @@ describe 'CI shared runner limits' do ...@@ -120,7 +120,12 @@ describe 'CI shared runner limits' do
def expect_quota_exceeded_alert(message = nil) def expect_quota_exceeded_alert(message = nil)
expect(page).to have_selector('.shared-runner-quota-message', count: 1) expect(page).to have_selector('.shared-runner-quota-message', count: 1)
expect(page.find('.shared-runner-quota-message')).to have_content(message) unless message.nil?
if message
element = page.find('.shared-runner-quota-message')
expect(element).to have_content(message)
expect(element['data-scope']).to eq(project.full_path)
end
end end
def expect_no_quota_exceeded_alert def expect_no_quota_exceeded_alert
......
...@@ -3,10 +3,11 @@ ...@@ -3,10 +3,11 @@
require "spec_helper" require "spec_helper"
describe EE::RunnersHelper do describe EE::RunnersHelper do
let_it_be(:user) { create(:user) }
describe '.ci_usage_warning_message' do describe '.ci_usage_warning_message' do
let(:project) { create(:project, namespace: namespace) } let(:project) { create(:project, namespace: namespace) }
let(:minutes_used) { 0 } let(:minutes_used) { 0 }
let(:user) { create(:user) }
let(:namespace) do let(:namespace) do
create(:group, shared_runners_minutes_limit: 100) create(:group, shared_runners_minutes_limit: 100)
...@@ -158,4 +159,66 @@ describe EE::RunnersHelper do ...@@ -158,4 +159,66 @@ describe EE::RunnersHelper do
it { is_expected.to be_truthy } it { is_expected.to be_truthy }
end end
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)
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 }
it { is_expected.to be_falsey }
end
context 'when experiment is enabled with user pipelines' do
it { is_expected.to be_truthy }
context 'without a project' do
let(:project) { build(:project) }
it { is_expected.to be_truthy }
end
context 'without a namespace' do
let(: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) }
it { is_expected.to be_falsey }
end
context 'when show notification is falsey' do
let(:ci_minutes_show) { false }
it { is_expected.to be_falsey }
end
context 'without user pipelines' do
before do
user.pipelines.clear
end
it { is_expected.to be_falsey }
end
end
end
end
end end
# 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
...@@ -117,24 +117,4 @@ describe NavHelper, :do_not_mock_admin_mode do ...@@ -117,24 +117,4 @@ describe NavHelper, :do_not_mock_admin_mode do
it { is_expected.to all(be_a(String)) } it { is_expected.to all(be_a(String)) }
end end
describe '.show_user_notification_dot?' do
subject { helper.show_user_notification_dot? }
context 'when experiment is disabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(false)
end
it { is_expected.to be_falsey }
end
context 'when experiment is enabled' do
before do
allow(helper).to receive(:experiment_enabled?).with(:ci_notification_dot).and_return(true)
end
it { is_expected.to be_truthy }
end
end
end end
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