diff --git a/config/feature_flags/development/other_storage_tab.yml b/config/feature_flags/development/other_storage_tab.yml deleted file mode 100644 index 8ce4848f98bfe651b3c17b605ab46d7d3c28c919..0000000000000000000000000000000000000000 --- a/config/feature_flags/development/other_storage_tab.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: other_storage_tab -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57121 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/325967 -milestone: '13.11' -type: development -group: group::fulfillment -default_enabled: false diff --git a/ee/app/assets/javascripts/other_storage_counter/components/app.vue b/ee/app/assets/javascripts/other_storage_counter/components/app.vue deleted file mode 100644 index c482a6184c3fb37053e9387ab3831ab0261cc0bd..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/app.vue +++ /dev/null @@ -1,249 +0,0 @@ -<script> -import { - GlLink, - GlSprintf, - GlModalDirective, - GlButton, - GlIcon, - GlKeysetPagination, -} from '@gitlab/ui'; -import { parseBoolean } from '~/lib/utils/common_utils'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import { PROJECTS_PER_PAGE } from '../constants'; -import query from '../queries/storage.query.graphql'; -import { formatUsageSize, parseGetStorageResults } from '../utils'; -import ProjectsTable from './projects_table.vue'; -import StorageInlineAlert from './storage_inline_alert.vue'; -import TemporaryStorageIncreaseModal from './temporary_storage_increase_modal.vue'; -import UsageGraph from './usage_graph.vue'; -import UsageStatistics from './usage_statistics.vue'; - -export default { - name: 'OtherStorageCounterApp', - components: { - GlLink, - GlIcon, - GlButton, - GlSprintf, - UsageGraph, - ProjectsTable, - UsageStatistics, - StorageInlineAlert, - GlKeysetPagination, - TemporaryStorageIncreaseModal, - }, - directives: { - GlModalDirective, - }, - mixins: [glFeatureFlagsMixin()], - props: { - namespacePath: { - type: String, - required: true, - }, - helpPagePath: { - type: String, - required: true, - }, - purchaseStorageUrl: { - type: String, - required: false, - default: null, - }, - isTemporaryStorageIncreaseVisible: { - type: String, - required: false, - default: 'false', - }, - }, - apollo: { - namespace: { - query, - variables() { - return { - fullPath: this.namespacePath, - searchTerm: this.searchTerm, - withExcessStorageData: this.isAdditionalStorageFlagEnabled, - first: PROJECTS_PER_PAGE, - }; - }, - update: parseGetStorageResults, - result() { - this.firstFetch = false; - }, - }, - }, - data() { - return { - namespace: {}, - searchTerm: '', - firstFetch: true, - }; - }, - computed: { - namespaceProjects() { - return this.namespace?.projects?.data ?? []; - }, - isStorageIncreaseModalVisible() { - return parseBoolean(this.isTemporaryStorageIncreaseVisible); - }, - isAdditionalStorageFlagEnabled() { - return this.glFeatures.additionalRepoStorageByNamespace; - }, - - formattedNamespaceLimit() { - return formatUsageSize(this.namespace.limit); - }, - storageStatistics() { - if (!this.namespace) { - return null; - } - - return { - totalRepositorySize: this.namespace.totalRepositorySize, - actualRepositorySizeLimit: this.namespace.actualRepositorySizeLimit, - totalRepositorySizeExcess: this.namespace.totalRepositorySizeExcess, - additionalPurchasedStorageSize: this.namespace.additionalPurchasedStorageSize, - }; - }, - isQueryLoading() { - return this.$apollo.queries.namespace.loading; - }, - pageInfo() { - return this.namespace.projects?.pageInfo ?? {}; - }, - shouldShowStorageInlineAlert() { - if (this.firstFetch) { - // for initial load check if the data fetch is done (isQueryLoading) - return this.isAdditionalStorageFlagEnabled && !this.isQueryLoading; - } - // for all subsequent queries the storage inline alert doesn't - // have to be re-rendered as the data from graphql will remain - // the same. - return this.isAdditionalStorageFlagEnabled; - }, - showPagination() { - return Boolean(this.pageInfo?.hasPreviousPage || this.pageInfo?.hasNextPage); - }, - }, - methods: { - handleSearch(input) { - // if length === 0 clear the search, if length > 2 update the search term - if (input.length === 0 || input.length > 2) { - this.searchTerm = input; - } - }, - fetchMoreProjects(vars) { - this.$apollo.queries.namespace.fetchMore({ - variables: { - fullPath: this.namespacePath, - withExcessStorageData: this.isAdditionalStorageFlagEnabled, - first: PROJECTS_PER_PAGE, - ...vars, - }, - updateQuery(previousResult, { fetchMoreResult }) { - return fetchMoreResult; - }, - }); - }, - onPrev(before) { - if (this.pageInfo?.hasPreviousPage) { - this.fetchMoreProjects({ before }); - } - }, - onNext(after) { - if (this.pageInfo?.hasNextPage) { - this.fetchMoreProjects({ after }); - } - }, - }, - modalId: 'temporary-increase-storage-modal', -}; -</script> -<template> - <div> - <storage-inline-alert - v-if="shouldShowStorageInlineAlert" - :contains-locked-projects="namespace.containsLockedProjects" - :repository-size-excess-project-count="namespace.repositorySizeExcessProjectCount" - :total-repository-size-excess="namespace.totalRepositorySizeExcess" - :total-repository-size="namespace.totalRepositorySize" - :additional-purchased-storage-size="namespace.additionalPurchasedStorageSize" - :actual-repository-size-limit="namespace.actualRepositorySizeLimit" - /> - <div v-if="isAdditionalStorageFlagEnabled && storageStatistics"> - <usage-statistics - :root-storage-statistics="storageStatistics" - :purchase-storage-url="purchaseStorageUrl" - /> - </div> - <div v-else class="gl-py-4 gl-px-2 gl-m-0"> - <div class="gl-display-flex gl-align-items-center"> - <div class="gl-w-half"> - <gl-sprintf :message="s__('UsageQuota|You used: %{usage} %{limit}')"> - <template #usage> - <span class="gl-font-weight-bold" data-testid="total-usage"> - {{ namespace.totalUsage }} - </span> - </template> - <template #limit> - <gl-sprintf - v-if="namespace.limit" - :message="s__('UsageQuota|out of %{formattedLimit} of your namespace storage')" - > - <template #formattedLimit> - <span class="gl-font-weight-bold">{{ formattedNamespaceLimit }}</span> - </template> - </gl-sprintf> - </template> - </gl-sprintf> - <gl-link - :href="helpPagePath" - target="_blank" - :aria-label="s__('UsageQuota|Usage quotas help link')" - > - <gl-icon name="question" :size="12" /> - </gl-link> - </div> - <div class="gl-w-half gl-text-right"> - <gl-button - v-if="isStorageIncreaseModalVisible" - v-gl-modal-directive="$options.modalId" - category="secondary" - variant="success" - data-testid="temporary-storage-increase-button" - >{{ s__('UsageQuota|Increase storage temporarily') }}</gl-button - > - <gl-link - v-if="purchaseStorageUrl" - :href="purchaseStorageUrl" - class="btn btn-success gl-ml-2" - target="_blank" - data-testid="purchase-storage-link" - >{{ s__('UsageQuota|Purchase more storage') }}</gl-link - > - </div> - </div> - <div v-if="namespace.rootStorageStatistics" class="gl-w-full"> - <usage-graph - :root-storage-statistics="namespace.rootStorageStatistics" - :limit="namespace.limit" - /> - </div> - </div> - <projects-table - :projects="namespaceProjects" - :is-loading="isQueryLoading" - :additional-purchased-storage-size="namespace.additionalPurchasedStorageSize || 0" - @search="handleSearch" - /> - <div class="gl-display-flex gl-justify-content-center gl-mt-5"> - <gl-keyset-pagination v-if="showPagination" v-bind="pageInfo" @prev="onPrev" @next="onNext" /> - </div> - <temporary-storage-increase-modal - v-if="isStorageIncreaseModalVisible" - :limit="formattedNamespaceLimit" - :modal-id="$options.modalId" - /> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/project.vue b/ee/app/assets/javascripts/other_storage_counter/components/project.vue deleted file mode 100644 index ce0c87e1301de071d7ae5ee3a3f218d49a5491b6..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/project.vue +++ /dev/null @@ -1,142 +0,0 @@ -<script> -import { GlLink, GlIcon } from '@gitlab/ui'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { numberToHumanSize, isOdd } from '~/lib/utils/number_utils'; -import { s__ } from '~/locale'; -import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; -import StorageRow from './storage_row.vue'; - -export default { - components: { - GlIcon, - GlLink, - ProjectAvatar, - StorageRow, - }, - props: { - project: { - required: true, - type: Object, - }, - }, - data() { - return { - isOpen: false, - }; - }, - computed: { - projectAvatar() { - const { name, id, avatarUrl, webUrl } = this.project; - return { - name, - id: Number(getIdFromGraphQLId(id)), - avatar_url: avatarUrl, - path: webUrl, - }; - }, - name() { - return this.project.nameWithNamespace; - }, - storageSize() { - return numberToHumanSize(this.project.statistics.storageSize); - }, - iconName() { - return this.isOpen ? 'angle-down' : 'angle-right'; - }, - statistics() { - const statisticsCopy = { ...this.project.statistics }; - delete statisticsCopy.storageSize; - // eslint-disable-next-line no-underscore-dangle - delete statisticsCopy.__typename; - delete statisticsCopy.commitCount; - - return statisticsCopy; - }, - }, - methods: { - toggleProject(e) { - const NO_EXPAND_CLS = 'js-project-link'; - const targetClasses = e.target.classList; - - if (targetClasses.contains(NO_EXPAND_CLS)) { - return; - } - this.isOpen = !this.isOpen; - }, - getFormattedName(name) { - return this.$options.i18nStatisticsMap[name]; - }, - isOdd(num) { - return isOdd(num); - }, - /** - * Some values can be `nil` - * for those, we send 0 instead - */ - getValue(val) { - return val || 0; - }, - }, - i18nStatisticsMap: { - repositorySize: s__('UsageQuota|Repository'), - lfsObjectsSize: s__('UsageQuota|LFS Storage'), - buildArtifactsSize: s__('UsageQuota|Artifacts'), - packagesSize: s__('UsageQuota|Packages'), - wikiSize: s__('UsageQuota|Wiki'), - snippetsSize: s__('UsageQuota|Snippets'), - uploadsSize: s__('UsageQuota|Uploads'), - }, -}; -</script> -<template> - <div> - <div - class="gl-responsive-table-row gl-border-solid gl-border-b-1 gl-pt-3 gl-pb-3 gl-border-b-gray-100 gl-hover-bg-blue-50 gl-hover-border-blue-200 gl-hover-cursor-pointer" - role="row" - data-testid="projectTableRow" - @click="toggleProject" - > - <div - class="table-section gl-white-space-normal! gl-sm-flex-wrap section-70 gl-text-truncate" - role="gridcell" - > - <div class="table-mobile-header gl-font-weight-bold" role="rowheader"> - {{ __('Project') }} - </div> - <div class="table-mobile-content gl-display-flex gl-align-items-center"> - <div class="gl-display-flex gl-mr-3 gl-align-items-center"> - <gl-icon :size="10" :name="iconName" use-deprecated-sizes class="gl-mr-2" /> - <gl-icon name="bookmark" /> - </div> - <div> - <project-avatar :project="projectAvatar" :size="32" /> - </div> - <gl-link - :href="project.webUrl" - class="js-project-link gl-font-weight-bold gl-text-gray-900!" - >{{ name }}</gl-link - > - </div> - </div> - <div - class="table-section gl-white-space-normal! gl-sm-flex-wrap section-30 gl-text-truncate" - role="gridcell" - > - <div class="table-mobile-header gl-font-weight-bold" role="rowheader"> - {{ __('Usage') }} - </div> - <div class="table-mobile-content gl-text-gray-900">{{ storageSize }}</div> - </div> - </div> - - <template v-if="isOpen"> - <storage-row - v-for="(value, statisticsName, index) in statistics" - :key="index" - :name="getFormattedName(statisticsName)" - :value="getValue(value)" - :class="{ 'gl-bg-gray-10': isOdd(index) }" - /> - </template> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/project_with_excess_storage.vue b/ee/app/assets/javascripts/other_storage_counter/components/project_with_excess_storage.vue deleted file mode 100644 index d5abe91532ef5d074ab0c86cc5321216686448e0..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/project_with_excess_storage.vue +++ /dev/null @@ -1,161 +0,0 @@ -<script> -import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import { s__, sprintf } from '~/locale'; -import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; -import { ALERT_THRESHOLD, ERROR_THRESHOLD, WARNING_THRESHOLD } from '../constants'; -import { formatUsageSize, usageRatioToThresholdLevel } from '../utils'; - -export default { - i18n: { - warningWithNoPurchasedStorageText: s__( - 'UsageQuota|This project is near the free %{actualRepositorySizeLimit} limit and at risk of being locked.', - ), - lockedWithNoPurchasedStorageText: s__( - 'UsageQuota|This project is locked because it is using %{actualRepositorySizeLimit} of free storage and there is no purchased storage available.', - ), - warningWithPurchasedStorageText: s__( - 'UsageQuota|This project is at risk of being locked because purchased storage is running low.', - ), - lockedWithPurchasedStorageText: s__( - 'UsageQuota|This project is locked because it used %{actualRepositorySizeLimit} of free storage and all the purchased storage.', - ), - }, - components: { - GlIcon, - GlLink, - ProjectAvatar, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - project: { - required: true, - type: Object, - }, - additionalPurchasedStorageSize: { - type: Number, - required: true, - }, - }, - computed: { - projectAvatar() { - const { name, id, avatarUrl, webUrl } = this.project; - return { - name, - id: Number(getIdFromGraphQLId(id)), - avatar_url: avatarUrl, - path: webUrl, - }; - }, - name() { - return this.project.nameWithNamespace; - }, - hasPurchasedStorage() { - return this.additionalPurchasedStorageSize > 0; - }, - storageSize() { - return formatUsageSize(this.project.totalCalculatedUsedStorage); - }, - excessStorageSize() { - return formatUsageSize(this.project.repositorySizeExcess); - }, - excessStorageRatio() { - return this.project.totalCalculatedUsedStorage / this.project.totalCalculatedStorageLimit; - }, - thresholdLevel() { - return usageRatioToThresholdLevel(this.excessStorageRatio); - }, - status() { - const i18nTextOpts = { - actualRepositorySizeLimit: formatUsageSize(this.project.actualRepositorySizeLimit), - }; - if (this.thresholdLevel === ERROR_THRESHOLD) { - const tooltipText = this.hasPurchasedStorage - ? this.$options.i18n.lockedWithPurchasedStorageText - : this.$options.i18n.lockedWithNoPurchasedStorageText; - - return { - bgColor: { 'gl-bg-red-50': true }, - iconClass: { 'gl-text-red-500': true }, - linkClass: 'gl-text-red-500!', - tooltipText: sprintf(tooltipText, i18nTextOpts), - }; - } else if ( - this.thresholdLevel === WARNING_THRESHOLD || - this.thresholdLevel === ALERT_THRESHOLD - ) { - const tooltipText = this.hasPurchasedStorage - ? this.$options.i18n.warningWithPurchasedStorageText - : this.$options.i18n.warningWithNoPurchasedStorageText; - - return { - bgColor: { 'gl-bg-orange-50': true }, - iconClass: 'gl-text-orange-500', - tooltipText: sprintf(tooltipText, i18nTextOpts), - }; - } - - return {}; - }, - }, -}; -</script> -<template> - <div - class="gl-responsive-table-row gl-border-solid gl-border-b-1 gl-pt-3 gl-pb-3 gl-border-b-gray-100" - :class="status.bgColor" - role="row" - data-testid="projectTableRow" - > - <div - class="table-section gl-white-space-normal! gl-sm-flex-wrap section-50 gl-text-truncate gl-pr-5" - role="gridcell" - > - <div class="table-mobile-header gl-font-weight-bold" role="rowheader"> - {{ __('Project') }} - </div> - <div class="table-mobile-content gl-display-flex gl-align-items-center"> - <div class="gl-display-flex gl-mr-3 gl-ml-5 gl-align-items-center"> - <gl-icon name="bookmark" /> - </div> - <div> - <project-avatar :project="projectAvatar" :size="32" /> - </div> - <div v-if="status.iconClass"> - <gl-icon - v-gl-tooltip="{ title: status.tooltipText }" - name="status_warning" - class="gl-mr-3" - :class="status.iconClass" - /> - </div> - <gl-link - :href="project.webUrl" - class="gl-font-weight-bold gl-text-gray-900!" - :class="status.linkClass" - >{{ name }}</gl-link - > - </div> - </div> - <div - class="table-section gl-white-space-normal! gl-sm-flex-wrap section-15 gl-text-truncate" - role="gridcell" - > - <div class="table-mobile-header gl-font-weight-bold" role="rowheader"> - {{ __('Usage') }} - </div> - <div class="table-mobile-content gl-text-gray-900">{{ storageSize }}</div> - </div> - <div - class="table-section gl-white-space-normal! gl-sm-flex-wrap section-15 gl-text-truncate" - role="gridcell" - > - <div class="table-mobile-header gl-font-weight-bold" role="rowheader"> - {{ __('Excess storage') }} - </div> - <div class="table-mobile-content gl-text-gray-900">{{ excessStorageSize }}</div> - </div> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/projects_skeleton_loader.vue b/ee/app/assets/javascripts/other_storage_counter/components/projects_skeleton_loader.vue deleted file mode 100644 index 88ac52b60ecf92798e15c1b16cd466008a6a3ee3..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/projects_skeleton_loader.vue +++ /dev/null @@ -1,42 +0,0 @@ -<script> -import { GlSkeletonLoader } from '@gitlab/ui'; -import { SKELETON_LOADER_ROWS } from '../constants'; - -export default { - name: 'ProjectsSkeletonLoader', - components: { GlSkeletonLoader }, - SKELETON_LOADER_ROWS, -}; -</script> -<template> - <div class="gl-border-b-solid gl-border-b-1 gl-border-gray-100"> - <div class="gl-flex-direction-column gl-md-display-none" data-testid="mobile-loader"> - <div - v-for="index in $options.SKELETON_LOADER_ROWS.mobile" - :key="index" - class="gl-responsive-table-row gl-border-solid gl-border-b-1 gl-pt-3 gl-pb-3 gl-border-b-gray-100" - > - <gl-skeleton-loader :width="500" :height="172"> - <rect width="480" height="20" x="10" y="15" rx="4" /> - <rect width="480" height="20" x="10" y="80" rx="4" /> - <rect width="480" height="20" x="10" y="145" rx="4" /> - </gl-skeleton-loader> - </div> - </div> - <div - class="gl-display-none gl-md-display-flex gl-flex-direction-column" - data-testid="desktop-loader" - > - <gl-skeleton-loader - v-for="index in $options.SKELETON_LOADER_ROWS.desktop" - :key="index" - :width="1000" - :height="39" - > - <rect rx="4" width="320" height="8" x="0" y="18" /> - <rect rx="4" width="60" height="8" x="500" y="18" /> - <rect rx="4" width="60" height="8" x="750" y="18" /> - </gl-skeleton-loader> - </div> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/projects_table.vue b/ee/app/assets/javascripts/other_storage_counter/components/projects_table.vue deleted file mode 100644 index 238aaedf9231a308ea0a191f10ab05ef3beecd1e..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/projects_table.vue +++ /dev/null @@ -1,91 +0,0 @@ -<script> -import { GlSearchBoxByType } from '@gitlab/ui'; -import { SEARCH_DEBOUNCE_MS } from '~/ref/constants'; -import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; -import Project from './project.vue'; -import ProjectWithExcessStorage from './project_with_excess_storage.vue'; -import ProjectsSkeletonLoader from './projects_skeleton_loader.vue'; - -export default { - components: { - Project, - ProjectsSkeletonLoader, - ProjectWithExcessStorage, - GlSearchBoxByType, - }, - mixins: [glFeatureFlagsMixin()], - props: { - projects: { - type: Array, - required: true, - }, - additionalPurchasedStorageSize: { - type: Number, - required: true, - }, - isLoading: { - type: Boolean, - required: false, - default: false, - }, - }, - computed: { - isAdditionalStorageFlagEnabled() { - return this.glFeatures.additionalRepoStorageByNamespace; - }, - projectRowComponent() { - if (this.isAdditionalStorageFlagEnabled) { - return ProjectWithExcessStorage; - } - return Project; - }, - }, - searchDebounceValue: SEARCH_DEBOUNCE_MS, -}; -</script> - -<template> - <div> - <div - class="gl-responsive-table-row table-row-header gl-border-t-solid gl-border-t-1 gl-border-gray-100 gl-mt-5 gl-line-height-normal gl-text-black-normal gl-font-base" - role="row" - > - <template v-if="isAdditionalStorageFlagEnabled"> - <div class="table-section section-50 gl-font-weight-bold gl-pl-5" role="columnheader"> - {{ __('Project') }} - </div> - <div class="table-section section-15 gl-font-weight-bold" role="columnheader"> - {{ __('Usage') }} - </div> - <div class="table-section section-15 gl-font-weight-bold" role="columnheader"> - {{ __('Excess storage') }} - </div> - <div class="table-section section-20 gl-font-weight-bold gl-pl-6" role="columnheader"> - <gl-search-box-by-type - :placeholder="__('Search by name')" - :debounce="$options.searchDebounceValue" - @input="(input) => this.$emit('search', input)" - /> - </div> - </template> - <template v-else> - <div class="table-section section-70 gl-font-weight-bold" role="columnheader"> - {{ __('Project') }} - </div> - <div class="table-section section-30 gl-font-weight-bold" role="columnheader"> - {{ __('Usage') }} - </div> - </template> - </div> - <projects-skeleton-loader v-if="isAdditionalStorageFlagEnabled && isLoading" /> - <template v-else> - <component - :is="projectRowComponent" - v-for="project in projects" - :key="project.id" - :project="project" - :additional-purchased-storage-size="additionalPurchasedStorageSize" - /> - </template> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/storage_inline_alert.vue b/ee/app/assets/javascripts/other_storage_counter/components/storage_inline_alert.vue deleted file mode 100644 index 0a071843478cc6bae518444ab2fba554de52e2ee..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/storage_inline_alert.vue +++ /dev/null @@ -1,145 +0,0 @@ -<script> -import { GlAlert } from '@gitlab/ui'; -import { n__, s__, sprintf } from '~/locale'; -import { ALERT_THRESHOLD, ERROR_THRESHOLD, WARNING_THRESHOLD } from '../constants'; -import { formatUsageSize, usageRatioToThresholdLevel } from '../utils'; - -export default { - i18n: { - lockedWithNoPurchasedStorageTitle: s__('UsageQuota|This namespace contains locked projects'), - lockedWithNoPurchasedStorageText: s__( - 'UsageQuota|You have reached the free storage limit of %{actualRepositorySizeLimit} on %{projectsLockedText}. To unlock them, please purchase additional storage.', - ), - storageUsageText: s__('UsageQuota|%{percentageLeft} of purchased storage is available'), - lockedWithPurchaseText: s__( - 'UsageQuota|You have consumed all of your additional storage, please purchase more to unlock your projects over the free %{actualRepositorySizeLimit} limit.', - ), - warningWithPurchaseText: s__( - 'UsageQuota|Your purchased storage is running low. To avoid locked projects, please purchase more storage.', - ), - infoWithPurchaseText: s__( - 'UsageQuota|When you purchase additional storage, we automatically unlock projects that were locked when you reached the %{actualRepositorySizeLimit} limit.', - ), - }, - 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, - }, - actualRepositorySizeLimit: { - 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 this.$options.i18n.lockedWithNoPurchasedStorageTitle; - } - return sprintf(this.$options.i18n.storageUsageText, { - percentageLeft: `${this.excessStoragePercentageLeft}%`, - }); - }, - 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) { - return formatUsageSize(size); - }, - hasPurchasedStorageText() { - if (this.thresholdLevel === ERROR_THRESHOLD) { - return sprintf(this.$options.i18n.lockedWithPurchaseText, { - actualRepositorySizeLimit: this.formatSize(this.actualRepositorySizeLimit), - }); - } else if ( - this.thresholdLevel === WARNING_THRESHOLD || - this.thresholdLevel === ALERT_THRESHOLD - ) { - return this.$options.i18n.warningWithPurchaseText; - } - return sprintf(this.$options.i18n.infoWithPurchaseText, { - actualRepositorySizeLimit: this.formatSize(this.actualRepositorySizeLimit), - }); - }, - hasNotPurchasedStorageText() { - if (this.thresholdLevel === ERROR_THRESHOLD) { - return sprintf(this.$options.i18n.lockedWithNoPurchasedStorageText, { - actualRepositorySizeLimit: this.formatSize(this.actualRepositorySizeLimit), - projectsLockedText: this.projectsLockedText, - }); - } - return ''; - }, - }, -}; -</script> -<template> - <gl-alert - v-if="shouldShowAlert" - class="gl-mt-5" - :variant="thresholdLevelToAlertVariant" - :dismissible="false" - :title="alertTitle" - > - {{ alertText }} - </gl-alert> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/storage_row.vue b/ee/app/assets/javascripts/other_storage_counter/components/storage_row.vue deleted file mode 100644 index a4509bc3aa893cbd993c3ebe0c3284868b87de7b..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/storage_row.vue +++ /dev/null @@ -1,34 +0,0 @@ -<script> -import { numberToHumanSize } from '~/lib/utils/number_utils'; - -export default { - props: { - name: { - type: String, - required: true, - }, - value: { - type: Number, - required: true, - }, - }, - computed: { - formattedValue() { - return numberToHumanSize(this.value); - }, - }, -}; -</script> -<template> - <div class="gl-responsive-table-row lh-100" role="row"> - <div class="table-section section-wrap section-70 text-truncate pl-2 ml-3" role="gridcell"> - <div class="table-mobile-header" role="rowheader"></div> - <div class="table-mobile-content ml-1">{{ name }}</div> - </div> - - <div class="table-section section-wrap section-30 text-truncate" role="gridcell"> - <div class="table-mobile-header" role="rowheader"></div> - <div class="table-mobile-content">{{ formattedValue }}</div> - </div> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/temporary_storage_increase_modal.vue b/ee/app/assets/javascripts/other_storage_counter/components/temporary_storage_increase_modal.vue deleted file mode 100644 index 8c8513aac3b8abdb2c5c4231eacf91b4f02d3a81..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/temporary_storage_increase_modal.vue +++ /dev/null @@ -1,44 +0,0 @@ -<script> -import { GlModal, GlSprintf } from '@gitlab/ui'; -import { s__, __ } from '~/locale'; - -export default { - components: { - GlModal, - GlSprintf, - }, - props: { - limit: { - type: String, - required: true, - }, - modalId: { - type: String, - required: true, - }, - }, - modalBody: s__( - "TemporaryStorage|GitLab allows you a %{strongStart}free, one-time storage increase%{strongEnd}. For 30 days your storage will be unlimited. This gives you time to reduce your storage usage. After 30 days, your original storage limit of %{limit} applies. If you are at maximum storage capacity, your account will be read-only. To continue using GitLab you'll have to purchase additional storage or decrease storage usage.", - ), - modalTitle: s__('TemporaryStorage|Temporarily increase storage now?'), - okTitle: s__('TemporaryStorage|Increase storage temporarily'), - cancelTitle: __('Cancel'), -}; -</script> -<template> - <gl-modal - size="sm" - ok-variant="success" - :title="$options.modalTitle" - :ok-title="$options.okTitle" - :cancel-title="$options.cancelTitle" - :modal-id="modalId" - > - <gl-sprintf :message="$options.modalBody"> - <template #strong="{ content }"> - <strong>{{ content }}</strong> - </template> - <template #limit>{{ limit }}</template> - </gl-sprintf> - </gl-modal> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/usage_graph.vue b/ee/app/assets/javascripts/other_storage_counter/components/usage_graph.vue deleted file mode 100644 index c33d065ff4b9a133b721fc4ac9e1f17a1699fe55..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/usage_graph.vue +++ /dev/null @@ -1,148 +0,0 @@ -<script> -import { GlIcon, GlTooltipDirective } from '@gitlab/ui'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; -import { s__ } from '~/locale'; - -export default { - components: { - GlIcon, - }, - directives: { - GlTooltip: GlTooltipDirective, - }, - props: { - rootStorageStatistics: { - required: true, - type: Object, - }, - limit: { - required: true, - type: Number, - }, - }, - computed: { - storageTypes() { - const { - buildArtifactsSize, - pipelineArtifactsSize, - lfsObjectsSize, - packagesSize, - repositorySize, - storageSize, - wikiSize, - snippetsSize, - uploadsSize, - } = this.rootStorageStatistics; - const artifactsSize = buildArtifactsSize + pipelineArtifactsSize; - - if (storageSize === 0) { - return null; - } - - return [ - { - name: s__('UsageQuota|Repositories'), - style: this.usageStyle(this.barRatio(repositorySize)), - class: 'gl-bg-data-viz-blue-500', - size: repositorySize, - }, - { - name: s__('UsageQuota|LFS Objects'), - style: this.usageStyle(this.barRatio(lfsObjectsSize)), - class: 'gl-bg-data-viz-orange-600', - size: lfsObjectsSize, - }, - { - name: s__('UsageQuota|Packages'), - style: this.usageStyle(this.barRatio(packagesSize)), - class: 'gl-bg-data-viz-aqua-500', - size: packagesSize, - }, - { - name: s__('UsageQuota|Artifacts'), - style: this.usageStyle(this.barRatio(artifactsSize)), - class: 'gl-bg-data-viz-green-600', - size: artifactsSize, - tooltip: s__('UsageQuota|Artifacts is a sum of build and pipeline artifacts.'), - }, - { - name: s__('UsageQuota|Wikis'), - style: this.usageStyle(this.barRatio(wikiSize)), - class: 'gl-bg-data-viz-magenta-500', - size: wikiSize, - }, - { - name: s__('UsageQuota|Snippets'), - style: this.usageStyle(this.barRatio(snippetsSize)), - class: 'gl-bg-data-viz-orange-800', - size: snippetsSize, - }, - { - name: s__('UsageQuota|Uploads'), - style: this.usageStyle(this.barRatio(uploadsSize)), - class: 'gl-bg-data-viz-aqua-700', - size: uploadsSize, - }, - ] - .filter((data) => data.size !== 0) - .sort((a, b) => b.size - a.size); - }, - }, - methods: { - formatSize(size) { - return numberToHumanSize(size); - }, - usageStyle(ratio) { - return { flex: ratio }; - }, - barRatio(size) { - let max = this.rootStorageStatistics.storageSize; - - if (this.limit !== 0 && max <= this.limit) { - max = this.limit; - } - - return size / max; - }, - }, -}; -</script> -<template> - <div v-if="storageTypes" class="gl-display-flex gl-flex-direction-column w-100"> - <div class="gl-h-6 gl-my-5 gl-bg-gray-50 gl-rounded-base gl-display-flex"> - <div - v-for="storageType in storageTypes" - :key="storageType.name" - class="storage-type-usage gl-h-full gl-display-inline-block" - :class="storageType.class" - :style="storageType.style" - data-testid="storage-type-usage" - ></div> - </div> - <div class="row py-0"> - <div - v-for="storageType in storageTypes" - :key="storageType.name" - class="col-md-auto gl-display-flex gl-align-items-center" - data-testid="storage-type-legend" - > - <div class="gl-h-2 gl-w-5 gl-mr-2 gl-display-inline-block" :class="storageType.class"></div> - <span class="gl-mr-2 gl-font-weight-bold gl-font-sm"> - {{ storageType.name }} - </span> - <span class="gl-text-gray-500 gl-font-sm"> - {{ formatSize(storageType.size) }} - </span> - <span - v-if="storageType.tooltip" - v-gl-tooltip - :title="storageType.tooltip" - :aria-label="storageType.tooltip" - class="gl-ml-2" - > - <gl-icon name="question" :size="12" /> - </span> - </div> - </div> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics.vue b/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics.vue deleted file mode 100644 index bf6964c2dd64b1c00f3872580fbe231897695730..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics.vue +++ /dev/null @@ -1,134 +0,0 @@ -<script> -import { GlButton } from '@gitlab/ui'; -import { helpPagePath } from '~/helpers/help_page_helper'; -import { s__ } from '~/locale'; -import { formatUsageSize } from '../utils'; -import UsageStatisticsCard from './usage_statistics_card.vue'; - -export default { - components: { - GlButton, - UsageStatisticsCard, - }, - props: { - rootStorageStatistics: { - required: true, - type: Object, - }, - purchaseStorageUrl: { - required: false, - type: String, - default: '', - }, - }, - computed: { - formattedActualRepoSizeLimit() { - 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 { - totalRepositorySizeExcess, - additionalPurchasedStorageSize, - } = this.rootStorageStatistics; - return this.purchaseStorageUrl - ? { - usage: this.formatSizeAndSplit( - Math.max(0, additionalPurchasedStorageSize - totalRepositorySizeExcess), - ), - usageTotal: this.formatSizeAndSplit(additionalPurchasedStorageSize), - description: s__('UsageQuota|Purchased storage available'), - link: { - text: s__('UsageQuota|Purchase more storage'), - url: this.purchaseStorageUrl, - }, - } - : null; - }, - }, - methods: { - /** - * 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 - */ - formatSizeAndSplit(size) { - const formattedSize = formatUsageSize(size); - return { - value: formattedSize.slice(0, -3), - unit: formattedSize.slice(-3), - }; - }, - }, -}; -</script> -<template> - <div class="gl-display-flex gl-sm-flex-direction-column"> - <usage-statistics-card - data-testid="total-usage" - :usage="totalUsage.usage" - :link="totalUsage.link" - :description="totalUsage.description" - 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 }"> - <gl-button - target="_blank" - :href="link.url" - class="mb-0" - variant="success" - category="primary" - block - > - {{ link.text }} - </gl-button> - </template> - </usage-statistics-card> - </div> -</template> diff --git a/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics_card.vue b/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics_card.vue deleted file mode 100644 index e8243a5c92d06d801f491e637f1228cf00fbaf88..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/components/usage_statistics_card.vue +++ /dev/null @@ -1,75 +0,0 @@ -<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 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> diff --git a/ee/app/assets/javascripts/other_storage_counter/constants.js b/ee/app/assets/javascripts/other_storage_counter/constants.js deleted file mode 100644 index f5e2dbda55d8cb136a12c752ea97a9ad770388a8..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/constants.js +++ /dev/null @@ -1,20 +0,0 @@ -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'; - -export 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 const PROJECTS_PER_PAGE = 20; - -export const SKELETON_LOADER_ROWS = { - desktop: PROJECTS_PER_PAGE, - mobile: 5, -}; diff --git a/ee/app/assets/javascripts/other_storage_counter/index.js b/ee/app/assets/javascripts/other_storage_counter/index.js deleted file mode 100644 index 2c258faea19bf6bb1fc6bb78dfda1207339286a9..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/index.js +++ /dev/null @@ -1,35 +0,0 @@ -import Vue from 'vue'; -import VueApollo from 'vue-apollo'; -import createDefaultClient from '~/lib/graphql'; -import App from './components/app.vue'; - -Vue.use(VueApollo); - -export default () => { - const el = document.getElementById('js-other-storage-counter-app'); - const { - namespacePath, - helpPagePath, - purchaseStorageUrl, - isTemporaryStorageIncreaseVisible, - } = el.dataset; - - const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), - }); - - return new Vue({ - el, - apolloProvider, - render(h) { - return h(App, { - props: { - namespacePath, - helpPagePath, - purchaseStorageUrl, - isTemporaryStorageIncreaseVisible, - }, - }); - }, - }); -}; diff --git a/ee/app/assets/javascripts/other_storage_counter/queries/storage.query.graphql b/ee/app/assets/javascripts/other_storage_counter/queries/storage.query.graphql deleted file mode 100644 index 91d73c8283e1c2d6d3805466952fe2c47440248e..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/queries/storage.query.graphql +++ /dev/null @@ -1,66 +0,0 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" - -query getStorageCounter( - $fullPath: ID! - $withExcessStorageData: Boolean = false - $searchTerm: String = "" - $first: Int! - $after: String - $before: String -) { - namespace(fullPath: $fullPath) { - id - name - storageSizeLimit - actualRepositorySizeLimit @include(if: $withExcessStorageData) - additionalPurchasedStorageSize @include(if: $withExcessStorageData) - totalRepositorySizeExcess @include(if: $withExcessStorageData) - totalRepositorySize @include(if: $withExcessStorageData) - containsLockedProjects @include(if: $withExcessStorageData) - repositorySizeExcessProjectCount @include(if: $withExcessStorageData) - rootStorageStatistics { - storageSize - repositorySize - lfsObjectsSize - buildArtifactsSize - pipelineArtifactsSize - packagesSize - wikiSize - snippetsSize - uploadsSize - } - projects( - includeSubgroups: true - search: $searchTerm - first: $first - after: $after - before: $before - sort: STORAGE - ) { - nodes { - id - fullPath - nameWithNamespace - avatarUrl - webUrl - name - repositorySizeExcess @include(if: $withExcessStorageData) - actualRepositorySizeLimit @include(if: $withExcessStorageData) - statistics { - commitCount - storageSize - repositorySize - lfsObjectsSize - buildArtifactsSize - packagesSize - wikiSize - snippetsSize - uploadsSize - } - } - pageInfo { - ...PageInfo - } - } - } -} diff --git a/ee/app/assets/javascripts/other_storage_counter/utils.js b/ee/app/assets/javascripts/other_storage_counter/utils.js deleted file mode 100644 index 6f962e35775e7269fda91b5ef07e5f75cfb877c3..0000000000000000000000000000000000000000 --- a/ee/app/assets/javascripts/other_storage_counter/utils.js +++ /dev/null @@ -1,144 +0,0 @@ -import { numberToHumanSize, bytesToKiB } from '~/lib/utils/number_utils'; -import { getFormatter, SUPPORTED_FORMATS } from '~/lib/utils/unit_format'; - -import { STORAGE_USAGE_THRESHOLDS } from './constants'; - -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; -} - -/** - * 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 {Number} size size in bytes - * @returns {String} - */ -export const formatUsageSize = (size) => { - const formatDecimalBytes = getFormatter(SUPPORTED_FORMATS.kibibytes); - return formatDecimalBytes(bytesToKiB(size), 1); -}; - -/** - * 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 `getStorageCounter` - * call. - * - * `rootStorageStatistics` will be sent as null until an - * event happens to trigger the storage count. - * For that reason we have to verify if `storageSize` is sent or - * if we should render N/A - * - * @param {Object} data graphql result - * @returns {Object} - */ -export const parseGetStorageResults = (data) => { - const { - namespace: { - projects, - storageSizeLimit, - totalRepositorySize, - containsLockedProjects, - totalRepositorySizeExcess, - rootStorageStatistics = {}, - actualRepositorySizeLimit, - additionalPurchasedStorageSize, - repositorySizeExcessProjectCount, - }, - } = data || {}; - - const totalUsage = rootStorageStatistics?.storageSize - ? numberToHumanSize(rootStorageStatistics.storageSize) - : 'N/A'; - - return { - projects: { - data: parseProjects({ - projects, - additionalPurchasedStorageSize, - totalRepositorySizeExcess, - }), - pageInfo: projects.pageInfo, - }, - additionalPurchasedStorageSize, - actualRepositorySizeLimit, - containsLockedProjects, - repositorySizeExcessProjectCount, - totalRepositorySize, - totalRepositorySizeExcess, - totalUsage, - rootStorageStatistics, - limit: storageSizeLimit, - }; -}; diff --git a/ee/app/assets/javascripts/pages/groups/usage_quotas/index.js b/ee/app/assets/javascripts/pages/groups/usage_quotas/index.js index 2200b2ceb256c414cd90366ff9f710bd00339045..61571e6050ea5e3763bc1111347b862db5447255 100644 --- a/ee/app/assets/javascripts/pages/groups/usage_quotas/index.js +++ b/ee/app/assets/javascripts/pages/groups/usage_quotas/index.js @@ -1,4 +1,3 @@ -import otherStorageCounter from 'ee/other_storage_counter'; import SeatUsageApp from 'ee/seat_usage'; import storageCounter from 'ee/storage_counter'; import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; @@ -24,10 +23,6 @@ const initVueApps = () => { if (document.querySelector('#js-storage-counter-app')) { storageCounter(); } - - if (document.querySelector('#js-other-storage-counter-app')) { - otherStorageCounter(); - } }; initVueApps(); diff --git a/ee/app/assets/javascripts/pages/profiles/usage_quotas/index.js b/ee/app/assets/javascripts/pages/profiles/usage_quotas/index.js index 34d039ca89e7823e395e16f2ce06d159cdca41e7..a17d96412b4e2055b4f862f849109bd38cc699f4 100644 --- a/ee/app/assets/javascripts/pages/profiles/usage_quotas/index.js +++ b/ee/app/assets/javascripts/pages/profiles/usage_quotas/index.js @@ -1,5 +1,4 @@ import ciMinutesUsage from 'ee/ci_minutes_usage'; -import otherStorageCounter from 'ee/other_storage_counter'; import storageCounter from 'ee/storage_counter'; import LinkedTabs from '~/lib/utils/bootstrap_linked_tabs'; @@ -14,15 +13,4 @@ if (document.querySelector('#js-storage-counter-app')) { }); } -if (document.querySelector('#js-other-storage-counter-app')) { - otherStorageCounter(); - - // eslint-disable-next-line no-new - new LinkedTabs({ - defaultAction: '#pipelines-quota-tab', - parentEl: '.js-other-storage-tabs', - hashedTabs: true, - }); -} - ciMinutesUsage(); diff --git a/ee/app/views/groups/usage_quotas/index.html.haml b/ee/app/views/groups/usage_quotas/index.html.haml index 3d99f4c8e091fda869bf23a27b7e49d42c1e76f8..b98a9c6092194feb772b4a4f3d47495c502318d6 100644 --- a/ee/app/views/groups/usage_quotas/index.html.haml +++ b/ee/app/views/groups/usage_quotas/index.html.haml @@ -1,6 +1,5 @@ - page_title s_("UsageQuota|Usage") - url_to_purchase_storage = purchase_storage_url if purchase_storage_link_enabled?(@group) -- other_storage_enabled = Feature.enabled?(:other_storage_tab, @group) - if show_product_purchase_success_alert? = render 'product_purchase_success_alert', product_name: params[:purchased_product] @@ -23,10 +22,6 @@ %li.nav-item %a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': false } = s_('UsageQuota|Storage') - - if other_storage_enabled - %li.nav-item - %a.nav-link#storage-quota{ data: { toggle: "tab", action: '#other-storage-quota-tab' }, href: '#other-storage-quota-tab', 'aria-controls': '#other-storage-quota-tab', 'aria-selected': false } - = s_('UsageQuota|Other Storage') .tab-content .tab-pane#seats-quota-tab #js-seat-usage-app{ data: { namespace_id: @group.id, namespace_name: @group.name, seat_usage_export_path: group_seat_usage_path(@group, format: :csv) } } @@ -35,6 +30,3 @@ locals: { namespace: @group, projects: @projects } .tab-pane#storage-quota-tab #js-storage-counter-app{ data: { namespace_path: @group.full_path, help_page_path: help_page_path('user/usage_quotas.md'), purchase_storage_url: url_to_purchase_storage, is_temporary_storage_increase_visible: temporary_storage_increase_visible?(@group).to_s } } - - if other_storage_enabled - .tab-pane#other-storage-quota-tab - #js-other-storage-counter-app{ data: { namespace_path: @group.full_path, help_page_path: help_page_path('user/usage_quotas.md'), purchase_storage_url: url_to_purchase_storage, is_temporary_storage_increase_visible: temporary_storage_increase_visible?(@group).to_s } } diff --git a/ee/app/views/profiles/usage_quotas/index.html.haml b/ee/app/views/profiles/usage_quotas/index.html.haml index c83efbc620cb0308cb00045418e949503df1a5ae..9924cb33d80dfb05056008cbad08439a9f49b35e 100644 --- a/ee/app/views/profiles/usage_quotas/index.html.haml +++ b/ee/app/views/profiles/usage_quotas/index.html.haml @@ -1,7 +1,6 @@ - page_title s_("UsageQuota|Usage") - @content_class = "limit-container-width" unless fluid_layout - url_to_purchase_storage = purchase_storage_url if purchase_storage_link_enabled?(@namespace) -- other_storage_enabled = Feature.enabled?(:other_storage_tab, @namespace) %h3.page-title = s_('UsageQuota|Usage Quotas') @@ -18,16 +17,9 @@ %li.nav-item %a.nav-link#storage-quota{ data: { toggle: "tab", action: '#storage-quota-tab' }, href: '#storage-quota-tab', 'aria-controls': '#storage-quota-tab', 'aria-selected': false } = s_('UsageQuota|Storage') - - if other_storage_enabled - %li.nav-item - %a.nav-link#storage-quota{ data: { toggle: "tab", action: '#other-storage-quota-tab' }, href: '#other-storage-quota-tab', 'aria-controls': '#other-storage-quota-tab', 'aria-selected': false } - = s_('UsageQuota|Other Storage') .tab-content .tab-pane#pipelines-quota-tab = render "namespaces/pipelines_quota/list", locals: { namespace: @namespace, projects: @projects } .tab-pane#storage-quota-tab #js-storage-counter-app{ data: { namespace_path: @namespace.full_path, help_page_path: help_page_path('user/usage_quotas.md'), purchase_storage_url: url_to_purchase_storage, is_temporary_storage_increase_visible: temporary_storage_increase_visible?(@namespace).to_s } } - - if other_storage_enabled - .tab-pane#other-storage-quota-tab - #js-other-storage-counter-app{ data: { namespace_path: @namespace.full_path, help_page_path: help_page_path('user/usage_quotas.md'), purchase_storage_url: url_to_purchase_storage, is_temporary_storage_increase_visible: temporary_storage_increase_visible?(@namespace).to_s } } diff --git a/ee/spec/frontend/other_storage_counter/components/app_spec.js b/ee/spec/frontend/other_storage_counter/components/app_spec.js deleted file mode 100644 index f2ff03bdcd50c97a69d4655a94023c1144cb54cc..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/app_spec.js +++ /dev/null @@ -1,288 +0,0 @@ -import { mount } from '@vue/test-utils'; -import StorageApp from 'ee/other_storage_counter/components/app.vue'; -import Project from 'ee/other_storage_counter/components/project.vue'; -import ProjectsTable from 'ee/other_storage_counter/components/projects_table.vue'; -import StorageInlineAlert from 'ee/other_storage_counter/components/storage_inline_alert.vue'; -import TemporaryStorageIncreaseModal from 'ee/other_storage_counter/components/temporary_storage_increase_modal.vue'; -import UsageGraph from 'ee/other_storage_counter/components/usage_graph.vue'; -import UsageStatistics from 'ee/other_storage_counter/components/usage_statistics.vue'; -import { formatUsageSize } from 'ee/other_storage_counter/utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import { namespaceData, withRootStorageStatistics } from '../mock_data'; - -const TEST_LIMIT = 1000; - -describe('Storage counter app', () => { - let wrapper; - - const findTotalUsage = () => wrapper.find("[data-testid='total-usage']"); - const findPurchaseStorageLink = () => wrapper.find("[data-testid='purchase-storage-link']"); - const findTemporaryStorageIncreaseButton = () => - wrapper.find("[data-testid='temporary-storage-increase-button']"); - const findUsageGraph = () => wrapper.find(UsageGraph); - const findUsageStatistics = () => wrapper.find(UsageStatistics); - const findStorageInlineAlert = () => wrapper.find(StorageInlineAlert); - const findProjectsTable = () => wrapper.find(ProjectsTable); - const findPrevButton = () => wrapper.find('[data-testid="prevButton"]'); - const findNextButton = () => wrapper.find('[data-testid="nextButton"]'); - - const createComponent = ({ - props = {}, - loading = false, - additionalRepoStorageByNamespace = false, - namespace = {}, - } = {}) => { - const $apollo = { - queries: { - namespace: { - loading, - }, - }, - }; - - wrapper = mount(StorageApp, { - propsData: { namespacePath: 'h5bp', helpPagePath: 'help', ...props }, - mocks: { $apollo }, - directives: { - GlModalDirective: createMockDirective(), - }, - provide: { - glFeatures: { - additionalRepoStorageByNamespace, - }, - }, - data() { - return { - namespace, - }; - }, - }); - }; - - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders the 2 projects', async () => { - wrapper.setData({ - namespace: namespaceData, - }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.findAll(Project)).toHaveLength(3); - }); - - describe('limit', () => { - it('when limit is set it renders limit information', async () => { - wrapper.setData({ - namespace: namespaceData, - }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.text()).toContain(formatUsageSize(namespaceData.limit)); - }); - - it('when limit is 0 it does not render limit information', async () => { - wrapper.setData({ - namespace: { ...namespaceData, limit: 0 }, - }); - - await wrapper.vm.$nextTick(); - - expect(wrapper.text()).not.toContain(formatUsageSize(0)); - }); - }); - - describe('with rootStorageStatistics information', () => { - it('renders total usage', async () => { - wrapper.setData({ - namespace: withRootStorageStatistics, - }); - - await wrapper.vm.$nextTick(); - - expect(findTotalUsage().text()).toContain(withRootStorageStatistics.totalUsage); - }); - }); - - describe('with additional_repo_storage_by_namespace feature', () => { - it('usage_graph component hidden is when feature is false', async () => { - wrapper.setData({ - namespace: withRootStorageStatistics, - }); - - await wrapper.vm.$nextTick(); - - expect(findUsageGraph().exists()).toBe(true); - expect(findUsageStatistics().exists()).toBe(false); - expect(findStorageInlineAlert().exists()).toBe(false); - }); - - it('usage_statistics component is rendered when feature is true', async () => { - createComponent({ - additionalRepoStorageByNamespace: true, - namespace: withRootStorageStatistics, - }); - - await wrapper.vm.$nextTick(); - - expect(findUsageStatistics().exists()).toBe(true); - expect(findUsageGraph().exists()).toBe(false); - expect(findStorageInlineAlert().exists()).toBe(true); - }); - }); - - describe('without rootStorageStatistics information', () => { - it('renders N/A', async () => { - wrapper.setData({ - namespace: namespaceData, - }); - - await wrapper.vm.$nextTick(); - - expect(findTotalUsage().text()).toContain('N/A'); - }); - }); - - describe('purchase storage link', () => { - describe('when purchaseStorageUrl is not set', () => { - it('does not render an additional link', () => { - expect(findPurchaseStorageLink().exists()).toBe(false); - }); - }); - - describe('when purchaseStorageUrl is set', () => { - beforeEach(() => { - createComponent({ props: { purchaseStorageUrl: 'customers.gitlab.com' } }); - }); - - it('does render link', () => { - const link = findPurchaseStorageLink(); - - expect(link).toExist(); - expect(link.attributes('href')).toBe('customers.gitlab.com'); - }); - }); - }); - - describe('temporary storage increase', () => { - describe.each` - props | isVisible - ${{}} | ${false} - ${{ isTemporaryStorageIncreaseVisible: 'false' }} | ${false} - ${{ isTemporaryStorageIncreaseVisible: 'true' }} | ${true} - `('with $props', ({ props, isVisible }) => { - beforeEach(() => { - createComponent({ props }); - }); - - it(`renders button = ${isVisible}`, () => { - expect(findTemporaryStorageIncreaseButton().exists()).toBe(isVisible); - }); - }); - - describe('when temporary storage increase is visible', () => { - beforeEach(() => { - createComponent({ props: { isTemporaryStorageIncreaseVisible: 'true' } }); - wrapper.setData({ - namespace: { - ...namespaceData, - limit: TEST_LIMIT, - }, - }); - }); - - it('binds button to modal', () => { - const { value } = getBinding( - findTemporaryStorageIncreaseButton().element, - 'gl-modal-directive', - ); - - // Check for truthiness so we're assured we're not comparing two undefineds - expect(value).toBeTruthy(); - expect(value).toEqual(StorageApp.modalId); - }); - - it('renders modal', () => { - expect(wrapper.find(TemporaryStorageIncreaseModal).props()).toEqual({ - limit: formatUsageSize(TEST_LIMIT), - modalId: StorageApp.modalId, - }); - }); - }); - }); - - describe('filtering projects', () => { - beforeEach(() => { - createComponent({ - additionalRepoStorageByNamespace: true, - namespace: withRootStorageStatistics, - }); - }); - - const sampleSearchTerm = 'GitLab'; - const sampleShortSearchTerm = '12'; - - it('triggers search if user enters search input', () => { - expect(wrapper.vm.searchTerm).toBe(''); - - findProjectsTable().vm.$emit('search', sampleSearchTerm); - - expect(wrapper.vm.searchTerm).toBe(sampleSearchTerm); - }); - - it('triggers search if user clears the entered search input', () => { - const projectsTable = findProjectsTable(); - - expect(wrapper.vm.searchTerm).toBe(''); - - projectsTable.vm.$emit('search', sampleSearchTerm); - - expect(wrapper.vm.searchTerm).toBe(sampleSearchTerm); - - projectsTable.vm.$emit('search', ''); - - expect(wrapper.vm.searchTerm).toBe(''); - }); - - it('does not trigger search if user enters short search input', () => { - expect(wrapper.vm.searchTerm).toBe(''); - - findProjectsTable().vm.$emit('search', sampleShortSearchTerm); - - expect(wrapper.vm.searchTerm).toBe(''); - }); - }); - - describe('renders projects table pagination component', () => { - const namespaceWithPageInfo = { - namespace: { - ...withRootStorageStatistics, - projects: { - ...withRootStorageStatistics.projects, - pageInfo: { - hasPreviousPage: false, - hasNextPage: true, - }, - }, - }, - }; - beforeEach(() => { - createComponent(namespaceWithPageInfo); - }); - - it('with disabled "Prev" button', () => { - expect(findPrevButton().attributes().disabled).toBe('disabled'); - }); - - it('with enabled "Next" button', () => { - expect(findNextButton().attributes().disabled).toBeUndefined(); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/project_spec.js b/ee/spec/frontend/other_storage_counter/components/project_spec.js deleted file mode 100644 index f8a1e4869b805d0458e5708e088502a8f45ce8b7..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/project_spec.js +++ /dev/null @@ -1,55 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Project from 'ee/other_storage_counter/components/project.vue'; -import StorageRow from 'ee/other_storage_counter/components/storage_row.vue'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; -import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; -import { projects } from '../mock_data'; - -let wrapper; -const createComponent = () => { - wrapper = shallowMount(Project, { - propsData: { - project: projects[1], - }, - }); -}; - -const findTableRow = () => wrapper.find('[data-testid="projectTableRow"]'); -const findStorageRow = () => wrapper.find(StorageRow); - -describe('Storage Counter project component', () => { - beforeEach(() => { - createComponent(); - }); - - it('renders project avatar', () => { - expect(wrapper.find(ProjectAvatar).exists()).toBe(true); - }); - - it('renders project name', () => { - expect(wrapper.text()).toContain(projects[1].nameWithNamespace); - }); - - it('renders formatted storage size', () => { - expect(wrapper.text()).toContain(numberToHumanSize(projects[1].statistics.storageSize)); - }); - - describe('toggle row', () => { - describe('on click', () => { - it('toggles isOpen', () => { - expect(findStorageRow().exists()).toBe(false); - - findTableRow().trigger('click'); - - wrapper.vm.$nextTick(() => { - expect(findStorageRow().exists()).toBe(true); - findTableRow().trigger('click'); - - wrapper.vm.$nextTick(() => { - expect(findStorageRow().exists()).toBe(false); - }); - }); - }); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/project_with_excess_storage_spec.js b/ee/spec/frontend/other_storage_counter/components/project_with_excess_storage_spec.js deleted file mode 100644 index 07ac4ef437ea5f9c2c5d26fd3c89b0a7ab0f6635..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/project_with_excess_storage_spec.js +++ /dev/null @@ -1,148 +0,0 @@ -import { GlIcon, GlLink } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import ProjectWithExcessStorage from 'ee/other_storage_counter/components/project_with_excess_storage.vue'; -import { formatUsageSize } from 'ee/other_storage_counter/utils'; -import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; -import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue'; -import { projects } from '../mock_data'; - -let wrapper; - -const createComponent = (propsData = {}) => { - wrapper = shallowMount(ProjectWithExcessStorage, { - propsData: { - project: projects[0], - additionalPurchasedStorageSize: 0, - ...propsData, - }, - directives: { - GlTooltip: createMockDirective(), - }, - }); -}; - -const findTableRow = () => wrapper.find('[data-testid="projectTableRow"]'); -const findWarningIcon = () => - wrapper.findAll(GlIcon).wrappers.find((w) => w.props('name') === 'status_warning'); -const findProjectLink = () => wrapper.find(GlLink); -const getWarningIconTooltipText = () => getBinding(findWarningIcon().element, 'gl-tooltip').value; - -describe('Storage Counter project component', () => { - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - describe('without extra storage purchased', () => { - it('renders project avatar', () => { - expect(wrapper.find(ProjectAvatar).exists()).toBe(true); - }); - - it('renders project name', () => { - expect(wrapper.text()).toContain(projects[0].nameWithNamespace); - }); - - it('renders formatted storage size', () => { - expect(wrapper.text()).toContain(formatUsageSize(projects[0].statistics.storageSize)); - }); - - it('does not render the warning icon if project is not in error state', () => { - expect(findWarningIcon()).toBe(undefined); - }); - - it('render row without error state background', () => { - expect(findTableRow().classes('gl-bg-red-50')).toBe(false); - }); - - describe('renders the row in error state', () => { - beforeEach(() => { - createComponent({ project: projects[2] }); - }); - - it('with error state background', () => { - expect(findTableRow().classes('gl-bg-red-50')).toBe(true); - }); - - it('with project link in error state', () => { - expect(findProjectLink().classes('gl-text-red-500!')).toBe(true); - }); - - it('with error icon', () => { - expect(findWarningIcon().exists()).toBe(true); - }); - - it('with tooltip', () => { - expect(getWarningIconTooltipText().title).toBe( - 'This project is locked because it is using 97.7KiB of free storage and there is no purchased storage available.', - ); - }); - }); - - describe('renders the row in warning state', () => { - beforeEach(() => { - createComponent({ project: projects[1] }); - }); - - it('with warning state background', () => { - expect(findTableRow().classes('gl-bg-orange-50')).toBe(true); - }); - - it('with project link in default gray state', () => { - expect(findProjectLink().classes('gl-text-gray-900!')).toBe(true); - }); - - it('with warning icon', () => { - expect(findWarningIcon().exists()).toBe(true); - }); - - it('with tooltip', () => { - expect(getWarningIconTooltipText().title).toBe( - 'This project is near the free 97.7KiB limit and at risk of being locked.', - ); - }); - }); - }); - - describe('with extra storage purchased', () => { - describe('if projects is in error state', () => { - beforeEach(() => { - createComponent({ - project: projects[2], - additionalPurchasedStorageSize: 100000, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders purchased storage specific error tooltip ', () => { - expect(getWarningIconTooltipText().title).toBe( - 'This project is locked because it used 97.7KiB of free storage and all the purchased storage.', - ); - }); - }); - - describe('if projects is in warning state', () => { - beforeEach(() => { - createComponent({ - project: projects[1], - additionalPurchasedStorageSize: 100000, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders purchased storage specific warning tooltip ', () => { - expect(getWarningIconTooltipText().title).toBe( - 'This project is at risk of being locked because purchased storage is running low.', - ); - }); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/projects_skeleton_loader_spec.js b/ee/spec/frontend/other_storage_counter/components/projects_skeleton_loader_spec.js deleted file mode 100644 index 4fe1a86c255830824571948a571e94932dc2ec44..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/projects_skeleton_loader_spec.js +++ /dev/null @@ -1,51 +0,0 @@ -import { mount } from '@vue/test-utils'; -import ProjectsSkeletonLoader from 'ee/other_storage_counter/components/projects_skeleton_loader.vue'; - -describe('ProjectsSkeletonLoader', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = mount(ProjectsSkeletonLoader, { - propsData: { - ...props, - }, - }); - }; - - const findDesktopLoader = () => wrapper.find('[data-testid="desktop-loader"]'); - const findMobileLoader = () => wrapper.find('[data-testid="mobile-loader"]'); - - beforeEach(createComponent); - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - describe('desktop loader', () => { - it('produces 20 rows', () => { - expect(findDesktopLoader().findAll('rect[width="1000"]')).toHaveLength(20); - }); - - it('has the correct classes', () => { - expect(findDesktopLoader().classes()).toEqual([ - 'gl-display-none', - 'gl-md-display-flex', - 'gl-flex-direction-column', - ]); - }); - }); - - describe('mobile loader', () => { - it('produces 5 rows', () => { - expect(findMobileLoader().findAll('rect[height="172"]')).toHaveLength(5); - }); - - it('has the correct classes', () => { - expect(findMobileLoader().classes()).toEqual([ - 'gl-flex-direction-column', - 'gl-md-display-none', - ]); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/projects_table_spec.js b/ee/spec/frontend/other_storage_counter/components/projects_table_spec.js deleted file mode 100644 index 81629d76b839aa9dec82d7e153885f74fc274488..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/projects_table_spec.js +++ /dev/null @@ -1,61 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import Project from 'ee/other_storage_counter/components/project.vue'; -import ProjectWithExcessStorage from 'ee/other_storage_counter/components/project_with_excess_storage.vue'; -import ProjectsTable from 'ee/other_storage_counter/components/projects_table.vue'; -import { projects } from '../mock_data'; - -let wrapper; - -const createComponent = ({ additionalRepoStorageByNamespace = false } = {}) => { - const stubs = { - 'anonymous-stub': additionalRepoStorageByNamespace ? ProjectWithExcessStorage : Project, - }; - - wrapper = shallowMount(ProjectsTable, { - propsData: { - projects, - additionalPurchasedStorageSize: 0, - }, - stubs, - provide: { - glFeatures: { - additionalRepoStorageByNamespace, - }, - }, - }); -}; - -const findTableRows = () => wrapper.findAll(Project); -const findTableRowsWithExcessStorage = () => wrapper.findAll(ProjectWithExcessStorage); - -describe('Usage Quotas project table component', () => { - beforeEach(() => { - createComponent(); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders regular project rows by default', () => { - expect(findTableRows()).toHaveLength(3); - expect(findTableRowsWithExcessStorage()).toHaveLength(0); - }); - - describe('with additional repo storage feature flag ', () => { - beforeEach(() => { - createComponent({ additionalRepoStorageByNamespace: true }); - }); - - it('renders table row with excess storage', () => { - expect(findTableRowsWithExcessStorage()).toHaveLength(3); - }); - - it('renders excess storage rows with error state', () => { - const rowsWithError = findTableRowsWithExcessStorage().filter((r) => - r.classes('gl-bg-red-50'), - ); - expect(rowsWithError).toHaveLength(1); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/storage_inline_alert_spec.js b/ee/spec/frontend/other_storage_counter/components/storage_inline_alert_spec.js deleted file mode 100644 index ac2ba70b10a9bd4200e605e2a35ec95f130ba52d..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/storage_inline_alert_spec.js +++ /dev/null @@ -1,104 +0,0 @@ -import { GlAlert } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import StorageInlineAlert from 'ee/other_storage_counter/components/storage_inline_alert.vue'; - -const GB_IN_BYTES = 1_074_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, - actualRepositorySizeLimit: 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, - actualRepositorySizeLimit: 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 10.0GiB 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, - actualRepositorySizeLimit: 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 10.0GiB 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, - actualRepositorySizeLimit: TEN_GB_IN_BYTES, - }); - }); - - it('renders danger alert', () => { - expect(findAlert().exists()).toBe(true); - expect(findAlert().props('variant')).toBe('danger'); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/storage_row_spec.js b/ee/spec/frontend/other_storage_counter/components/storage_row_spec.js deleted file mode 100644 index 33538273a98d65a1a00ce09641ed2bdc7469603d..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/storage_row_spec.js +++ /dev/null @@ -1,32 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import StorageRow from 'ee/other_storage_counter/components/storage_row.vue'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; - -let wrapper; -const data = { - name: 'LFS Package', - value: 1293346, -}; - -function factory({ name, value }) { - wrapper = shallowMount(StorageRow, { - propsData: { - name, - value, - }, - }); -} - -describe('Storage Counter row component', () => { - beforeEach(() => { - factory(data); - }); - - it('renders provided name', () => { - expect(wrapper.text()).toContain(data.name); - }); - - it('renders formatted value', () => { - expect(wrapper.text()).toContain(numberToHumanSize(data.value)); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/temporary_storage_increase_modal_spec.js b/ee/spec/frontend/other_storage_counter/components/temporary_storage_increase_modal_spec.js deleted file mode 100644 index 57810dbf960886e03f4fe4da7a50015d9556d1bc..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/temporary_storage_increase_modal_spec.js +++ /dev/null @@ -1,47 +0,0 @@ -import { GlModal } from '@gitlab/ui'; -import { mount, shallowMount } from '@vue/test-utils'; -import TemporaryStorageIncreaseModal from 'ee/other_storage_counter/components/temporary_storage_increase_modal.vue'; - -const TEST_LIMIT = '8 bytes'; -const TEST_MODAL_ID = 'test-modal-id'; - -describe('Temporary storage increase modal', () => { - let wrapper; - - const createComponent = (mountFn, props = {}) => { - wrapper = mountFn(TemporaryStorageIncreaseModal, { - propsData: { - modalId: TEST_MODAL_ID, - limit: TEST_LIMIT, - ...props, - }, - }); - }; - const findModal = () => wrapper.find(GlModal); - const showModal = () => { - findModal().vm.show(); - return wrapper.vm.$nextTick(); - }; - const findModalText = () => document.body.innerText; - - afterEach(() => { - wrapper.destroy(); - wrapper = null; - }); - - it('shows modal message', async () => { - createComponent(mount); - - await showModal(); - - const text = findModalText(); - expect(text).toContain('GitLab allows you a free, one-time storage increase.'); - expect(text).toContain(`your original storage limit of ${TEST_LIMIT} applies.`); - }); - - it('passes along modalId', () => { - createComponent(shallowMount); - - expect(findModal().attributes('modalid')).toBe(TEST_MODAL_ID); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/usage_graph_spec.js b/ee/spec/frontend/other_storage_counter/components/usage_graph_spec.js deleted file mode 100644 index 3df005b17f8be1db71fda8c7af6691781c50859c..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/usage_graph_spec.js +++ /dev/null @@ -1,137 +0,0 @@ -import { shallowMount } from '@vue/test-utils'; -import UsageGraph from 'ee/other_storage_counter/components/usage_graph.vue'; -import { numberToHumanSize } from '~/lib/utils/number_utils'; - -let data; -let wrapper; - -function mountComponent({ rootStorageStatistics, limit }) { - wrapper = shallowMount(UsageGraph, { - propsData: { - rootStorageStatistics, - limit, - }, - }); -} -function findStorageTypeUsagesSerialized() { - return wrapper - .findAll('[data-testid="storage-type-usage"]') - .wrappers.map((wp) => wp.element.style.flex); -} - -describe('Storage Counter usage graph component', () => { - beforeEach(() => { - data = { - rootStorageStatistics: { - wikiSize: 5000, - repositorySize: 4000, - packagesSize: 3000, - lfsObjectsSize: 2000, - buildArtifactsSize: 500, - pipelineArtifactsSize: 500, - snippetsSize: 2000, - storageSize: 17000, - uploadsSize: 1000, - }, - limit: 2000, - }; - mountComponent(data); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders the legend in order', () => { - const types = wrapper.findAll('[data-testid="storage-type-legend"]'); - - const { - buildArtifactsSize, - pipelineArtifactsSize, - lfsObjectsSize, - packagesSize, - repositorySize, - wikiSize, - snippetsSize, - uploadsSize, - } = data.rootStorageStatistics; - - expect(types.at(0).text()).toMatchInterpolatedText(`Wikis ${numberToHumanSize(wikiSize)}`); - expect(types.at(1).text()).toMatchInterpolatedText( - `Repositories ${numberToHumanSize(repositorySize)}`, - ); - expect(types.at(2).text()).toMatchInterpolatedText( - `Packages ${numberToHumanSize(packagesSize)}`, - ); - expect(types.at(3).text()).toMatchInterpolatedText( - `LFS Objects ${numberToHumanSize(lfsObjectsSize)}`, - ); - expect(types.at(4).text()).toMatchInterpolatedText( - `Snippets ${numberToHumanSize(snippetsSize)}`, - ); - expect(types.at(5).text()).toMatchInterpolatedText( - `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`, - ); - expect(types.at(6).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`); - }); - - describe('when storage type is not used', () => { - beforeEach(() => { - data.rootStorageStatistics.wikiSize = 0; - mountComponent(data); - }); - - it('filters the storage type', () => { - expect(wrapper.text()).not.toContain('Wikis'); - }); - }); - - describe('when there is no storage usage', () => { - beforeEach(() => { - data.rootStorageStatistics.storageSize = 0; - mountComponent(data); - }); - - it('it does not render', () => { - expect(wrapper.html()).toEqual(''); - }); - }); - - describe('when limit is 0', () => { - beforeEach(() => { - data.limit = 0; - mountComponent(data); - }); - - it('sets correct flex values', () => { - expect(findStorageTypeUsagesSerialized()).toStrictEqual([ - '0.29411764705882354', - '0.23529411764705882', - '0.17647058823529413', - '0.11764705882352941', - '0.11764705882352941', - '0.058823529411764705', - '0.058823529411764705', - ]); - }); - }); - - describe('when storage exceeds limit', () => { - beforeEach(() => { - data.limit = data.rootStorageStatistics.storageSize - 1; - mountComponent(data); - }); - - it('it does render correclty', () => { - expect(findStorageTypeUsagesSerialized()).toStrictEqual([ - '0.29411764705882354', - '0.23529411764705882', - '0.17647058823529413', - '0.11764705882352941', - '0.11764705882352941', - '0.058823529411764705', - '0.058823529411764705', - ]); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/components/usage_statistics_spec.js b/ee/spec/frontend/other_storage_counter/components/usage_statistics_spec.js deleted file mode 100644 index e90e13d1a430ff2a8b5d48480932a016aa6015f6..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/components/usage_statistics_spec.js +++ /dev/null @@ -1,81 +0,0 @@ -import { GlButton, GlLink, GlSprintf } from '@gitlab/ui'; -import { shallowMount } from '@vue/test-utils'; -import UsageStatistics from 'ee/other_storage_counter/components/usage_statistics.vue'; -import UsageStatisticsCard from 'ee/other_storage_counter/components/usage_statistics_card.vue'; -import { withRootStorageStatistics } from '../mock_data'; - -describe('Usage Statistics component', () => { - let wrapper; - - const createComponent = (props = {}) => { - wrapper = shallowMount(UsageStatistics, { - propsData: { - rootStorageStatistics: { - totalRepositorySize: withRootStorageStatistics.totalRepositorySize, - actualRepositorySizeLimit: withRootStorageStatistics.actualRepositorySizeLimit, - totalRepositorySizeExcess: withRootStorageStatistics.totalRepositorySizeExcess, - additionalPurchasedStorageSize: withRootStorageStatistics.additionalPurchasedStorageSize, - }, - ...props, - }, - stubs: { - UsageStatisticsCard, - GlSprintf, - GlLink, - }, - }); - }; - - const getStatisticsCards = () => wrapper.findAll(UsageStatisticsCard); - const getStatisticsCard = (testId) => wrapper.find(`[data-testid="${testId}"]`); - const findGlLinkInCard = (cardName) => - getStatisticsCard(cardName).find('[data-testid="statistics-card-footer"]').find(GlLink); - - describe('with purchaseStorageUrl passed', () => { - beforeEach(() => { - createComponent({ - purchaseStorageUrl: 'some-fancy-url', - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('renders three statistics cards', () => { - expect(getStatisticsCards()).toHaveLength(3); - }); - - it('renders URL in total usage card footer', () => { - const url = findGlLinkInCard('total-usage'); - - expect(url.attributes('href')).toBe('/help/user/usage_quotas'); - }); - - it('renders URL in excess usage card footer', () => { - const url = findGlLinkInCard('excess-usage'); - - expect(url.attributes('href')).toBe('/help/user/usage_quotas#excess-storage-usage'); - }); - - it('renders button in purchased usage card footer', () => { - expect(getStatisticsCard('purchased-usage').find(GlButton).exists()).toBe(true); - }); - }); - - describe('with no purchaseStorageUrl', () => { - beforeEach(() => { - createComponent({ - purchaseStorageUrl: null, - }); - }); - - afterEach(() => { - wrapper.destroy(); - }); - - it('does not render purchased usage card if purchaseStorageUrl is not provided', () => { - expect(getStatisticsCard('purchased-usage').exists()).toBe(false); - }); - }); -}); diff --git a/ee/spec/frontend/other_storage_counter/mock_data.js b/ee/spec/frontend/other_storage_counter/mock_data.js deleted file mode 100644 index 84c03a13d94fd825dcef3a1a0490cd6fc99656ed..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/mock_data.js +++ /dev/null @@ -1,90 +0,0 @@ -export const projects = [ - { - id: '24', - fullPath: 'h5bp/dummy-project', - nameWithNamespace: 'H5bp / dummy project', - avatarUrl: null, - webUrl: 'http://localhost:3001/h5bp/dummy-project', - name: 'dummy project', - statistics: { - commitCount: 1, - storageSize: 41943, - repositorySize: 41943, - lfsObjectsSize: 0, - buildArtifactsSize: 0, - packagesSize: 0, - }, - actualRepositorySizeLimit: 100000, - totalCalculatedUsedStorage: 41943, - totalCalculatedStorageLimit: 41943000, - }, - { - id: '8', - fullPath: 'h5bp/html5-boilerplate', - nameWithNamespace: 'H5bp / Html5 Boilerplate', - avatarUrl: null, - webUrl: 'http://localhost:3001/h5bp/html5-boilerplate', - name: 'Html5 Boilerplate', - statistics: { - commitCount: 0, - storageSize: 99000, - repositorySize: 0, - lfsObjectsSize: 0, - buildArtifactsSize: 1272375, - packagesSize: 0, - }, - actualRepositorySizeLimit: 100000, - totalCalculatedUsedStorage: 89000, - totalCalculatedStorageLimit: 99430, - }, - { - id: '80', - fullPath: 'twit/twitter', - nameWithNamespace: 'Twitter', - avatarUrl: null, - webUrl: 'http://localhost:3001/twit/twitter', - name: 'Twitter', - statistics: { - commitCount: 0, - storageSize: 12933460, - repositorySize: 209710, - lfsObjectsSize: 209720, - buildArtifactsSize: 1272375, - packagesSize: 0, - }, - actualRepositorySizeLimit: 100000, - totalCalculatedUsedStorage: 13143170, - totalCalculatedStorageLimit: 12143170, - }, -]; - -export const namespaceData = { - totalUsage: 'N/A', - limit: 10000000, - projects: { data: projects }, -}; - -export const withRootStorageStatistics = { - projects, - limit: 10000000, - totalUsage: 129334601, - containsLockedProjects: true, - repositorySizeExcessProjectCount: 1, - totalRepositorySizeExcess: 2321, - totalRepositorySize: 1002321, - additionalPurchasedStorageSize: 321, - actualRepositorySizeLimit: 1002321, - rootStorageStatistics: { - storageSize: 129334601, - repositorySize: 46012030, - lfsObjectsSize: 4329334601203, - buildArtifactsSize: 1272375, - packagesSize: 123123120, - wikiSize: 1000, - snippetsSize: 10000, - }, -}; - -export const mockGetStorageCounterGraphQLResponse = { - nodes: projects.map((node) => node), -}; diff --git a/ee/spec/frontend/other_storage_counter/utils_spec.js b/ee/spec/frontend/other_storage_counter/utils_spec.js deleted file mode 100644 index c74c22f8f637bb57ccb157b4e342fdc7f1e5b087..0000000000000000000000000000000000000000 --- a/ee/spec/frontend/other_storage_counter/utils_spec.js +++ /dev/null @@ -1,84 +0,0 @@ -import { - usageRatioToThresholdLevel, - formatUsageSize, - parseProjects, - calculateUsedAndRemStorage, -} from 'ee/other_storage_counter/utils'; -import { projects as mockProjectsData, mockGetStorageCounterGraphQLResponse } from './mock_data'; - -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); - }); -}); - -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); - }); -}); - -describe('calculateUsedAndRemStorage', () => { - it.each` - description | project | purchasedStorageRemaining | totalCalculatedUsedStorage | totalCalculatedStorageLimit - ${'project within limit and purchased 0'} | ${mockProjectsData[0]} | ${0} | ${41943} | ${100000} - ${'project within limit and purchased 10000'} | ${mockProjectsData[0]} | ${100000} | ${41943} | ${200000} - ${'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 in error state and purchased 0'} | ${mockProjectsData[2]} | ${0} | ${419430} | ${419430} - ${'project in error state and purchased 10000'} | ${mockProjectsData[2]} | ${100000} | ${419430} | ${519430} - `( - '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: mockGetStorageCounterGraphQLResponse, - additionalPurchasedStorageSize: 10000, - totalRepositorySizeExcess: 5000, - }); - - projects.forEach((project) => { - expect(project).toMatchObject({ - totalCalculatedUsedStorage: expect.any(Number), - totalCalculatedStorageLimit: expect.any(Number), - }); - }); - }); -}); diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 886e476df0b78a3e3b04c7d76c19f99cb884a8ab..657a2494028102e2d071d7b866a6e0b6cebc1239 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -36013,9 +36013,6 @@ msgstr "" msgid "UsageQuota|Learn more about usage quotas" msgstr "" -msgid "UsageQuota|Other Storage" -msgstr "" - msgid "UsageQuota|Packages" msgstr ""