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'; ...@@ -8,6 +8,7 @@ import NotificationsForm from '~/notifications_form';
import ProjectsList from '~/projects_list'; import ProjectsList from '~/projects_list';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation'; import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import GroupTabs from './group_tabs'; import GroupTabs from './group_tabs';
import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert';
export default function initGroupDetails(actionName = 'show') { export default function initGroupDetails(actionName = 'show') {
const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup'); const newGroupChildWrapper = document.querySelector('.js-new-project-subgroup');
...@@ -27,4 +28,6 @@ export default function initGroupDetails(actionName = 'show') { ...@@ -27,4 +28,6 @@ export default function initGroupDetails(actionName = 'show') {
if (newGroupChildWrapper) { if (newGroupChildWrapper) {
new NewGroupChild(newGroupChildWrapper); new NewGroupChild(newGroupChildWrapper);
} }
initNamespaceStorageLimitAlert();
} }
...@@ -14,9 +14,11 @@ import initReadMore from '~/read_more'; ...@@ -14,9 +14,11 @@ import initReadMore from '~/read_more';
import leaveByUrl from '~/namespaces/leave_by_url'; import leaveByUrl from '~/namespaces/leave_by_url';
import Star from '../../../star'; import Star from '../../../star';
import notificationsDropdown from '../../../notifications_dropdown'; import notificationsDropdown from '../../../notifications_dropdown';
import initNamespaceStorageLimitAlert from '~/namespace_storage_limit_alert';
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initReadMore(); initReadMore();
initNamespaceStorageLimitAlert();
new Star(); // eslint-disable-line no-new new Star(); // eslint-disable-line no-new
notificationsDropdown(); notificationsDropdown();
new ShortcutsNavigation(); // eslint-disable-line no-new new ShortcutsNavigation(); // eslint-disable-line no-new
......
...@@ -56,6 +56,45 @@ module NamespacesHelper ...@@ -56,6 +56,45 @@ module NamespacesHelper
namespaces_options(selected, options) namespaces_options(selected, options)
end 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 private
# Many importers create a temporary Group, so use the real # Many importers create a temporary Group, so use the real
...@@ -89,4 +128,4 @@ module NamespacesHelper ...@@ -89,4 +128,4 @@ module NamespacesHelper
end end
end end
NamespacesHelper.include_if_ee('EE::NamespacesHelper') NamespacesHelper.prepend_if_ee('EE::NamespacesHelper')
...@@ -41,7 +41,8 @@ module Namespaces ...@@ -41,7 +41,8 @@ module Namespaces
{ {
explanation_message: explanation_message, explanation_message: explanation_message,
usage_message: usage_message, usage_message: usage_message,
alert_level: alert_level alert_level: alert_level,
root_namespace: root_namespace
} }
end end
...@@ -50,7 +51,7 @@ module Namespaces ...@@ -50,7 +51,7 @@ module Namespaces
end end
def usage_message 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 end
def alert_level def alert_level
......
= content_for :flash_message do = 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_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 @@ ...@@ -9,3 +9,4 @@
= render 'shared/auto_devops_implicitly_enabled_banner', project: project = render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', 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_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 @@ ...@@ -2,6 +2,8 @@
module EE module EE
module NamespacesHelper module NamespacesHelper
extend ::Gitlab::Utils::Override
def ci_minutes_report(quota_report) def ci_minutes_report(quota_report)
content_tag(:span, class: "shared_runners_limit_#{quota_report.status}") do content_tag(:span, class: "shared_runners_limit_#{quota_report.status}") do
"#{quota_report.used} / #{quota_report.limit}" "#{quota_report.used} / #{quota_report.limit}"
...@@ -29,5 +31,14 @@ module EE ...@@ -29,5 +31,14 @@ module EE
content_tag :div, nil, options content_tag :div, nil, options
end end
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
end end
...@@ -103,4 +103,20 @@ RSpec.describe EE::NamespacesHelper do ...@@ -103,4 +103,20 @@ RSpec.describe EE::NamespacesHelper do
end end
end 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 end
...@@ -13403,6 +13403,9 @@ msgstr "" ...@@ -13403,6 +13403,9 @@ msgstr ""
msgid "Manage project labels" msgid "Manage project labels"
msgstr "" msgstr ""
msgid "Manage storage usage"
msgstr ""
msgid "Manage two-factor authentication" msgid "Manage two-factor authentication"
msgstr "" msgstr ""
...@@ -25766,7 +25769,7 @@ msgstr "" ...@@ -25766,7 +25769,7 @@ msgstr ""
msgid "You need to upload a Google Takeout archive." msgid "You need to upload a Google Takeout archive."
msgstr "" 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 "" msgstr ""
msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:" msgid "You tried to fork %{link_to_the_project} but it failed for the following reason:"
......
...@@ -37,6 +37,8 @@ RSpec.describe GroupsController do ...@@ -37,6 +37,8 @@ RSpec.describe GroupsController do
end end
shared_examples 'details view' do shared_examples 'details view' do
let(:namespace) { group }
it { is_expected.to render_template('groups/show') } it { is_expected.to render_template('groups/show') }
context 'as atom' do context 'as atom' do
...@@ -50,6 +52,8 @@ RSpec.describe GroupsController do ...@@ -50,6 +52,8 @@ RSpec.describe GroupsController do
expect(assigns(:events).map(&:id)).to contain_exactly(event.id) expect(assigns(:events).map(&:id)).to contain_exactly(event.id)
end end
end end
it_behaves_like 'namespace storage limit alert'
end end
describe 'GET #show' do describe 'GET #show' do
......
...@@ -380,6 +380,15 @@ RSpec.describe ProjectsController do ...@@ -380,6 +380,15 @@ RSpec.describe ProjectsController do
end end
end 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 end
describe 'GET edit' do 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 ...@@ -174,4 +174,96 @@ describe NamespacesHelper do
end end
end 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 end
...@@ -156,4 +156,10 @@ describe Namespaces::CheckStorageSizeService, '#execute' do ...@@ -156,4 +156,10 @@ describe Namespaces::CheckStorageSizeService, '#execute' do
expect(response).to include("60%") expect(response).to include("60%")
end end
end end
describe 'payload root_namespace' do
subject(:response) { service.execute.payload[:root_namespace] }
it { is_expected.to eq(namespace) }
end
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