Commit 9c424b27 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch 'namespace-project-storage-inline-alert' into 'master'

Usage quota storage alert

See merge request gitlab-org/gitlab!44494
parents b3bafa5d b4235a09
<script>
import { GlAlert } from '@gitlab/ui';
import { n__, __ } from '~/locale';
import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
import { usageRatioToThresholdLevel } from '../usage_thresholds';
import { ALERT_THRESHOLD, ERROR_THRESHOLD, WARNING_THRESHOLD } from '../constants';
export default {
components: {
GlAlert,
},
props: {
containsLockedProjects: {
type: Boolean,
required: true,
},
repositorySizeExcessProjectCount: {
type: Number,
required: true,
},
totalRepositorySizeExcess: {
type: Number,
required: true,
},
totalRepositorySize: {
type: Number,
required: true,
},
additionalPurchasedStorageSize: {
type: Number,
required: true,
},
repositoryFreeSizeLimit: {
type: Number,
required: true,
},
},
computed: {
shouldShowAlert() {
return this.hasPurchasedStorage() || this.containsLockedProjects;
},
alertText() {
return this.hasPurchasedStorage()
? this.hasPurchasedStorageText()
: this.hasNotPurchasedStorageText();
},
alertTitle() {
if (!this.hasPurchasedStorage() && this.containsLockedProjects) {
return __('UsageQuota|This namespace contains locked projects');
}
return `${this.excessStoragePercentageLeft}% of purchased storage is available`;
},
excessStorageRatio() {
return this.totalRepositorySizeExcess / this.additionalPurchasedStorageSize;
},
excessStoragePercentageUsed() {
return (this.excessStorageRatio * 100).toFixed(0);
},
excessStoragePercentageLeft() {
return Math.max(0, 100 - this.excessStoragePercentageUsed);
},
thresholdLevel() {
return usageRatioToThresholdLevel(this.excessStorageRatio);
},
thresholdLevelToAlertVariant() {
if (this.thresholdLevel === ERROR_THRESHOLD || this.thresholdLevel === ALERT_THRESHOLD) {
return 'danger';
} else if (this.thresholdLevel === WARNING_THRESHOLD) {
return 'warning';
}
return 'info';
},
projectsLockedText() {
if (this.repositorySizeExcessProjectCount === 0) {
return '';
}
return `${this.repositorySizeExcessProjectCount} ${n__(
'project',
'projects',
this.repositorySizeExcessProjectCount,
)}`;
},
},
methods: {
hasPurchasedStorage() {
return this.additionalPurchasedStorageSize > 0;
},
formatSize(size) {
const formatter = getFormatter(SUPPORTED_FORMATS.decimalBytes);
return formatter(size);
},
hasPurchasedStorageText() {
if (this.thresholdLevel === ERROR_THRESHOLD) {
return __(
`You have consumed all of your additional storage, please purchase more to unlock your projects over the free ${this.formatSize(
this.repositoryFreeSizeLimit,
)} limit`,
);
} else if (
this.thresholdLevel === WARNING_THRESHOLD ||
this.thresholdLevel === ALERT_THRESHOLD
) {
__(
`Your purchased storage is running low. To avoid locked projects, please purchase more storage.`,
);
}
return __(
`When you purchase additional storage, we automatically unlock projects that were locked when you reached the ${this.formatSize(
this.repositoryFreeSizeLimit,
)} limit.`,
);
},
hasNotPurchasedStorageText() {
if (this.thresholdLevel === ERROR_THRESHOLD) {
return __(
`You have reached the free storage limit of ${this.formatSize(
this.repositoryFreeSizeLimit,
)} on ${this.projectsLockedText}. To unlock them, please purchase additional storage.`,
);
}
return '';
},
},
};
</script>
<template>
<gl-alert
v-if="shouldShowAlert"
class="gl-mt-5"
:variant="thresholdLevelToAlertVariant"
:dismissible="false"
:title="alertTitle"
>
{{ alertText }}
</gl-alert>
</template>
export const NONE_THRESHOLD = 'none';
export const INFO_THRESHOLD = 'info';
export const WARNING_THRESHOLD = 'warning';
export const ALERT_THRESHOLD = 'alert';
export const ERROR_THRESHOLD = 'error';
import {
ALERT_THRESHOLD,
ERROR_THRESHOLD,
INFO_THRESHOLD,
NONE_THRESHOLD,
WARNING_THRESHOLD,
} from './constants';
const STORAGE_USAGE_THRESHOLDS = {
[NONE_THRESHOLD]: 0.0,
[INFO_THRESHOLD]: 0.5,
[WARNING_THRESHOLD]: 0.75,
[ALERT_THRESHOLD]: 0.95,
[ERROR_THRESHOLD]: 1.0,
};
export function usageRatioToThresholdLevel(currentUsageRatio) {
let currentLevel = Object.keys(STORAGE_USAGE_THRESHOLDS)[0];
Object.keys(STORAGE_USAGE_THRESHOLDS).forEach(thresholdLevel => {
if (currentUsageRatio >= STORAGE_USAGE_THRESHOLDS[thresholdLevel])
currentLevel = thresholdLevel;
});
return currentLevel;
}
import { shallowMount } from '@vue/test-utils';
import StorageInlineAlert from 'ee/storage_counter/components/storage_inline_alert.vue';
import { GlAlert } from '@gitlab/ui';
const GB_IN_BYTES = 1_000_000_000;
const THIRTEEN_GB_IN_BYTES = 13 * GB_IN_BYTES;
const TEN_GB_IN_BYTES = 10 * GB_IN_BYTES;
const FIVE_GB_IN_BYTES = 5 * GB_IN_BYTES;
const THREE_GB_IN_BYTES = 3 * GB_IN_BYTES;
describe('StorageInlineAlert', () => {
let wrapper;
function mountComponent(props) {
wrapper = shallowMount(StorageInlineAlert, {
propsData: props,
});
}
const findAlert = () => wrapper.find(GlAlert);
describe('no excess storage and no purchase', () => {
beforeEach(() => {
mountComponent({
containsLockedProjects: false,
repositorySizeExcessProjectCount: 0,
totalRepositorySizeExcess: 0,
totalRepositorySize: FIVE_GB_IN_BYTES,
additionalPurchasedStorageSize: 0,
repositoryFreeSizeLimit: TEN_GB_IN_BYTES,
});
});
it('does not render an alert', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('excess storage and no purchase', () => {
beforeEach(() => {
mountComponent({
containsLockedProjects: true,
repositorySizeExcessProjectCount: 1,
totalRepositorySizeExcess: THREE_GB_IN_BYTES,
totalRepositorySize: THIRTEEN_GB_IN_BYTES,
additionalPurchasedStorageSize: 0,
repositoryFreeSizeLimit: TEN_GB_IN_BYTES,
});
});
it('renders danger variant alert', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().props('variant')).toBe('danger');
});
it('renders human readable repositoryFreeLimit', () => {
expect(findAlert().text()).toBe(
'You have reached the free storage limit of 10GB on 1 project. To unlock them, please purchase additional storage.',
);
});
});
describe('excess storage below purchase limit', () => {
beforeEach(() => {
mountComponent({
containsLockedProjects: false,
repositorySizeExcessProjectCount: 0,
totalRepositorySizeExcess: THREE_GB_IN_BYTES,
totalRepositorySize: THIRTEEN_GB_IN_BYTES,
additionalPurchasedStorageSize: FIVE_GB_IN_BYTES,
repositoryFreeSizeLimit: TEN_GB_IN_BYTES,
});
});
it('renders info variant alert', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().props('variant')).toBe('info');
});
it('renders text explaining storage', () => {
expect(findAlert().text()).toBe(
'When you purchase additional storage, we automatically unlock projects that were locked when you reached the 10GB limit.',
);
});
});
describe('excess storage above purchase limit', () => {
beforeEach(() => {
mountComponent({
containsLockedProjects: true,
repositorySizeExcessProjectCount: 1,
totalRepositorySizeExcess: THREE_GB_IN_BYTES,
totalRepositorySize: THIRTEEN_GB_IN_BYTES,
additionalPurchasedStorageSize: THREE_GB_IN_BYTES,
repositoryFreeSizeLimit: TEN_GB_IN_BYTES,
});
});
it('renders danger alert', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().props('variant')).toBe('danger');
});
});
});
import { usageRatioToThresholdLevel } from 'ee/storage_counter/usage_thresholds';
describe('UsageThreshold', () => {
it.each`
usageRatio | expectedLevel
${0} | ${'none'}
${0.4} | ${'none'}
${0.5} | ${'info'}
${0.9} | ${'warning'}
${0.99} | ${'alert'}
${1} | ${'error'}
${1.5} | ${'error'}
`('returns $expectedLevel from $usageRatio', ({ usageRatio, expectedLevel }) => {
expect(usageRatioToThresholdLevel(usageRatio)).toBe(expectedLevel);
});
});
......@@ -28123,6 +28123,9 @@ msgstr ""
msgid "UsageQuota|Storage"
msgstr ""
msgid "UsageQuota|This namespace contains locked projects"
msgstr ""
msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr ""
......@@ -30193,6 +30196,9 @@ msgstr ""
msgid "Your projects"
msgstr ""
msgid "Your purchased storage is running low. To avoid locked projects, please purchase more storage."
msgstr ""
msgid "Your request for access could not be processed: %{error_meesage}"
msgstr ""
......
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