diff --git a/ee/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue b/ee/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue index 60cc94a9241e206cc47ea716fef7224e4631c3c6..288430edcbe9b63aa8ea8827712e40f7c5863316 100644 --- a/ee/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue +++ b/ee/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue @@ -2,6 +2,7 @@ import { GlAlert, GlKeysetPagination } from '@gitlab/ui'; import { captureException } from '~/ci/runner/sentry_utils'; import { convertToSnakeCase } from '~/lib/utils/text_utility'; +import { __ } from '~/locale'; import NamespaceStorageQuery from '../queries/namespace_storage.query.graphql'; import GetDependencyProxyTotalSizeQuery from '../queries/dependency_proxy_usage.query.graphql'; import { parseGetStorageResults } from '../utils'; @@ -65,6 +66,7 @@ export default { i18n: { NAMESPACE_STORAGE_ERROR_MESSAGE, NAMESPACE_STORAGE_BREAKDOWN_SUBTITLE, + search: __('Search'), }, data() { return { @@ -186,7 +188,7 @@ export default { <div class="gl-bg-gray-10 gl-p-5 gl-display-flex"> <search-and-sort-bar :namespace="namespaceId" - :search-input-placeholder="s__('UsageQuota|Search')" + :search-input-placeholder="$options.i18n.search" @onFilter="onSearch" /> </div> diff --git a/ee/app/assets/javascripts/usage_quotas/storage/components/storage_statistics_card.vue b/ee/app/assets/javascripts/usage_quotas/storage/components/storage_statistics_card.vue index 7bd37ed17b12d590649382386d74cd04727fbdda..278d68a02e7f92a364ab0d8bbe9c8b4cc91a668a 100644 --- a/ee/app/assets/javascripts/usage_quotas/storage/components/storage_statistics_card.vue +++ b/ee/app/assets/javascripts/usage_quotas/storage/components/storage_statistics_card.vue @@ -1,10 +1,17 @@ <script> -import { GlCard, GlProgressBar, GlSkeletonLoader } from '@gitlab/ui'; -import { formatSizeAndSplit } from 'ee/usage_quotas/storage/utils'; +import { GlCard, GlProgressBar, GlSkeletonLoader, GlIcon, GlLink } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import { numberToHumanSizeSplit } from '~/lib/utils/number_utils'; +import { usageQuotasHelpPaths } from '~/usage_quotas/storage/constants'; +import { + STORAGE_STATISTICS_PERCENTAGE_REMAINING, + STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, + STORAGE_STATISTICS_NAMESPACE_STORAGE_USED, +} from '../constants'; export default { name: 'StorageStatisticsCard', - components: { GlCard, GlProgressBar, GlSkeletonLoader }, + components: { GlCard, GlProgressBar, GlSkeletonLoader, GlIcon, GlLink }, props: { totalStorage: { type: Number, @@ -22,45 +29,44 @@ export default { }, }, computed: { - formattedUsage() { - return this.formatSizeAndSplit(this.usedStorage); + storageUsed() { + if (!this.usedStorage) { + // if there is no used storage, we want + // to show `0` instead of the formatted `0.0` + return '0'; + } + return numberToHumanSizeSplit(this.usedStorage, 1); }, - formattedTotal() { - return this.formatSizeAndSplit(this.totalStorage); + storageTotal() { + if (!this.totalStorage) { + return null; + } + return numberToHumanSizeSplit(this.totalStorage, 1); }, - percentage() { + percentageUsed() { // 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); + const usedRatio = Math.max(Math.round((this.usedStorage / this.totalStorage) * 100), 0); + return Math.min(usedRatio, 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'; + percentageRemaining() { + if (this.percentageUsed === null) { + return null; } - return this.formattedUsage?.value; - }, - usageUnit() { - return this.formattedUsage?.unit; - }, - totalValue() { - return this.formattedTotal?.value; - }, - totalUnit() { - return this.formattedTotal?.unit; - }, - shouldRenderTotalBlock() { - return this.totalStorage && this.usedStorage !== null; - }, - shouldShowProgressBar() { - return this.percentage !== null; + + const percentageRemaining = Math.max(100 - this.percentageUsed, 0); + + return sprintf(STORAGE_STATISTICS_PERCENTAGE_REMAINING, { + percentageRemaining, + }); }, }, - methods: { - formatSizeAndSplit, + i18n: { + USED_STORAGE_HELP_LINK: usageQuotasHelpPaths.usageQuotas, + STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, + STORAGE_STATISTICS_NAMESPACE_STORAGE_USED, }, }; </script> @@ -74,21 +80,33 @@ export default { </gl-skeleton-loader> <div v-else> - <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 class="gl-font-weight-bold" data-testid="namespace-storage-card-title"> + {{ $options.i18n.STORAGE_STATISTICS_NAMESPACE_STORAGE_USED }} + + <gl-link + :href="$options.i18n.USED_STORAGE_HELP_LINK" + target="_blank" + class="gl-ml-2" + :aria-label="$options.i18n.STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE" + > + <gl-icon name="question-o" /> + </gl-link> + </div> + <div class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-my-3"> + {{ storageUsed[0] }} + <span v-if="storageUsed[1]" class="gl-font-lg">{{ storageUsed[1] }}</span> + <span v-if="storageTotal"> + / + {{ storageTotal[0] }} + <span class="gl-font-lg">{{ storageTotal[1] }}</span> + </span> </div> - <p class="gl-font-weight-bold gl-mb-0" data-testid="description"> - <slot name="description"></slot> - </p> - <gl-progress-bar v-if="shouldShowProgressBar" :value="percentage" class="gl-mt-4" /> + <template v-if="percentageUsed !== null"> + <gl-progress-bar :value="percentageUsed" class="gl-my-4" /> + <div data-testid="namespace-storage-percentage-remaining"> + {{ percentageRemaining }} + </div> + </template> </div> </gl-card> </template> diff --git a/ee/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue b/ee/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue index cd1589d249a2c7cdd36c13e768f896e1db60c70f..d8331701e7b8381a9ce54f1330b173b34968e2e1 100644 --- a/ee/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue +++ b/ee/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue @@ -1,13 +1,12 @@ <script> import { GlSprintf, GlIcon, GlLink, GlButton, GlCard, GlSkeletonLoader } from '@gitlab/ui'; import { sprintf } from '~/locale'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; import StorageStatisticsCard from 'ee/usage_quotas/storage/components/storage_statistics_card.vue'; import { usageQuotasHelpPaths } from '~/usage_quotas/storage/constants'; -import { formatSizeAndSplit } from 'ee/usage_quotas/storage/utils'; import { BUY_STORAGE, STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, - STORAGE_STATISTICS_NAMESPACE_STORAGE_USED, STORAGE_INCLUDED_IN_PLAN_PROJECT_ENFORCEMENT, STORAGE_INCLUDED_IN_PLAN_NAMESPACE_ENFORCEMENT, PROJECT_ENFORCEMENT_TYPE, @@ -17,6 +16,7 @@ import { PROJECT_ENFORCEMENT_TYPE_SUBTITLE, NAMESPACE_ENFORCEMENT_TYPE_SUBTITLE, } from '../constants'; +import NumberToHumanSize from './number_to_human_size.vue'; export default { components: { @@ -27,6 +27,7 @@ export default { GlCard, GlSkeletonLoader, StorageStatisticsCard, + NumberToHumanSize, }, inject: [ 'purchaseStorageUrl', @@ -57,7 +58,6 @@ export default { usedUsageHelpLink: usageQuotasHelpPaths.usageQuotas, usedUsageHelpText: STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, purchaseButtonText: BUY_STORAGE, - totalUsageDescription: STORAGE_STATISTICS_NAMESPACE_STORAGE_USED, namespaceStorageOverviewSubtitle: NAMESPACE_STORAGE_OVERVIEW_SUBTITLE, storageStatisticsPurchasedStorage: STORAGE_STATISTICS_PURCHASED_STORAGE, storageStatisticsTotalStorage: STORAGE_STATISTICS_TOTAL_STORAGE, @@ -93,14 +93,14 @@ export default { }); }, includedStorage() { - const formatted = formatSizeAndSplit(this.namespacePlanStorageIncluded || 0); - - return `${formatted.value} ${formatted.unit}`; + return numberToHumanSize(this.namespacePlanStorageIncluded || 0, 1); }, purchasedTotalStorage() { - const formatted = formatSizeAndSplit(this.additionalPurchasedStorageSize || 0); + if (!this.additionalPurchasedStorageSize) { + return 0; + } - return `${formatted.value} ${formatted.unit}`; + return numberToHumanSize(this.additionalPurchasedStorageSize || 0, 1); }, totalStorage() { return ( @@ -108,10 +108,6 @@ export default { Number(this.additionalPurchasedStorageSize || 0) ); }, - totalStorageFormatted() { - const formatted = formatSizeAndSplit(this.totalStorage); - return `${formatted.value} ${formatted.unit}`; - }, }, }; </script> @@ -131,7 +127,7 @@ export default { {{ $options.i18n.purchaseButtonText }} </gl-button> </div> - <p> + <p class="gl-mb-0"> <gl-sprintf :message="enforcementTypeSubtitle"> <template #link="{ content }"> <gl-link :href="enforcementTypeLearnMoreUrl">{{ content }}</gl-link> @@ -143,23 +139,9 @@ export default { :used-storage="usedStorage" :total-storage="totalStorage" :loading="loading" - data-testid="namespace-usage-card" data-qa-selector="namespace_usage_total" class="gl-w-full" - > - <template #description> - {{ $options.i18n.totalUsageDescription }} - - <gl-link - :href="$options.i18n.usedUsageHelpLink" - target="_blank" - class="gl-ml-2" - :aria-label="$options.i18n.usedUsageHelpText" - > - <gl-icon name="question-o" /> - </gl-link> - </template> - </storage-statistics-card> + /> <gl-card v-if="namespacePlanName" class="gl-w-full" data-testid="storage-detail-card"> <gl-skeleton-loader v-if="loading" :height="64"> <rect width="140" height="30" x="5" y="0" rx="4" /> @@ -178,13 +160,23 @@ export default { class="gl-display-flex gl-justify-content-space-between" data-testid="storage-purchased" > - <div class="gl-w-80p">{{ $options.i18n.storageStatisticsPurchasedStorage }}</div> + <div class="gl-w-80p"> + {{ $options.i18n.storageStatisticsPurchasedStorage }} + <gl-link + :href="$options.i18n.purchasedUsageHelpLink" + target="_blank" + class="gl-ml-2" + :aria-label="$options.i18n.purchasedUsageHelpText" + > + <gl-icon name="question-o" /> + </gl-link> + </div> <div class="gl-white-space-nowrap">{{ purchasedTotalStorage }}</div> </div> <hr /> <div class="gl-display-flex gl-justify-content-space-between" data-testid="total-storage"> <div class="gl-w-80p">{{ $options.i18n.storageStatisticsTotalStorage }}</div> - <div class="gl-white-space-nowrap">{{ totalStorageFormatted }}</div> + <number-to-human-size class="gl-white-space-nowrap" :value="totalStorage" /> </div> </div> </gl-card> diff --git a/ee/app/assets/javascripts/usage_quotas/storage/constants.js b/ee/app/assets/javascripts/usage_quotas/storage/constants.js index ad1c58d0539f4158fbb5e39ecec94600677ad1c5..c8f8f56beda6fe928418cb332bddeb32bb4532a7 100644 --- a/ee/app/assets/javascripts/usage_quotas/storage/constants.js +++ b/ee/app/assets/javascripts/usage_quotas/storage/constants.js @@ -25,6 +25,9 @@ export const NAMESPACE_STORAGE_ERROR_MESSAGE = s__( ); export const STORAGE_STATISTICS_NAMESPACE_STORAGE_USED = s__('UsageQuota|Namespace storage used'); +export const STORAGE_STATISTICS_PERCENTAGE_REMAINING = s__( + 'UsageQuota|%{percentageRemaining}%% namespace storage remaining.', +); export const STORAGE_STATISTICS_TOTAL_STORAGE = s__('UsageQuota|Total storage'); export const STORAGE_INCLUDED_IN_PLAN_PROJECT_ENFORCEMENT = s__( diff --git a/ee/app/assets/javascripts/usage_quotas/storage/utils.js b/ee/app/assets/javascripts/usage_quotas/storage/utils.js index 1a8c729e0f54f8d9386539c2952e36a52af48455..b0b3abfeb67cba612130d3743a80ce143bd778c9 100644 --- a/ee/app/assets/javascripts/usage_quotas/storage/utils.js +++ b/ee/app/assets/javascripts/usage_quotas/storage/utils.js @@ -1,85 +1,6 @@ -import { numberToHumanSize, bytesToKiB } from '~/lib/utils/number_utils'; -import { gibibytes, kibibytes } from '~/lib/utils/unit_format'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; -/** - * Formats given bytes to formatted human readable size - * - * We want to display all units above bytes. Hence - * converting bytesToKiB before passing it to - * `getFormatter` - - * @param sizeInBytes - * @param {String} unitSeparator - * @returns {String} - */ -export const formatUsageSize = (sizeInBytes, unitSeparator = '') => { - return kibibytes(bytesToKiB(sizeInBytes), 1, { unitSeparator }); -}; - -/** - * Parses each project to add additional purchased data - * equally so that locked projects can be unlocked. - * - * For example, if a group contains the below projects and - * project 2, 3 have exceeded the default 10.0 GB limit. - * 2 and 3 will remain locked until user purchases additional - * data. - * - * Project 1: 7.0GB - * Project 2: 13.0GB Locked - * Project 3: 12.0GB Locked - * - * If user purchases X GB, it will be equally available - * to all the locked projects for further use. - * - * @param {Object} data project - * @param {Number} purchasedStorageRemaining Remaining purchased data in bytes - * @returns {Object} - */ -export const calculateUsedAndRemStorage = (project, purchasedStorageRemaining) => { - // We only consider repo size and lfs object size as of %13.5 - const totalCalculatedUsedStorage = - project.statistics.repositorySize + project.statistics.lfsObjectsSize; - // If a project size is above the default limit, then the remaining - // storage value will be calculated on top of the project size as - // opposed to the default limit. - // This - const totalCalculatedStorageLimit = - totalCalculatedUsedStorage > project.actualRepositorySizeLimit - ? totalCalculatedUsedStorage + purchasedStorageRemaining - : project.actualRepositorySizeLimit + purchasedStorageRemaining; - return { - ...project, - totalCalculatedUsedStorage, - totalCalculatedStorageLimit, - }; -}; -/** - * Parses projects coming in from GraphQL response - * and patches each project with purchased related - * data - * - * @param {Array} params.projects list of projects - * @param {Number} params.additionalPurchasedStorageSize Amt purchased in bytes - * @param {Number} params.totalRepositorySizeExcess Sum of excess amounts on all projects - * @returns {Array} - */ -export const parseProjects = ({ - projects, - additionalPurchasedStorageSize = 0, - totalRepositorySizeExcess = 0, -}) => { - const purchasedStorageRemaining = Math.max( - 0, - additionalPurchasedStorageSize - totalRepositorySizeExcess, - ); - - return projects.nodes.map((project) => - calculateUsedAndRemStorage(project, purchasedStorageRemaining), - ); -}; - /** * This method parses the results from `getNamespaceStorageStatistics` * call. @@ -113,11 +34,7 @@ export const parseGetStorageResults = (data) => { return { projects: { - data: parseProjects({ - projects, - additionalPurchasedStorageSize, - totalRepositorySizeExcess, - }), + data: projects.nodes, pageInfo: projects.pageInfo, }, additionalPurchasedStorageSize, @@ -131,27 +48,3 @@ export const parseGetStorageResults = (data) => { limit: storageSizeLimit, }; }; - -/** - * 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 }; -} diff --git a/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb b/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb index e8c877294dfeca7b6a89e840c330f4516c64c7ec..cc4343ad232bc825ffc6648d466469c9e1b50a00 100644 --- a/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb +++ b/ee/spec/features/groups/usage_quotas/storage_tab_spec.rb @@ -101,7 +101,7 @@ # A cost factor for forks of 0.1 means that forks consume only 10% of their storage size. # So this is the total storage_size (300 MB) - 90% of the public_forks_storage_size (90 MB). - expect(page).to have_css('[data-testid="denominator"]', text: "210.0 MiB") + expect(page).to have_text('Namespace storage used 210.0 MiB') end end diff --git a/ee/spec/frontend/usage_quotas/storage/components/storage_statistics_card_spec.js b/ee/spec/frontend/usage_quotas/storage/components/storage_statistics_card_spec.js index 8af039ddfdd3d1e8ac2757ae9004b3b86bb5d35c..d3901740f7f95ed5bf2e09cfff667f3a4417094e 100644 --- a/ee/spec/frontend/usage_quotas/storage/components/storage_statistics_card_spec.js +++ b/ee/spec/frontend/usage_quotas/storage/components/storage_statistics_card_spec.js @@ -1,5 +1,11 @@ -import { GlProgressBar, GlSkeletonLoader } from '@gitlab/ui'; +import { GlProgressBar, GlSkeletonLoader, GlLink } from '@gitlab/ui'; +import { numberToHumanSize } from '~/lib/utils/number_utils'; +import { usageQuotasHelpPaths } from '~/usage_quotas/storage/constants'; import StorageStatisticsCard from 'ee/usage_quotas/storage/components/storage_statistics_card.vue'; +import { + STORAGE_STATISTICS_NAMESPACE_STORAGE_USED, + STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, +} from 'ee/usage_quotas/storage/constants'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { statisticsCardDefaultProps } from '../mock_data'; @@ -15,66 +21,93 @@ describe('StorageStatisticsCard', () => { }); }; - const findDenominatorBlock = () => wrapper.findByTestId('denominator'); - const findTotalBlock = () => wrapper.findByTestId('denominator-total'); - const findDescriptionBlock = () => wrapper.findByTestId('description'); + const findCardTitle = () => wrapper.findByTestId('namespace-storage-card-title'); + const findPercentageRemaining = () => + wrapper.findByTestId('namespace-storage-percentage-remaining'); const findProgressBar = () => wrapper.findComponent(GlProgressBar); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); - 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'); + describe('skeleton loader', () => { + it('renders skeleton loader when loading prop is true', () => { + createComponent({ loading: true }); + expect(findSkeletonLoader().exists()).toBe(true); }); - it('does not render total part of denominator if there is no total passed', () => { - createComponent({ totalStorage: null }); - - expect(findDenominatorBlock().text()).toMatchInterpolatedText('50.0 KiB'); + it('does not render skeleton loader when loading prop is false', () => { + createComponent({ loading: false }); + expect(findSkeletonLoader().exists()).toBe(false); }); + }); - it('does not render total block if totalStorage and usedStorage are not passed', () => { - createComponent({ - usedStorage: null, - totalStorage: null, - }); - - expect(findTotalBlock().exists()).toBe(false); + describe('card title', () => { + beforeEach(() => { + createComponent(); }); - 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'); + it('renders the card title', () => { + expect(findCardTitle().text()).toBe(STORAGE_STATISTICS_NAMESPACE_STORAGE_USED); }); - }); - describe('slots', () => { - it('renders description slot', () => { - createComponent(); - expect(findDescriptionBlock().text()).toBe('storage-statistics-card description slot'); + it('renders the help link with the proper attributes', () => { + expect(findCardTitle().findComponent(GlLink).attributes('href')).toBe( + usageQuotasHelpPaths.usageQuotas, + ); + expect(findCardTitle().findComponent(GlLink).attributes('aria-label')).toBe( + STORAGE_STATISTICS_USAGE_QUOTA_LEARN_MORE, + ); }); }); - it('renders progress bar with correct percentage', () => { - createComponent({ totalStorage: 10, usedStorage: 5 }); + describe.each` + totalStorage | usedStorage | percentageUsage | percentageRemaining + ${10} | ${3} | ${30} | ${70} + ${10} | ${0} | ${0} | ${100} + ${10} | ${-1} | ${0} | ${100} + ${10} | ${undefined} | ${false} | ${false} + ${10} | ${null} | ${false} | ${false} + ${3} | ${10} | ${100} | ${0} + ${0} | ${10} | ${false} | ${false} + ${-1} | ${10} | ${0} | ${100} + `( + 'UI behavior when totalStorage: $totalStorage, usedStorage: $usedStorage', + ({ totalStorage, usedStorage, percentageUsage, percentageRemaining }) => { + beforeEach(() => { + createComponent({ totalStorage, usedStorage }); + }); - expect(findProgressBar().attributes('value')).toBe(String(50)); - }); + it('renders the used and total storage block', () => { + const usedStorageFormatted = + usedStorage === 0 || typeof usedStorage !== 'number' + ? '0' + : numberToHumanSize(usedStorage); - describe('skeleton loader', () => { - it('renders skeleton loader when loading prop is true', () => { - createComponent({ loading: true }); - expect(findSkeletonLoader().exists()).toBe(true); - }); + const componentText = wrapper.text().replace(/[\s\n]+/g, ' '); - it('does not render skeleton loader when loading prop is false', () => { - createComponent({ loading: false }); - expect(findSkeletonLoader().exists()).toBe(false); - }); - }); + if (totalStorage === 0) { + expect(componentText).toContain(` ${usedStorageFormatted}`); + expect(componentText).not.toContain('/'); + } else { + expect(componentText).toContain( + ` ${usedStorageFormatted} / ${numberToHumanSize(totalStorage)}`, + ); + } + }); + + it(`renders the progress bar as ${percentageUsage}`, () => { + if (percentageUsage === false) { + expect(findProgressBar().exists()).toBe(false); + } else { + expect(findProgressBar().attributes('value')).toBe(String(percentageUsage)); + } + }); + + it(`renders the percentage remaining as ${percentageRemaining}`, () => { + if (percentageRemaining === false) { + expect(findPercentageRemaining().exists()).toBe(false); + } else { + expect(findPercentageRemaining().text()).toContain(String(percentageRemaining)); + } + }); + }, + ); }); diff --git a/ee/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js b/ee/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js index cef751b6eda7410d7d95c23f5a5035a861d58204..bbb37989062ca6ec8ddd5a7f8949ef3583e6f0de 100644 --- a/ee/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js +++ b/ee/spec/frontend/usage_quotas/storage/components/storage_usage_statistics_spec.js @@ -1,5 +1,6 @@ import { GlButton, GlLink, GlSprintf, GlProgressBar } from '@gitlab/ui'; import StorageStatisticsCard from 'ee/usage_quotas/storage/components/storage_statistics_card.vue'; +import numberToHumanSize from 'ee/usage_quotas/storage/components/number_to_human_size.vue'; import { shallowMountExtended } from 'helpers/vue_test_utils_helper'; import { NAMESPACE_STORAGE_OVERVIEW_SUBTITLE, @@ -26,6 +27,7 @@ describe('StorageUsageStatistics', () => { }, stubs: { StorageStatisticsCard, + numberToHumanSize, GlSprintf, GlButton, GlLink, @@ -34,7 +36,7 @@ describe('StorageUsageStatistics', () => { }); }; - const findNamespaceStorageCard = () => wrapper.findByTestId('namespace-usage-card'); + const findNamespaceStorageCard = () => wrapper.findComponent(StorageStatisticsCard); const findStorageDetailCard = () => wrapper.findByTestId('storage-detail-card'); const findStorageIncludedInPlan = () => wrapper.findByTestId('storage-included-in-plan'); const findStoragePurchased = () => wrapper.findByTestId('storage-purchased'); @@ -90,16 +92,9 @@ describe('StorageUsageStatistics', () => { }); describe('StorageStatisticsCard', () => { - beforeEach(() => { + it('passes the correct props to StorageStatisticsCard', () => { createComponent(); - }); - it('renders card description with help link', () => { - expect(findNamespaceStorageCard().text()).toContain('Namespace storage used'); - expect(findNamespaceStorageCard().findComponent(GlLink).exists()).toBe(true); - }); - - it('passes the correct props to StorageStatisticsCard', () => { expect(findNamespaceStorageCard().props()).toEqual({ usedStorage: withRootStorageStatistics.rootStorageStatistics.storageSize, totalStorage: @@ -136,7 +131,7 @@ describe('StorageUsageStatistics', () => { }); it('renders purchased storage', () => { - expect(findStoragePurchased().text()).toContain('0.3 KiB'); + expect(findStoragePurchased().text()).toContain('321 B'); }); it('renders total storage', () => { diff --git a/ee/spec/frontend/usage_quotas/storage/utils_spec.js b/ee/spec/frontend/usage_quotas/storage/utils_spec.js index ff618e8036dec2ebe4ca40eaa81b01b23209f21d..b3a0aa682d704ef902b4e8a917890e40b0cc0753 100644 --- a/ee/spec/frontend/usage_quotas/storage/utils_spec.js +++ b/ee/spec/frontend/usage_quotas/storage/utils_spec.js @@ -1,88 +1,22 @@ -import { - formatUsageSize, - parseProjects, - calculateUsedAndRemStorage, - formatSizeAndSplit, -} from 'ee/usage_quotas/storage/utils'; - -import { - projects as mockProjectsData, - mockGetNamespaceStorageStatisticsGraphQLResponse, -} from './mock_data'; - -describe('formatUsageSize', () => { - it.each` - input | expected - ${0} | ${'0.0KiB'} - ${999} | ${'1.0KiB'} - ${1000} | ${'1.0KiB'} - ${10240} | ${'10.0KiB'} - ${1024 * 10 ** 5} | ${'97.7MiB'} - ${10 ** 6} | ${'976.6KiB'} - ${1024 * 10 ** 6} | ${'976.6MiB'} - ${10 ** 8} | ${'95.4MiB'} - ${1024 * 10 ** 8} | ${'95.4GiB'} - ${10 ** 10} | ${'9.3GiB'} - ${10 ** 12} | ${'931.3GiB'} - ${10 ** 15} | ${'909.5TiB'} - `('returns $expected from $input', ({ input, 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', () => { - it.each` - description | project | purchasedStorageRemaining | totalCalculatedUsedStorage | totalCalculatedStorageLimit - ${'project in error state and purchased 0'} | ${mockProjectsData[0]} | ${0} | ${419430} | ${419430} - ${'project in error state and purchased 10000'} | ${mockProjectsData[0]} | ${100000} | ${419430} | ${519430} - ${'project in warning state and purchased 0'} | ${mockProjectsData[1]} | ${0} | ${0} | ${100000} - ${'project in warning state and purchased 10000'} | ${mockProjectsData[1]} | ${100000} | ${0} | ${200000} - ${'project within limit and purchased 0'} | ${mockProjectsData[2]} | ${0} | ${41943} | ${100000} - ${'project within limit and purchased 10000'} | ${mockProjectsData[2]} | ${100000} | ${41943} | ${200000} - `( - 'returns used: $totalCalculatedUsedStorage and remaining: $totalCalculatedStorageLimit storage for $description', - ({ - project, - purchasedStorageRemaining, - totalCalculatedUsedStorage, - totalCalculatedStorageLimit, - }) => { - const result = calculateUsedAndRemStorage(project, purchasedStorageRemaining); - - expect(result.totalCalculatedUsedStorage).toBe(totalCalculatedUsedStorage); - expect(result.totalCalculatedStorageLimit).toBe(totalCalculatedStorageLimit); - }, - ); -}); - -describe('parseProjects', () => { - it('ensures all projects have totalCalculatedUsedStorage and totalCalculatedStorageLimit', () => { - const projects = parseProjects({ - projects: mockGetNamespaceStorageStatisticsGraphQLResponse.data.namespace.projects, - additionalPurchasedStorageSize: 10000, - totalRepositorySizeExcess: 5000, - }); - - projects.forEach((project) => { - expect(project).toMatchObject({ - totalCalculatedUsedStorage: expect.any(Number), - totalCalculatedStorageLimit: expect.any(Number), - }); - }); - }); -}); - -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' }); +import { parseGetStorageResults } from 'ee/usage_quotas/storage/utils'; +import { mockGetNamespaceStorageStatisticsGraphQLResponse } from './mock_data'; + +describe('parseGetStorageResults', () => { + it('returns the object keys we use', () => { + const objectKeys = Object.keys( + parseGetStorageResults(mockGetNamespaceStorageStatisticsGraphQLResponse.data), + ); + expect(objectKeys).toEqual([ + 'projects', + 'additionalPurchasedStorageSize', + 'actualRepositorySizeLimit', + 'containsLockedProjects', + 'repositorySizeExcessProjectCount', + 'totalRepositorySize', + 'totalRepositorySizeExcess', + 'totalUsage', + 'rootStorageStatistics', + 'limit', + ]); }); }); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index f7a683b02bd8f409d036518f46b5764075f63687..f714e09878c5ecb9302bf7a706aeaeab254f49b4 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -49857,6 +49857,9 @@ msgstr "" msgid "UsageQuota|%{linkTitle} help link" msgstr "" +msgid "UsageQuota|%{percentageRemaining}%% namespace storage remaining." +msgstr "" + msgid "UsageQuota|%{storage_limit_link_start}A namespace storage limit%{link_end} will soon be enforced for the %{strong_start}%{namespace_name}%{strong_end} namespace. %{extra_message}" msgstr "" @@ -49968,9 +49971,6 @@ msgstr "" msgid "UsageQuota|Registry" msgstr "" -msgid "UsageQuota|Search" -msgstr "" - msgid "UsageQuota|Seats" msgstr ""