Commit 9a00cb7d authored by Miguel Rincon's avatar Miguel Rincon

Merge branch '350646-use-statistics-block' into 'master'

Use StatisticsCard in usage_quota/storage

See merge request gitlab-org/gitlab!79525
parents d80842a5 7f3519b3
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
.col-sm-12 .col-sm-12
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.' = s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: '<strong>'.html_safe, strong_end: '</strong>'.html_safe, project_name: @project.name } + '.'
%a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' } %a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' }
= s_('UsageQuota|Learn more about usage quotas') + '.' = s_('UsageQuota|Learn more about usage quotas.')
= gl_tabs_nav do = gl_tabs_nav do
= gl_tab_link_to '#storage-quota-tab', item_active: true do = gl_tab_link_to '#storage-quota-tab', item_active: true do
......
...@@ -55,11 +55,6 @@ export default { ...@@ -55,11 +55,6 @@ export default {
required: false, required: false,
default: null, default: null,
}, },
cssClass: {
type: String,
required: false,
default: null,
},
}, },
}; };
</script> </script>
...@@ -68,7 +63,6 @@ export default { ...@@ -68,7 +63,6 @@ export default {
<div <div
class="gl-bg-white gl-border-1 gl-border-gray-100 gl-border-solid gl-p-5 gl-rounded-base" class="gl-bg-white gl-border-1 gl-border-gray-100 gl-border-solid gl-p-5 gl-rounded-base"
data-testid="container" data-testid="container"
:class="cssClass"
> >
<div class="gl-display-flex gl-justify-content-space-between"> <div class="gl-display-flex gl-justify-content-space-between">
<p <p
......
<script>
import { GlProgressBar } from '@gitlab/ui';
import { formatSizeAndSplit } from 'ee/usage_quotas/storage/utils';
export default {
name: 'StorageStatisticsCard',
components: { GlProgressBar },
props: {
totalStorage: {
type: Number,
required: false,
default: null,
},
usedStorage: {
type: Number,
required: false,
default: null,
},
},
computed: {
formattedUsage() {
// we want to show the usage only if there's purchased storage
if (this.totalStorage === null) {
return null;
}
return this.formatSizeAndSplit(this.usedStorage);
},
formattedTotal() {
return this.formatSizeAndSplit(this.totalStorage);
},
percentage() {
// don't show the progress bar if there's no total storage
if (!this.totalStorage || this.usedStorage === null) {
return null;
}
return Math.min(Math.round((this.usedStorage / this.totalStorage) * 100), 100);
},
usageValue() {
if (!this.totalStorage && !this.usedStorage) {
// if there is no total storage and no used storage, we want
// to show `0` instead of the formatted `0.0`
return '0';
}
return this.formattedUsage?.value;
},
usageUnit() {
return this.formattedUsage?.unit;
},
totalValue() {
return this.formattedTotal?.value;
},
totalUnit() {
return this.formattedTotal?.unit;
},
shouldRenderTotalBlock() {
// only show the total block if the used and total storage are not 0
return this.usedStorage && this.totalStorage;
},
},
methods: {
formatSizeAndSplit,
},
};
</script>
<template>
<div
class="gl-bg-white gl-border-1 gl-border-gray-100 gl-border-solid gl-p-5 gl-rounded-base"
data-testid="container"
>
<div class="gl-display-flex gl-justify-content-space-between">
<p class="gl-font-size-h-display gl-font-weight-bold gl-mb-3" data-testid="denominator">
{{ usageValue }}
<span class="gl-font-lg">{{ usageUnit }}</span>
<span v-if="shouldRenderTotalBlock" data-testid="denominator-total">
/
{{ totalValue }}
<span class="gl-font-lg">{{ totalUnit }}</span>
</span>
</p>
<div data-testid="actions">
<slot name="actions"></slot>
</div>
</div>
<p class="gl-font-weight-bold" data-testid="description">
<slot name="description"></slot>
</p>
<gl-progress-bar v-if="percentage !== null" :value="percentage" />
</div>
</template>
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlIcon, GlLink, GlButton } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper'; import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
import { formatUsageSize } from '../utils'; import StorageStatisticsCard from 'ee/usage_quotas/components/storage_statistics_card.vue';
import UsageStatisticsCard from './usage_statistics_card.vue';
export default { export default {
components: { components: {
GlIcon,
GlLink,
GlButton, GlButton,
UsageStatisticsCard, StorageStatisticsCard,
}, },
inject: ['purchaseStorageUrl', 'buyAddonTargetAttr'], inject: ['purchaseStorageUrl', 'buyAddonTargetAttr'],
props: { props: {
...@@ -17,115 +18,93 @@ export default { ...@@ -17,115 +18,93 @@ export default {
type: Object, type: Object,
}, },
}, },
i18n: {
purchasedUsageHelpLink: helpPagePath('user/usage_quotas'),
purchasedUsageHelpText: s__('UsageQuota|Learn more about usage quotas.'),
usedUsageHelpLink: helpPagePath('user/usage_quotas'),
usedUsageHelpText: s__('UsageQuota|Learn more about usage quotas.'),
purchaseButtonText: s__('UsageQuota|Buy storage'),
totalUsageDescription: s__('UsageQuota|Namespace storage used'),
},
computed: { computed: {
formattedActualRepoSizeLimit() { usedStorageAmount() {
return formatUsageSize(this.rootStorageStatistics.actualRepositorySizeLimit);
},
totalUsage() {
return {
usage: this.formatSizeAndSplit(this.rootStorageStatistics.totalRepositorySize),
description: s__('UsageQuota|Total namespace storage used'),
footerNote: s__(
'UsageQuota|This is the total amount of storage used across your projects within this namespace.',
),
link: {
text: `${s__('UsageQuota|Learn more about usage quotas')}.`,
url: helpPagePath('user/usage_quotas'),
},
};
},
excessUsage() {
return {
usage: this.formatSizeAndSplit(this.rootStorageStatistics.totalRepositorySizeExcess),
description: s__('UsageQuota|Total excess storage used'),
footerNote: s__(
'UsageQuota|This is the total amount of storage used by projects above the free %{actualRepositorySizeLimit} storage limit.',
),
link: {
text: s__('UsageQuota|Learn more about excess storage usage'),
url: helpPagePath('user/usage_quotas', { anchor: 'excess-storage-usage' }),
},
};
},
purchasedUsage() {
const { const {
totalRepositorySizeExcess,
additionalPurchasedStorageSize, additionalPurchasedStorageSize,
actualRepositorySizeLimit,
totalRepositorySize,
} = this.rootStorageStatistics; } = this.rootStorageStatistics;
return this.purchaseStorageUrl
? { if (additionalPurchasedStorageSize && totalRepositorySize > actualRepositorySizeLimit) {
usage: this.formatSizeAndSplit( return actualRepositorySizeLimit;
Math.max(0, additionalPurchasedStorageSize - totalRepositorySizeExcess), }
), return totalRepositorySize;
usageTotal: this.formatSizeAndSplit(additionalPurchasedStorageSize),
description: s__('UsageQuota|Purchased storage available'),
link: {
text: s__('UsageQuota|Purchase more storage'),
url: this.purchaseStorageUrl,
target: this.buyAddonTargetAttr,
},
}
: null;
}, },
}, purchasedUsageDescription() {
methods: { if (this.rootStorageStatistics.additionalPurchasedStorageSize) {
/** return s__('UsageQuota|Purchased storage used');
* The formatUsageSize method returns }
* value along with the unit. However, the unit return s__('UsageQuota|Purchased storage');
* and the value needs to be separated so that },
* they can have different styles. The method repositorySizeLimit() {
* splits the value into value and unit. return Number(this.rootStorageStatistics.actualRepositorySizeLimit);
* },
* @params {Number} size size in bytes purchasedTotalStorage() {
* @returns {Object} value and unit of formatted size return Number(this.rootStorageStatistics.additionalPurchasedStorageSize);
*/ },
formatSizeAndSplit(size) { purchasedUsedStorage() {
const formattedSize = formatUsageSize(size); // we don't want to show the used value if there's no purchased storage
return { return this.rootStorageStatistics.additionalPurchasedStorageSize
value: formattedSize.slice(0, -3), ? Number(this.rootStorageStatistics.totalRepositorySizeExcess)
unit: formattedSize.slice(-3), : 0;
};
}, },
}, },
}; };
</script> </script>
<template> <template>
<div class="gl-display-flex gl-sm-flex-direction-column"> <div class="gl-display-flex gl-sm-flex-direction-column gl-py-5">
<usage-statistics-card <storage-statistics-card
data-testid="total-usage" :used-storage="usedStorageAmount"
:usage="totalUsage.usage" :total-storage="repositorySizeLimit"
:link="totalUsage.link" data-testid="namespace-usage-card"
:description="totalUsage.description" class="gl-w-half gl-md-mr-5"
css-class="gl-mr-4"
/>
<usage-statistics-card
data-testid="excess-usage"
:usage="excessUsage.usage"
:link="excessUsage.link"
:description="excessUsage.description"
css-class="gl-mx-4"
/>
<usage-statistics-card
v-if="purchasedUsage"
data-testid="purchased-usage"
:usage="purchasedUsage.usage"
:usage-total="purchasedUsage.usageTotal"
:link="purchasedUsage.link"
:description="purchasedUsage.description"
css-class="gl-ml-4"
> >
<template #footer="{ link }"> <template #description>
<gl-button {{ $options.i18n.totalUsageDescription }}
:target="link.target"
:href="link.url" <gl-link
class="mb-0" :href="$options.i18n.usedUsageHelpLink"
variant="confirm" target="_blank"
category="primary" class="gl-ml-2"
block :aria-label="$options.i18n.usedUsageHelpText"
> >
{{ link.text }} <gl-icon name="question-o" />
</gl-link>
</template>
</storage-statistics-card>
<storage-statistics-card
v-if="purchaseStorageUrl"
:used-storage="purchasedUsedStorage"
:total-storage="purchasedTotalStorage"
data-testid="purchased-usage-card"
class="gl-w-half"
>
<template #actions>
<gl-button :href="purchaseStorageUrl" target="_blank" category="primary" variant="confirm">
{{ $options.i18n.purchaseButtonText }}
</gl-button> </gl-button>
</template> </template>
</usage-statistics-card> <template #description>
{{ purchasedUsageDescription }}
<gl-link
:href="$options.purchasedUsageHelpLink"
target="_blank"
class="gl-ml-2"
:aria-label="$options.i18n.purchasedUsageHelpText"
>
<gl-icon name="question-o" />
</gl-link>
</template>
</storage-statistics-card>
</div> </div>
</template> </template>
<script>
import { GlLink, GlIcon, GlSprintf } from '@gitlab/ui';
export default {
components: {
GlIcon,
GlLink,
GlSprintf,
},
props: {
link: {
type: Object,
required: false,
default: () => ({ text: '', url: '' }),
},
description: {
type: String,
required: true,
},
usage: {
type: Object,
required: true,
},
usageTotal: {
type: Object,
required: false,
default: null,
},
cssClass: {
type: String,
required: false,
default: '',
},
},
};
</script>
<template>
<div class="gl-p-5 gl-my-5 gl-bg-gray-10 gl-flex-grow-1 gl-white-space-nowrap" :class="cssClass">
<p class="mb-2">
<gl-sprintf :message="__('%{size} %{unit}')">
<template #size>
<span class="gl-font-size-h-display gl-font-weight-bold">{{ usage.value }}</span>
</template>
<template #unit>
<span class="gl-font-lg gl-font-weight-bold">{{ usage.unit }}</span>
</template>
</gl-sprintf>
<template v-if="usageTotal">
<span class="gl-font-size-h-display gl-font-weight-bold">/</span>
<gl-sprintf :message="__('%{size} %{unit}')">
<template #size>
<span
data-qa-selector="purchased_usage_total"
class="gl-font-size-h-display gl-font-weight-bold"
>{{ usageTotal.value }}</span
>
</template>
<template #unit>
<span class="gl-font-lg gl-font-weight-bold">{{ usageTotal.unit }}</span>
</template>
</gl-sprintf>
</template>
</p>
<p class="gl-border-b-2 gl-border-b-solid gl-border-b-gray-100 gl-font-weight-bold gl-pb-3">
{{ description }}
</p>
<p
class="gl-mb-0 gl-text-gray-900 gl-font-sm gl-white-space-normal"
data-testid="statistics-card-footer"
>
<slot v-bind="{ link }" name="footer">
<gl-link target="_blank" :href="link.url">
<span class="text-truncate">{{ link.text }}</span>
<gl-icon name="external-link" class="gl-ml-2 gl-flex-shrink-0 gl-text-black-normal" />
</gl-link>
</slot>
</p>
</div>
</template>
import { numberToHumanSize, bytesToKiB } from '~/lib/utils/number_utils'; import { numberToHumanSize, bytesToKiB } from '~/lib/utils/number_utils';
import { kibibytes } from '~/lib/utils/unit_format'; import { gibibytes, kibibytes } from '~/lib/utils/unit_format';
import { PROJECT_STORAGE_TYPES, STORAGE_USAGE_THRESHOLDS } from './constants'; import { PROJECT_STORAGE_TYPES, STORAGE_USAGE_THRESHOLDS } from './constants';
export function usageRatioToThresholdLevel(currentUsageRatio) { export function usageRatioToThresholdLevel(currentUsageRatio) {
...@@ -19,11 +19,12 @@ export function usageRatioToThresholdLevel(currentUsageRatio) { ...@@ -19,11 +19,12 @@ export function usageRatioToThresholdLevel(currentUsageRatio) {
* converting bytesToKiB before passing it to * converting bytesToKiB before passing it to
* `getFormatter` * `getFormatter`
* @param {Number} size size in bytes
* @returns {String} * @returns {String}
* @param sizeInBytes
* @param {String} unitSeparator
*/ */
export const formatUsageSize = (sizeInBytes) => { export const formatUsageSize = (sizeInBytes, unitSeparator = '') => {
return kibibytes(bytesToKiB(sizeInBytes), 1); return kibibytes(bytesToKiB(sizeInBytes), 1, { unitSeparator });
}; };
/** /**
...@@ -187,3 +188,27 @@ export const parseGetProjectStorageResults = (data, helpLinks) => { ...@@ -187,3 +188,27 @@ export const parseGetProjectStorageResults = (data, helpLinks) => {
export function descendingStorageUsageSort(storageUsageKey) { export function descendingStorageUsageSort(storageUsageKey) {
return (a, b) => b[storageUsageKey] - a[storageUsageKey]; return (a, b) => b[storageUsageKey] - a[storageUsageKey];
} }
/**
* The formatUsageSize method returns
* value along with the unit. However, the unit
* and the value needs to be separated so that
* they can have different styles. The method
* splits the value into value and unit.
*
* @params {Number} size size in bytes
* @returns {Object} value and unit of formatted size
*/
export function formatSizeAndSplit(sizeInBytes) {
if (sizeInBytes === null) {
return null;
}
/**
* we're using a special separator to help us split the formatted value properly,
* the separator won't be shown in the output
*/
const unitSeparator = '@';
const format = sizeInBytes === 0 ? gibibytes : kibibytes;
const [value, unit] = format(bytesToKiB(sizeInBytes), 1, { unitSeparator }).split(unitSeparator);
return { value, unit };
}
...@@ -30,12 +30,6 @@ describe('StatisticsCard', () => { ...@@ -30,12 +30,6 @@ describe('StatisticsCard', () => {
const findHelpLink = () => wrapper.findComponent(GlLink); const findHelpLink = () => wrapper.findComponent(GlLink);
const findProgressBar = () => wrapper.findComponent(GlProgressBar); const findProgressBar = () => wrapper.findComponent(GlProgressBar);
it('passes cssClass to container div', () => {
const cssClass = 'awesome-css-class';
createComponent({ cssClass });
expect(wrapper.find('[data-testid="container"]').classes()).toContain(cssClass);
});
describe('denominator block', () => { describe('denominator block', () => {
it('renders denominator block with all elements when all props are passed', () => { it('renders denominator block with all elements when all props are passed', () => {
createComponent(defaultProps); createComponent(defaultProps);
......
import { GlProgressBar } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import StorageStatisticsCard from 'ee/usage_quotas/components/storage_statistics_card.vue';
describe('StorageStatisticsCard', () => {
let wrapper;
const defaultProps = {
totalStorage: 100 * 1024,
usedStorage: 50 * 1024,
};
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(StorageStatisticsCard, {
propsData: { ...defaultProps, ...props },
slots: {
description: 'storage-statistics-card description slot',
actions: 'storage-statistics-card actions slot',
},
});
};
const findDenominatorBlock = () => wrapper.findByTestId('denominator');
const findTotalBlock = () => wrapper.findByTestId('denominator-total');
const findDescriptionBlock = () => wrapper.findByTestId('description');
const findActionsBlock = () => wrapper.findByTestId('actions');
const findProgressBar = () => wrapper.findComponent(GlProgressBar);
describe('denominator block', () => {
it('renders denominator block with all elements when all props are passed', () => {
createComponent();
expect(findDenominatorBlock().text()).toMatchInterpolatedText('50.0 KiB / 100.0 KiB');
});
it('does not render total block if totalStorage and usedStorage are not passed', () => {
createComponent({
usedStorage: null,
totalStorage: null,
});
expect(findTotalBlock().exists()).toBe(false);
});
it('renders the denominator block as 0 GiB if totalStorage and usedStorage are passed as 0', () => {
createComponent({
usedStorage: 0,
totalStorage: 0,
});
expect(findDenominatorBlock().text()).toMatchInterpolatedText('0 GiB');
});
});
describe('slots', () => {
it('renders description slot', () => {
createComponent();
expect(findDescriptionBlock().text()).toBe('storage-statistics-card description slot');
});
it('renders actions slot', () => {
createComponent();
expect(findActionsBlock().text()).toBe('storage-statistics-card actions slot');
});
});
describe('progress bar', () => {
it('does not render progress bar if there is no totalStorage', () => {
createComponent({ totalStorage: null });
expect(wrapper.findComponent(GlProgressBar).exists()).toBe(false);
});
it('renders progress bar if percentage is greater than 0', () => {
createComponent({ totalStorage: 10, usedStorage: 5 });
expect(findProgressBar().exists()).toBe(true);
expect(findProgressBar().attributes('value')).toBe(String(50));
});
it('renders the progress bar if percentage is 0', () => {
createComponent({ totalStorage: 10, usedStorage: 0 });
expect(findProgressBar().exists()).toBe(true);
expect(findProgressBar().attributes('value')).toBe(String(0));
});
});
});
import { GlButton, GlLink, GlSprintf } from '@gitlab/ui'; import { GlLink, GlSprintf, GlProgressBar, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import StorageStatisticsCard from 'ee/usage_quotas/components/storage_statistics_card.vue';
import UsageStatistics from 'ee/usage_quotas/storage/components/usage_statistics.vue'; import UsageStatistics from 'ee/usage_quotas/storage/components/usage_statistics.vue';
import UsageStatisticsCard from 'ee/usage_quotas/storage/components/usage_statistics_card.vue';
import { withRootStorageStatistics } from '../mock_data'; import { withRootStorageStatistics } from '../mock_data';
describe('UsageStatistics', () => { describe('UsageStatistics', () => {
let wrapper; let wrapper;
const createComponent = ({ props = {}, provide = {} } = {}) => { const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = shallowMount(UsageStatistics, { wrapper = shallowMountExtended(UsageStatistics, {
propsData: { propsData: {
rootStorageStatistics: { rootStorageStatistics: {
totalRepositorySize: withRootStorageStatistics.totalRepositorySize, totalRepositorySize: withRootStorageStatistics.totalRepositorySize,
...@@ -19,12 +19,15 @@ describe('UsageStatistics', () => { ...@@ -19,12 +19,15 @@ describe('UsageStatistics', () => {
...props, ...props,
}, },
provide: { provide: {
purchaseStorageUrl: 'some-fancy-url',
buyAddonTargetAttr: '_self',
...provide, ...provide,
}, },
stubs: { stubs: {
UsageStatisticsCard, StorageStatisticsCard,
GlSprintf, GlSprintf,
GlLink, GlLink,
GlProgressBar,
}, },
}); });
}; };
...@@ -33,79 +36,124 @@ describe('UsageStatistics', () => { ...@@ -33,79 +36,124 @@ describe('UsageStatistics', () => {
wrapper.destroy(); wrapper.destroy();
}); });
const getStatisticsCards = () => wrapper.findAllComponents(UsageStatisticsCard); const findAllStorageStatisticsCards = () => wrapper.findAllComponents(StorageStatisticsCard);
const getStatisticsCard = (testId) => wrapper.find(`[data-testid="${testId}"]`);
const findGlLinkInCard = (cardName) => const findNamespaceStorageCard = () => wrapper.findByTestId('namespace-usage-card');
getStatisticsCard(cardName) const findPurchasedStorageCard = () => wrapper.findByTestId('purchased-usage-card');
.find('[data-testid="statistics-card-footer"]')
.findComponent(GlLink);
const findPurchasedUsageButton = () =>
getStatisticsCard('purchased-usage').findComponent(GlButton);
describe('with purchaseStorageUrl passed', () => { describe('with purchaseStorageUrl passed', () => {
beforeEach(() => {
createComponent();
});
it('renders two statistics cards', () => {
expect(findAllStorageStatisticsCards()).toHaveLength(2);
});
});
describe('with no purchaseStorageUrl', () => {
beforeEach(() => { beforeEach(() => {
createComponent({ createComponent({
provide: { provide: {
purchaseStorageUrl: 'some-fancy-url', purchaseStorageUrl: null,
buyAddonTargetAttr: '_self',
}, },
}); });
}); });
it('renders three statistics cards', () => { it('renders one statistics cards', () => {
expect(getStatisticsCards()).toHaveLength(3); expect(findAllStorageStatisticsCards()).toHaveLength(1);
}); });
});
it('renders URL in total usage card footer', () => { describe('namespace storage used', () => {
const url = findGlLinkInCard('total-usage'); beforeEach(() => {
createComponent();
});
expect(url.attributes('href')).toBe('/help/user/usage_quotas'); it('renders progress bar with correct percentage', () => {
expect(findNamespaceStorageCard().findComponent(GlProgressBar).attributes('value')).toBe(
'100',
);
}); });
});
it('renders URL in excess usage card footer', () => { describe('purchase storage used', () => {
const url = findGlLinkInCard('excess-usage'); beforeEach(() => {
createComponent();
});
expect(url.attributes('href')).toBe('/help/user/usage_quotas#excess-storage-usage'); afterEach(() => {
wrapper.destroy();
}); });
it('renders button in purchased usage card footer with correct link', () => { it('renders the denominator and units correctly', () => {
expect(findPurchasedUsageButton().attributes()).toMatchObject({ expect(findPurchasedStorageCard().text().replace(/\s+/g, ' ')).toContain('2.3 KiB / 0.3 KiB');
href: 'some-fancy-url',
target: '_self',
});
}); });
});
describe('with buyAddonTargetAttr passed as _blank', () => { it('renders purchase more storage button', () => {
beforeEach(() => { const purchaseButton = findPurchasedStorageCard().findComponent(GlButton);
createComponent({ expect(purchaseButton.exists()).toBe(true);
provide: { expect(purchaseButton.attributes('href')).toBe('some-fancy-url');
purchaseStorageUrl: 'some-fancy-url',
buyAddonTargetAttr: '_blank',
},
});
}); });
it('renders button in purchased usage card footer with correct target', () => { it('renders the percentage bar', () => {
expect(findPurchasedUsageButton().attributes()).toMatchObject({ expect(findPurchasedStorageCard().findComponent(GlProgressBar).attributes('value')).toBe(
href: 'some-fancy-url', '100',
target: '_blank', );
});
}); });
}); });
describe('with no purchaseStorageUrl', () => { describe('when limit is exceeded', () => {
beforeEach(() => { describe('with purchased storage', () => {
createComponent({ beforeEach(() => {
provide: { createComponent({
purchaseStorageUrl: null, props: {
buyAddonTargetAttr: '_self', rootStorageStatistics: {
}, totalRepositorySize: 60 * 1024,
actualRepositorySizeLimit: 50 * 1024,
totalRepositorySizeExcess: 1024,
additionalPurchasedStorageSize: 10 * 1024,
},
},
});
});
it('shows only the limit in the namespace storage card', () => {
expect(findNamespaceStorageCard().text().replace(/\s+/g, ' ')).toContain(
'50.0 KiB / 50.0 KiB',
);
});
it('shows the excess amount in the purchased storage card', () => {
expect(findPurchasedStorageCard().text().replace(/\s+/g, ' ')).toContain(
'1.0 KiB / 10.0 KiB',
);
}); });
}); });
it('does not render purchased usage card if purchaseStorageUrl is not provided', () => { describe('without purchased storage', () => {
expect(getStatisticsCard('purchased-usage').exists()).toBe(false); beforeEach(() => {
createComponent({
props: {
rootStorageStatistics: {
totalRepositorySize: 502642,
actualRepositorySizeLimit: 500321,
totalRepositorySizeExcess: 2321,
additionalPurchasedStorageSize: 0,
},
},
});
});
it('shows the total of limit and excess in the namespace storage card', () => {
expect(findNamespaceStorageCard().text().replace(/\s+/g, ' ')).toContain(
'490.9 KiB / 488.6 KiB',
);
});
it('shows 0 GiB in the purchased storage card', () => {
expect(findPurchasedStorageCard().text().replace(/\s+/g, ' ')).toContain('0 GiB');
});
}); });
}); });
}); });
...@@ -5,6 +5,7 @@ import { ...@@ -5,6 +5,7 @@ import {
calculateUsedAndRemStorage, calculateUsedAndRemStorage,
parseGetProjectStorageResults, parseGetProjectStorageResults,
descendingStorageUsageSort, descendingStorageUsageSort,
formatSizeAndSplit,
} from 'ee/usage_quotas/storage/utils'; } from 'ee/usage_quotas/storage/utils';
import { import {
projectData, projectData,
...@@ -78,6 +79,11 @@ describe('formatUsageSize', () => { ...@@ -78,6 +79,11 @@ describe('formatUsageSize', () => {
`('returns $expected from $input', ({ input, expected }) => { `('returns $expected from $input', ({ input, expected }) => {
expect(formatUsageSize(input)).toBe(expected); expect(formatUsageSize(input)).toBe(expected);
}); });
it('render the output with unit separator when unitSeparator param is passed', () => {
expect(formatUsageSize(1000, '-')).toBe('1.0-KiB');
expect(formatUsageSize(1000, ' ')).toBe('1.0 KiB');
});
}); });
describe('calculateUsedAndRemStorage', () => { describe('calculateUsedAndRemStorage', () => {
...@@ -132,3 +138,13 @@ describe('descendingStorageUsageSort', () => { ...@@ -132,3 +138,13 @@ describe('descendingStorageUsageSort', () => {
expect(sorted).toEqual(expectedSorted); expect(sorted).toEqual(expectedSorted);
}); });
}); });
describe('formatSizeAndSplit', () => {
it('returns null if passed parameter is null', () => {
expect(formatSizeAndSplit(null)).toBe(null);
});
it('returns formatted size as object { value, unit }', () => {
expect(formatSizeAndSplit(1000)).toEqual({ value: '1.0', unit: 'KiB' });
});
});
...@@ -969,9 +969,6 @@ msgstr[1] "" ...@@ -969,9 +969,6 @@ msgstr[1] ""
msgid "%{service_ping_link_start}What information is shared with GitLab Inc.?%{service_ping_link_end}" msgid "%{service_ping_link_start}What information is shared with GitLab Inc.?%{service_ping_link_end}"
msgstr "" msgstr ""
msgid "%{size} %{unit}"
msgstr ""
msgid "%{size} GiB" msgid "%{size} GiB"
msgstr "" msgstr ""
...@@ -40445,6 +40442,9 @@ msgstr "" ...@@ -40445,6 +40442,9 @@ msgstr ""
msgid "UsageQuota|Buy additional minutes" msgid "UsageQuota|Buy additional minutes"
msgstr "" msgstr ""
msgid "UsageQuota|Buy storage"
msgstr ""
msgid "UsageQuota|CI minutes usage by month" msgid "UsageQuota|CI minutes usage by month"
msgstr "" msgstr ""
...@@ -40475,10 +40475,10 @@ msgstr "" ...@@ -40475,10 +40475,10 @@ msgstr ""
msgid "UsageQuota|LFS storage" msgid "UsageQuota|LFS storage"
msgstr "" msgstr ""
msgid "UsageQuota|Learn more about excess storage usage" msgid "UsageQuota|Learn more about usage quotas."
msgstr "" msgstr ""
msgid "UsageQuota|Learn more about usage quotas" msgid "UsageQuota|Namespace storage used"
msgstr "" msgstr ""
msgid "UsageQuota|No CI minutes usage data available." msgid "UsageQuota|No CI minutes usage data available."
...@@ -40499,7 +40499,10 @@ msgstr "" ...@@ -40499,7 +40499,10 @@ msgstr ""
msgid "UsageQuota|Purchase more storage" msgid "UsageQuota|Purchase more storage"
msgstr "" msgstr ""
msgid "UsageQuota|Purchased storage available" msgid "UsageQuota|Purchased storage"
msgstr ""
msgid "UsageQuota|Purchased storage used"
msgstr "" msgstr ""
msgid "UsageQuota|Repository" msgid "UsageQuota|Repository"
...@@ -40526,24 +40529,12 @@ msgstr "" ...@@ -40526,24 +40529,12 @@ msgstr ""
msgid "UsageQuota|Storage used" msgid "UsageQuota|Storage used"
msgstr "" msgstr ""
msgid "UsageQuota|This is the total amount of storage used across your projects within this namespace."
msgstr ""
msgid "UsageQuota|This is the total amount of storage used by projects above the free %{actualRepositorySizeLimit} storage limit."
msgstr ""
msgid "UsageQuota|This namespace contains locked projects" msgid "UsageQuota|This namespace contains locked projects"
msgstr "" msgstr ""
msgid "UsageQuota|This namespace has no projects which use shared runners" msgid "UsageQuota|This namespace has no projects which use shared runners"
msgstr "" msgstr ""
msgid "UsageQuota|Total excess storage used"
msgstr ""
msgid "UsageQuota|Total namespace storage used"
msgstr ""
msgid "UsageQuota|Unlimited" msgid "UsageQuota|Unlimited"
msgstr "" 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