Commit 631fdf97 authored by nicolasdular's avatar nicolasdular

Show storage limit alert for projects and groups

This adds an alert for projects and groups when the namespace
storage limit reaches a certain threhsold.
Depending on the threshold and alert_level we show different
styles of the alert.

We use cookies for to keep the alerts hidden for a user per
namespace and alert level. This is the first iteration of the
alert system for namespace storage limit and will be improved
later since it does not align with our UX guidelines to keep
alerts closed for a user.

We need to use cookies because we have no other system in place
so far to keep track of user + namespace + alert level.
parent 43efc7a4
import Cookies from 'js-cookie';
const handleOnDismiss = ({ currentTarget }) => {
const {
dataset: { id, level },
} = currentTarget;
Cookies.set(`hide_storage_limit_alert_${id}_${level}`, true, { expires: 365 });
const notification = document.querySelector('.js-namespace-storage-alert');
notification.parentNode.removeChild(notification);
};
export default () => {
const alert = document.querySelector('.js-namespace-storage-alert-dismiss');
if (alert) {
alert.addEventListener('click', handleOnDismiss);
}
};
......@@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs';
import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert';
export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
......@@ -27,4 +28,6 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper);
}
initNamespaceStorageLimitAlert();
}
......@@ -14,9 +14,11 @@ import initReadMore from '~/read_more';
import leaveByUrl from '~/namespaces/leave_by_url';
import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown';
import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert';
document.addEventListener('DOMContentLoaded', () => {
initReadMore();
initNamespaceStorageLimitAlert();
new Star(); // eslint-disable-line no-new
notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new
......
......@@ -56,6 +56,45 @@ module NamespacesHelper
namespaces_options(selected, options)
end
def namespace_storage_alert(namespace)
return {} if current_user.nil?
payload = Namespaces::CheckStorageSizeService.new(namespace, current_user).execute.payload
return {} if payload.empty?
alert_level = payload[:alert_level]
root_namespace = payload[:root_namespace]
return {} if cookies["hide_storage_limit_alert_#{root_namespace.id}_#{alert_level}"] == 'true'
payload
end
def namespace_storage_alert_style(alert_level)
if alert_level == :error || alert_level == :alert
'danger'
else
alert_level.to_s
end
end
def namespace_storage_alert_icon(alert_level)
if alert_level == :error || alert_level == :alert
'error'
elsif alert_level == :info
'information-o'
else
alert_level.to_s
end
end
def namespace_storage_usage_link(namespace)
# The usage quota page is only available in EE. This will be changed in
# the future, see https://gitlab.com/gitlab-org/gitlab/-/issues/220042.
nil
end
private
# Many importers create a temporary Group, so use the real
......@@ -89,4 +128,4 @@ module NamespacesHelper
end
end
NamespacesHelper.include_if_ee('EE::NamespacesHelper')
NamespacesHelper.prepend_if_ee('EE::NamespacesHelper')
......@@ -41,7 +41,8 @@ module Namespaces
{
explanation_message: explanation_message,
usage_message: usage_message,
alert_level: alert_level
alert_level: alert_level,
root_namespace: root_namespace
}
end
......@@ -50,7 +51,7 @@ module Namespaces
end
def usage_message
s_("You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
s_("You reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})" % current_usage_params)
end
def alert_level
......
= content_for :flash_message do
= render_if_exists 'shared/shared_runners_minutes_limit', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
= render 'shared/namespace_storage_limit_alert', namespace: @group, classes: [container_class, ("limit-container-width" unless fluid_layout)]
......@@ -9,3 +9,4 @@
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
= render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
= render 'shared/namespace_storage_limit_alert', namespace: project.namespace, classes: [container_class, ("limit-container-width" unless fluid_layout)]
- return unless current_user
- payload = namespace_storage_alert(namespace)
- return if payload.empty?
- alert_level = payload[:alert_level]
- root_namespace = payload[:root_namespace]
- style = namespace_storage_alert_style(alert_level)
- icon = namespace_storage_alert_icon(alert_level)
- link = namespace_storage_usage_link(root_namespace)
%div{ class: [classes, 'js-namespace-storage-alert'] }
.gl-pt-5.gl-pb-3
.gl-alert{ class: "gl-alert-#{style}", role: 'alert' }
= sprite_icon(icon, css_class: "gl-icon gl-alert-icon")
.gl-alert-title
%h4.gl-alert-title= payload[:usage_message]
- if alert_level != :error
%button.js-namespace-storage-alert-dismiss.gl-alert-dismiss.gl-cursor-pointer{ type: 'button', 'aria-label' => _('Dismiss'), data: { id: root_namespace.id, level: alert_level } }
= sprite_icon('close', size: 16, css_class: 'gl-icon')
.gl-alert-body
= payload[:explanation_message]
- if link
.gl-alert-actions
= link_to(_('Manage storage usage'), link, class: "btn gl-alert-action btn-md gl-button btn-#{style}")
......@@ -2,6 +2,8 @@
module EE
module NamespacesHelper
extend ::Gitlab::Utils::Override
def ci_minutes_report(quota_report)
content_tag(:span, class: "shared_runners_limit_#{quota_report.status}") do
"#{quota_report.used} / #{quota_report.limit}"
......@@ -29,5 +31,14 @@ module EE
content_tag :div, nil, options
end
end
override :namespace_storage_usage_link
def namespace_storage_usage_link(namespace)
if namespace.group?
group_usage_quotas_path(namespace, anchor: 'storage-quota-tab')
else
profile_usage_quotas_path(anchor: 'storage-quota-tab')
end
end
end
end
......@@ -103,4 +103,20 @@ RSpec.describe EE::NamespacesHelper do
end
end
end
describe '#namespace_storage_usage_link' do
subject { helper.namespace_storage_usage_link(namespace) }
context 'when namespace is a group' do
let(:namespace) { build(:group) }
it { is_expected.to eq(group_usage_quotas_path(namespace, anchor: 'storage-quota-tab')) }
end
context 'when namespace is a user' do
let(:namespace) { build(:namespace) }
it { is_expected.to eq(profile_usage_quotas_path(anchor: 'storage-quota-tab')) }
end
end
end
......@@ -13403,6 +13403,9 @@ msgstr ""
msgid "Manage project labels"
msgstr ""
msgid "Manage storage usage"
msgstr ""
msgid "Manage two-factor authentication"
msgstr ""
......@@ -25766,7 +25769,7 @@ msgstr ""
msgid "You need to upload a Google Takeout archive."
msgstr ""
msgid "You reached %{usage_in_percent} of %{namespace_name}'s capacity (%{used_storage} of %{storage_limit})"
msgid "You reached %{usage_in_percent} of %{namespace_name}'s storage capacity (%{used_storage} of %{storage_limit})"
msgstr ""
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
......
......@@ -37,6 +37,8 @@ RSpec.describe GroupsController do
end
shared_examples 'details view' do
let(:namespace) { group }
it { is_expected.to render_template('groups/show') }
context 'as atom' do
......@@ -50,6 +52,8 @@ RSpec.describe GroupsController do
expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
end
end
it_behaves_like 'namespace storage limit alert'
end
describe 'GET #show' do
......
......@@ -380,6 +380,15 @@ RSpec.describe ProjectsController do
end
end
end
context 'namespace storage limit' do
let_it_be(:project) { create(:project, :public, :repository ) }
let(:namespace) { project.namespace }
subject { get :show, params: { namespace_id: namespace, id: project } }
it_behaves_like 'namespace storage limit alert'
end
end
describe 'GET edit' do
......
import Cookies from 'js-cookie';
import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert';
describe('broadcast message on dismiss', () => {
const dismiss = () => {
const button = document.querySelector('.js-namespace-storage-alert-dismiss');
button.click();
};
beforeEach(() => {
setFixtures(`
<div class="js-namespace-storage-alert">
<button class="js-namespace-storage-alert-dismiss" data-id="1" data-level="info"></button>
</div>
`);
initNamespaceStorageLimitAlert();
});
it('removes alert', () => {
expect(document.querySelector('.js-namespace-storage-alert')).toBeTruthy();
dismiss();
expect(document.querySelector('.js-namespace-storage-alert')).toBeNull();
});
it('calls Cookies.set', () => {
jest.spyOn(Cookies, 'set');
dismiss();
expect(Cookies.set).toHaveBeenCalledWith('hide_storage_limit_alert_1_info', true, {
expires: 365,
});
});
});
......@@ -174,4 +174,96 @@ describe NamespacesHelper do
end
end
end
describe '#namespace_storage_alert' do
subject { helper.namespace_storage_alert(namespace) }
let(:namespace) { build(:namespace) }
let(:payload) do
{
alert_level: :info,
usage_message: "Usage",
explanation_message: "Explanation",
root_namespace: namespace
}
end
before do
allow(helper).to receive(:current_user).and_return(admin)
allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, admin) do |check_storage_size_service|
expect(check_storage_size_service).to receive(:execute).and_return(ServiceResponse.success(payload: payload))
end
end
context 'when payload is not empty and no cookie is set' do
it { is_expected.to eq(payload) }
end
context 'when there is no current_user' do
before do
allow(helper).to receive(:current_user).and_return(nil)
end
it { is_expected.to eq({}) }
end
context 'when payload is empty' do
let(:payload) { {} }
it { is_expected.to eq({}) }
end
context 'when cookie is set' do
before do
helper.request.cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true'
end
it { is_expected.to eq({}) }
end
context 'when payload is empty and cookie is set' do
let(:payload) { {} }
before do
helper.request.cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true'
end
it { is_expected.to eq({}) }
end
end
describe '#namespace_storage_alert_style' do
using RSpec::Parameterized::TableSyntax
subject { helper.namespace_storage_alert_style(alert_level) }
where(:alert_level, :result) do
:info | 'info'
:warning | 'warning'
:error | 'danger'
:alert | 'danger'
end
with_them do
it { is_expected.to eq(result) }
end
end
describe '#namespace_storage_alert_icon' do
using RSpec::Parameterized::TableSyntax
subject { helper.namespace_storage_alert_icon(alert_level) }
where(:alert_level, :result) do
:info | 'information-o'
:warning | 'warning'
:error | 'error'
:alert | 'error'
end
with_them do
it { is_expected.to eq(result) }
end
end
end
......@@ -156,4 +156,10 @@ describe Namespaces::CheckStorageSizeService, '#execute' do
expect(response).to include("60%")
end
end
describe 'payload root_namespace' do
subject(:response) { service.execute.payload[:root_namespace] }
it { is_expected.to eq(namespace) }
end
end
# frozen_string_literal: true
RSpec.shared_examples 'namespace storage limit alert' do
let(:alert_level) { :info }
before do
allow_next_instance_of(Namespaces::CheckStorageSizeService, namespace, user) do |check_storage_size_service|
expect(check_storage_size_service).to receive(:execute).and_return(
ServiceResponse.success(
payload: {
alert_level: alert_level,
usage_message: "Usage",
explanation_message: "Explanation",
root_namespace: namespace
}
)
)
end
allow(controller).to receive(:current_user).and_return(user)
end
render_views
it 'does render' do
subject
expect(response.body).to match(/Explanation/)
expect(response.body).to have_css('.js-namespace-storage-alert-dismiss')
end
context 'when alert_level is error' do
let(:alert_level) { :error }
it 'does not render a dismiss button' do
subject
expect(response.body).not_to have_css('.js-namespace-storage-alert-dismiss')
end
end
context 'when cookie is set' do
before do
cookies["hide_storage_limit_alert_#{namespace.id}_info"] = 'true'
end
it 'does not render alert' do
subject
expect(response.body).not_to match(/Explanation/)
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