diff options
Diffstat (limited to 'app/assets/javascripts/usage_quotas/storage/components')
3 files changed, 305 insertions, 3 deletions
diff --git a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue index a812b90e378..1594e125da3 100644 --- a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue +++ b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue @@ -1,20 +1,23 @@ <script> -import { GlAlert } from '@gitlab/ui'; +import { GlAlert, GlKeysetPagination } from '@gitlab/ui'; import StorageUsageStatistics from 'ee_else_ce/usage_quotas/storage/components/storage_usage_statistics.vue'; import SearchAndSortBar from '~/usage_quotas/components/search_and_sort_bar/search_and_sort_bar.vue'; import DependencyProxyUsage from './dependency_proxy_usage.vue'; import ContainerRegistryUsage from './container_registry_usage.vue'; +import ProjectList from './project_list.vue'; export default { name: 'NamespaceStorageApp', components: { GlAlert, + GlKeysetPagination, StorageUsageStatistics, DependencyProxyUsage, ContainerRegistryUsage, SearchAndSortBar, + ProjectList, }, - inject: ['userNamespace', 'namespaceId'], + inject: ['userNamespace', 'namespaceId', 'helpLinks', 'defaultPerPage'], props: { namespaceLoadingError: { type: Boolean, @@ -31,11 +34,26 @@ export default { required: false, default: false, }, + isNamespaceProjectsLoading: { + type: Boolean, + required: false, + default: false, + }, namespace: { type: Object, required: false, default: () => ({}), }, + projects: { + type: Object, + required: false, + default: () => ({}), + }, + initialSortBy: { + type: String, + required: false, + default: 'storage', + }, }, computed: { usedStorage() { @@ -55,6 +73,27 @@ export default { containerRegistrySizeIsEstimated() { return this.namespace.rootStorageStatistics?.containerRegistrySizeIsEstimated ?? false; }, + projectList() { + return this.projects?.nodes ?? []; + }, + pageInfo() { + return this.projects?.pageInfo; + }, + showPagination() { + return Boolean(this.pageInfo?.hasPreviousPage || this.pageInfo?.hasNextPage); + }, + }, + methods: { + onPrev(before) { + if (this.pageInfo?.hasPreviousPage) { + this.$emit('fetch-more-projects', { before, last: this.defaultPerPage, first: undefined }); + } + }, + onNext(after) { + if (this.pageInfo?.hasNextPage) { + this.$emit('fetch-more-projects', { after, first: this.defaultPerPage }); + } + }, }, }; </script> @@ -103,7 +142,26 @@ export default { " /> </div> - <slot name="ee-storage-app"></slot> + <project-list + :projects="projectList" + :is-loading="isNamespaceProjectsLoading" + :help-links="helpLinks" + :sort-by="initialSortBy" + :sort-desc="true" + @sortChanged=" + ($event) => { + $emit('sort-changed', $event); + } + " + /> + <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> </section> </div> </template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_list.vue b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue new file mode 100644 index 00000000000..c6f9b1fff03 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/project_list.vue @@ -0,0 +1,208 @@ +<script> +import { GlTable, GlLink, GlSprintf, GlIcon } from '@gitlab/ui'; +import { __ } from '~/locale'; +import ProjectAvatar from '~/vue_shared/components/project_avatar.vue'; +import { containerRegistryPopover } from '~/usage_quotas/storage/constants'; +import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue'; +import HelpPageLink from '~/vue_shared/components/help_page_link/help_page_link.vue'; +import StorageTypeHelpLink from './storage_type_help_link.vue'; +import StorageTypeWarning from './storage_type_warning.vue'; + +export default { + name: 'ProjectList', + components: { + GlTable, + GlLink, + GlSprintf, + GlIcon, + ProjectAvatar, + NumberToHumanSize, + HelpPageLink, + StorageTypeHelpLink, + StorageTypeWarning, + }, + inject: ['isUsingProjectEnforcementWithLimits'], + props: { + projects: { + type: Array, + required: true, + }, + isLoading: { + type: Boolean, + required: true, + }, + helpLinks: { + type: Object, + required: true, + }, + sortBy: { + type: String, + required: false, + default: undefined, + }, + sortDesc: { + type: Boolean, + required: false, + default: undefined, + }, + }, + created() { + this.fields = [ + { key: 'name', label: __('Project') }, + { key: 'storage', label: __('Total'), sortable: !this.isUsingProjectEnforcementWithLimits }, + { key: 'repository', label: __('Repository') }, + { key: 'snippets', label: __('Snippets') }, + { key: 'buildArtifacts', label: __('Jobs') }, + { key: 'lfsObjects', label: __('LFS') }, + { key: 'packages', label: __('Packages') }, + { key: 'wiki', label: __('Wiki') }, + { + key: 'containerRegistry', + label: __('Containers'), + thClass: 'gl-border-l!', + tdClass: 'gl-border-l!', + }, + ].map((f) => ({ + ...f, + // eslint-disable-next-line @gitlab/require-i18n-strings + thClass: `${f.thClass ?? ''} gl-px-3!`, + // eslint-disable-next-line @gitlab/require-i18n-strings + tdClass: `${f.tdClass ?? ''} gl-px-3!`, + })); + }, + methods: { + /** + * Builds a gl-table td cell slot name for particular field + * @param {string} key + * @returns {string} */ + getHeaderSlotName(key) { + return `head(${key})`; + }, + getUsageQuotasUrl(projectUrl) { + return `${projectUrl}/-/usage_quotas`; + }, + /** + * Creates a relative path from a full project path. + * E.g. input `namespace / subgroup / project` + * results in `subgroup / project` + */ + getProjectRelativePath(fullPath) { + return fullPath.replace(/.*?\s?\/\s?/, ''); + }, + isCostFactored(project) { + return project.statistics.storageSize !== project.statistics.costFactoredStorageSize; + }, + }, + containerRegistryPopover, +}; +</script> + +<template> + <gl-table + :fields="fields" + :items="projects" + :busy="isLoading" + show-empty + :empty-text="s__('UsageQuota|No projects to display.')" + small + stacked="lg" + :sort-by="sortBy" + :sort-desc="sortDesc" + no-local-sorting + no-sort-reset + @sort-changed="$emit('sortChanged', $event)" + > + <template v-for="field in fields" #[getHeaderSlotName(field.key)]> + <div :key="field.key" :data-testid="'th-' + field.key"> + {{ field.label }} + + <storage-type-help-link + v-if="field.key in helpLinks" + :storage-type="field.key" + :help-links="helpLinks" + /><storage-type-warning v-if="field.key == 'containerRegistry'"> + {{ $options.containerRegistryPopover.content }} + <gl-link :href="$options.containerRegistryPopover.docsLink" target="_blank"> + {{ __('Learn more.') }} + </gl-link> + </storage-type-warning> + </div> + </template> + + <template #cell(name)="{ item: project }"> + <project-avatar + :project-id="project.id" + :project-name="project.name" + :project-avatar-url="project.avatarUrl" + :size="16" + :alt="project.name" + class="gl-display-inline-block gl-mr-2 gl-text-center!" + /> + + <gl-link + :href="getUsageQuotasUrl(project.webUrl)" + class="gl-text-gray-900! js-project-link gl-word-break-word" + data-testid="project-link" + > + {{ getProjectRelativePath(project.nameWithNamespace) }} + </gl-link> + </template> + + <template #cell(storage)="{ item: project }"> + <template v-if="isCostFactored(project)"> + <number-to-human-size :value="project.statistics.costFactoredStorageSize" /> + + <div class="gl-text-gray-600 gl-mt-2 gl-font-sm"> + <gl-sprintf :message="s__('UsageQuotas|(of %{totalStorageSize})')"> + <template #totalStorageSize> + <number-to-human-size :value="project.statistics.storageSize" /> + </template> + </gl-sprintf> + <help-page-link href="user/usage_quotas#view-project-fork-storage-usage" target="_blank"> + <gl-icon name="question-o" :size="12" /> + </help-page-link> + </div> + </template> + <template v-else> + <number-to-human-size :value="project.statistics.storageSize" /> + </template> + </template> + + <template #cell(repository)="{ item: project }"> + <number-to-human-size + :value="project.statistics.repositorySize" + data-testid="project-repository-size" + /> + </template> + + <template #cell(lfsObjects)="{ item: project }"> + <number-to-human-size :value="project.statistics.lfsObjectsSize" /> + </template> + + <template #cell(buildArtifacts)="{ item: project }"> + <number-to-human-size :value="project.statistics.buildArtifactsSize" /> + </template> + + <template #cell(packages)="{ item: project }"> + <number-to-human-size :value="project.statistics.packagesSize" /> + </template> + + <template #cell(wiki)="{ item: project }"> + <number-to-human-size :value="project.statistics.wikiSize" data-testid="project-wiki-size" /> + </template> + + <template #cell(snippets)="{ item: project }"> + <number-to-human-size + :value="project.statistics.snippetsSize" + data-testid="project-snippets-size" + /> + </template> + + <template #cell(containerRegistry)="{ item: project }"> + <number-to-human-size + :value="project.statistics.containerRegistrySize" + data-testid="project-containers-registry-size" + /> + </template> + </gl-table> +</template> diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue new file mode 100644 index 00000000000..c25b1848124 --- /dev/null +++ b/app/assets/javascripts/usage_quotas/storage/components/storage_type_help_link.vue @@ -0,0 +1,36 @@ +<script> +import { GlLink, GlIcon } from '@gitlab/ui'; +import { sprintf } from '~/locale'; +import { HELP_LINK_ARIA_LABEL } from '~/usage_quotas/storage/constants'; + +export default { + name: 'StorageTypeHelpLink', + components: { + GlLink, + GlIcon, + }, + props: { + storageType: { + type: String, + required: true, + }, + helpLinks: { + type: Object, + required: true, + }, + }, + computed: { + ariaLabel() { + return sprintf(HELP_LINK_ARIA_LABEL, { + linkTitle: this.storageType, + }); + }, + }, +}; +</script> + +<template> + <gl-link :href="helpLinks[storageType]" target="_blank" :aria-label="ariaLabel"> + <gl-icon name="question-o" :size="12" /> + </gl-link> +</template> |