diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-12-20 16:37:47 +0300 |
commit | aee0a117a889461ce8ced6fcf73207fe017f1d99 (patch) | |
tree | 891d9ef189227a8445d83f35c1b0fc99573f4380 /app/assets/javascripts/packages_and_registries | |
parent | 8d46af3258650d305f53b819eabf7ab18d22f59e (diff) |
Add latest changes from gitlab-org/gitlab@14-6-stable-eev14.6.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
59 files changed, 1522 insertions, 173 deletions
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue index f857c96c9d1..7a8a1bbcf09 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_modal.vue @@ -82,6 +82,7 @@ export default { ref="deleteModal" modal-id="delete-tag-modal" ok-variant="danger" + size="sm" :action-primary="{ text: __('Delete'), attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }], diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue index e9e36151fe6..d988ad8d8ca 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue @@ -46,7 +46,6 @@ export default { data() { return { containerRepository: {}, - fetchTagsCount: false, }; }, apollo: { diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue index 3e19a646f53..2d32295b537 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue @@ -1,7 +1,8 @@ <script> -import { GlButton, GlKeysetPagination } from '@gitlab/ui'; import createFlash from '~/flash'; +import { n__ } from '~/locale'; import { joinPaths } from '~/lib/utils/url_utility'; +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE, @@ -16,11 +17,10 @@ import TagsLoader from './tags_loader.vue'; export default { name: 'TagsList', components: { - GlButton, - GlKeysetPagination, TagsListRow, EmptyState, TagsLoader, + RegistryList, }, inject: ['config'], props: { @@ -61,11 +61,13 @@ export default { }, data() { return { - selectedItems: {}, containerRepository: {}, }; }, computed: { + listTitle() { + return n__('%d tag', '%d tags', this.tags.length); + }, tags() { return this.containerRepository?.tags?.nodes || []; }, @@ -78,18 +80,9 @@ export default { first: GRAPHQL_PAGE_SIZE, }; }, - hasSelectedItems() { - return this.tags.some((tag) => this.selectedItems[tag.name]); - }, showMultiDeleteButton() { return this.tags.some((tag) => tag.canDelete) && !this.isMobile; }, - multiDeleteButtonIsDisabled() { - return !this.hasSelectedItems || this.disabled; - }, - showPagination() { - return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage; - }, hasNoTags() { return this.tags.length === 0; }, @@ -98,19 +91,13 @@ export default { }, }, methods: { - updateSelectedItems(name) { - this.$set(this.selectedItems, name, !this.selectedItems[name]); - }, - mapTagsToBeDleeted(items) { - return this.tags.filter((tag) => items[tag.name]); - }, fetchNextPage() { this.$apollo.queries.containerRepository.fetchMore({ variables: { after: this.tagsPageInfo?.endCursor, first: GRAPHQL_PAGE_SIZE, }, - updateQuery(previousResult, { fetchMoreResult }) { + updateQuery(_, { fetchMoreResult }) { return fetchMoreResult; }, }); @@ -122,7 +109,7 @@ export default { before: this.tagsPageInfo?.startCursor, last: GRAPHQL_PAGE_SIZE, }, - updateQuery(previousResult, { fetchMoreResult }) { + updateQuery(_, { fetchMoreResult }) { return fetchMoreResult; }, }); @@ -137,42 +124,27 @@ export default { <template v-else> <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" /> <template v-else> - <div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> - <h5 data-testid="list-title"> - {{ $options.i18n.TAGS_LIST_TITLE }} - </h5> - - <gl-button - v-if="showMultiDeleteButton" - :disabled="multiDeleteButtonIsDisabled" - category="secondary" - variant="danger" - @click="$emit('delete', mapTagsToBeDleeted(selectedItems))" - > - {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }} - </gl-button> - </div> - <tags-list-row - v-for="(tag, index) in tags" - :key="tag.path" - :tag="tag" - :first="index === 0" - :selected="selectedItems[tag.name]" - :is-mobile="isMobile" - :disabled="disabled" - @select="updateSelectedItems(tag.name)" - @delete="$emit('delete', mapTagsToBeDleeted({ [tag.name]: true }))" - /> - <div class="gl-display-flex gl-justify-content-center"> - <gl-keyset-pagination - v-if="showPagination" - :has-next-page="tagsPageInfo.hasNextPage" - :has-previous-page="tagsPageInfo.hasPreviousPage" - class="gl-mt-3" - @prev="fetchPreviousPage" - @next="fetchNextPage" - /> - </div> + <registry-list + :title="listTitle" + :pagination="tagsPageInfo" + :items="tags" + id-property="name" + @prev-page="fetchPreviousPage" + @next-page="fetchNextPage" + @delete="$emit('delete', $event)" + > + <template #default="{ selectItem, isSelected, item, first }"> + <tags-list-row + :tag="item" + :first="first" + :selected="isSelected(item)" + :is-mobile="isMobile" + :disabled="disabled" + @select="selectItem(item)" + @delete="$emit('delete', [item])" + /> + </template> + </registry-list> </template> </template> </div> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql index 01cb7fa1cab..bc34e9b5ef2 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repositories_details.query.graphql @@ -9,6 +9,7 @@ query getContainerRepositoriesDetails( $sort: ContainerRepositorySort ) { project(fullPath: $fullPath) @skip(if: $isGroupPage) { + id containerRepositories( name: $name after: $after @@ -24,6 +25,7 @@ query getContainerRepositoriesDetails( } } group(fullPath: $fullPath) @include(if: $isGroupPage) { + id containerRepositories( name: $name after: $after diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql index b5a99fd9ac1..916740f41b8 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_details.query.graphql @@ -11,6 +11,7 @@ query getContainerRepositoryDetails($id: ID!) { expirationPolicyStartedAt expirationPolicyCleanupStatus project { + id visibility path containerExpirationPolicy { diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql index a703c2dd0ac..502382010f9 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql @@ -9,6 +9,7 @@ query getContainerRepositoryTags( ) { containerRepository(id: $id) { id + tagsCount tags(after: $after, before: $before, first: $first, last: $last) { nodes { digest diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue index feabc4f770b..bc6e3091f0e 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue @@ -25,9 +25,11 @@ import { UNFINISHED_STATUS, MISSING_OR_DELETED_IMAGE_BREADCRUMB, ROOT_IMAGE_TEXT, + GRAPHQL_PAGE_SIZE, } from '../constants/index'; import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql'; import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql'; +import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql'; export default { name: 'RegistryDetailsPage', @@ -133,8 +135,8 @@ export default { awaitRefetchQueries: true, refetchQueries: [ { - query: getContainerRepositoryDetailsQuery, - variables: this.queryVariables, + query: getContainerRepositoryTagsQuery, + variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE }, }, ], }); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue index 73b957f42f2..3274de05803 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue @@ -388,6 +388,7 @@ export default { <template #default="{ doDelete }"> <gl-modal ref="deleteModal" + size="sm" modal-id="delete-image-modal" :action-primary="{ text: __('Remove'), attributes: { variant: 'danger' } }" @primary="doDelete" diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue index 71e8cf4f634..eb112238c11 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -1,11 +1,11 @@ <script> import { GlAlert, + GlEmptyState, GlFormGroup, GlFormInputGroup, GlSkeletonLoader, GlSprintf, - GlEmptyState, } from '@gitlab/ui'; import { s__ } from '~/locale'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; @@ -36,15 +36,15 @@ export default { proxyNotAvailableText: s__( 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.', ), - proxyDisabledText: s__( - 'DependencyProxy|Dependency Proxy disabled. To enable it, contact the group owner.', - ), proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'), copyImagePrefixText: s__('DependencyProxy|Copy prefix'), blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'), pageTitle: s__('DependencyProxy|Dependency Proxy'), noManifestTitle: s__('DependencyProxy|There are no images in the cache'), }, + links: { + DEPENDENCY_PROXY_DOCS_PATH, + }, data() { return { group: {}, @@ -70,9 +70,7 @@ export default { }, ]; }, - dependencyProxyEnabled() { - return this.group?.dependencyProxySetting?.enabled; - }, + queryVariables() { return { fullPath: this.groupPath, first: GRAPHQL_PAGE_SIZE }; }, @@ -122,7 +120,7 @@ export default { <gl-skeleton-loader v-else-if="$apollo.queries.group.loading" /> - <div v-else-if="dependencyProxyEnabled" data-testid="main-area"> + <div v-else data-testid="main-area"> <gl-form-group :label="$options.i18n.proxyImagePrefix"> <gl-form-input-group readonly @@ -161,8 +159,5 @@ export default { :title="$options.i18n.noManifestTitle" /> </div> - <gl-alert v-else :dismissible="false" data-testid="proxy-disabled"> - {{ $options.i18n.proxyDisabledText }} - </gl-alert> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql index 63d5469c955..9241dccb2d5 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql @@ -8,6 +8,7 @@ query getDependencyProxyDetails( $before: String ) { group(fullPath: $fullPath) { + id dependencyProxyBlobCount dependencyProxyTotalSize dependencyProxyImagePrefix @@ -16,6 +17,7 @@ query getDependencyProxyDetails( } dependencyProxyManifests(after: $after, before: $before, first: $first, last: $last) { nodes { + id createdAt imageName } diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue index 6016757c1b9..f198d2e1bfa 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue @@ -16,10 +16,13 @@ import { s__, __ } from '~/locale'; import TerraformTitle from '~/packages_and_registries/infrastructure_registry/details/components/details_title.vue'; import TerraformInstallation from '~/packages_and_registries/infrastructure_registry/details/components/terraform_installation.vue'; import Tracking from '~/tracking'; -import PackageListRow from '~/packages/shared/components/package_list_row.vue'; -import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; -import { TrackingActions, SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; -import { packageTypeToTrackCategory } from '~/packages/shared/utils'; +import PackageListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; +import { + TRACKING_ACTIONS, + SHOW_DELETE_SUCCESS_ALERT, +} from '~/packages_and_registries/shared/constants'; +import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; import PackageFiles from './package_files.vue'; import PackageHistory from './package_history.vue'; @@ -44,7 +47,7 @@ export default { GlModal: GlModalDirective, }, mixins: [Tracking.mixin()], - trackingActions: { ...TrackingActions }, + trackingActions: { ...TRACKING_ACTIONS }, data() { return { fileToDelete: null, @@ -68,7 +71,7 @@ export default { }, tracking() { return { - category: packageTypeToTrackCategory(this.packageEntity.package_type), + category: TRACK_CATEGORY, }; }, hasVersions() { @@ -86,7 +89,7 @@ export default { } }, async confirmPackageDeletion() { - this.track(TrackingActions.DELETE_PACKAGE); + this.track(TRACKING_ACTIONS.DELETE_PACKAGE); await this.deletePackage(); const returnTo = !this.groupListUrl || document.referrer.includes(this.projectName) @@ -96,12 +99,12 @@ export default { window.location.replace(`${returnTo}?${modalQuery}`); }, handleFileDelete(file) { - this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE); + this.track(TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE_FILE); this.fileToDelete = { ...file }; this.$refs.deleteFileModal.show(); }, confirmFileDelete() { - this.track(TrackingActions.DELETE_PACKAGE_FILE); + this.track(TRACKING_ACTIONS.DELETE_PACKAGE_FILE); this.deletePackageFile(this.fileToDelete.id); this.fileToDelete = null; }, @@ -203,6 +206,7 @@ export default { <gl-modal ref="deleteModal" + size="sm" modal-id="delete-modal" :action-primary="$options.modal.packageDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" @@ -223,6 +227,7 @@ export default { <gl-modal ref="deleteFileModal" + size="sm" modal-id="delete-file-modal" :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js index a03fa8d9d63..26d4aa13715 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/store/actions.js @@ -4,7 +4,7 @@ import { DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, -} from '~/packages/shared/constants'; +} from '~/packages_and_registries/shared/constants'; import { FETCH_PACKAGE_VERSIONS_ERROR } from '../constants'; import * as types from './mutation_types'; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue index 4928da862ea..c611f92036d 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_search.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue @@ -1,7 +1,7 @@ <script> import { mapState, mapActions } from 'vuex'; -import { LIST_KEY_PACKAGE_TYPE } from '~/packages/list/constants'; -import { sortableFields } from '~/packages/list/utils'; +import { LIST_KEY_PACKAGE_TYPE } from '~/packages_and_registries/infrastructure_registry/list/constants'; +import { sortableFields } from '~/packages_and_registries/infrastructure_registry/list/utils'; import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue'; import UrlSync from '~/vue_shared/components/url_sync.vue'; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue index 2a479c65d0c..2a479c65d0c 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_title.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue new file mode 100644 index 00000000000..a5f367bc1f6 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue @@ -0,0 +1,127 @@ +<script> +import { GlPagination, GlModal, GlSprintf } from '@gitlab/ui'; +import { mapState, mapGetters } from 'vuex'; +import { s__ } from '~/locale'; +import Tracking from '~/tracking'; +import PackagesListRow from '~/packages_and_registries/infrastructure_registry/shared/package_list_row.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; +import { TRACKING_ACTIONS } from '~/packages_and_registries/shared/constants'; +import { TRACK_CATEGORY } from '~/packages_and_registries/infrastructure_registry/shared/constants'; + +export default { + components: { + GlPagination, + GlModal, + GlSprintf, + PackagesListLoader, + PackagesListRow, + }, + mixins: [Tracking.mixin()], + data() { + return { + itemToBeDeleted: null, + }; + }, + computed: { + ...mapState({ + perPage: (state) => state.pagination.perPage, + totalItems: (state) => state.pagination.total, + page: (state) => state.pagination.page, + isGroupPage: (state) => state.config.isGroupPage, + isLoading: 'isLoading', + }), + ...mapGetters({ list: 'getList' }), + currentPage: { + get() { + return this.page; + }, + set(value) { + this.$emit('page:changed', value); + }, + }, + isListEmpty() { + return !this.list || this.list.length === 0; + }, + modalAction() { + return s__('PackageRegistry|Delete package'); + }, + deletePackageName() { + return this.itemToBeDeleted?.name ?? ''; + }, + tracking() { + return { + category: TRACK_CATEGORY, + }; + }, + }, + methods: { + setItemToBeDeleted(item) { + this.itemToBeDeleted = { ...item }; + this.track(TRACKING_ACTIONS.REQUEST_DELETE_PACKAGE); + this.$refs.packageListDeleteModal.show(); + }, + deleteItemConfirmation() { + this.$emit('package:delete', this.itemToBeDeleted); + this.track(TRACKING_ACTIONS.DELETE_PACKAGE); + this.itemToBeDeleted = null; + }, + deleteItemCanceled() { + this.track(TRACKING_ACTIONS.CANCEL_DELETE_PACKAGE); + this.itemToBeDeleted = null; + }, + }, + i18n: { + deleteModalContent: s__( + 'PackageRegistry|You are about to delete %{name}, this operation is irreversible, are you sure?', + ), + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-flex-direction-column"> + <slot v-if="isListEmpty && !isLoading" name="empty-state"></slot> + + <div v-else-if="isLoading"> + <packages-list-loader /> + </div> + + <template v-else> + <div data-qa-selector="packages-table"> + <packages-list-row + v-for="packageEntity in list" + :key="packageEntity.id" + :package-entity="packageEntity" + :package-link="packageEntity._links.web_path" + :is-group="isGroupPage" + @packageToDelete="setItemToBeDeleted" + /> + </div> + + <gl-pagination + v-model="currentPage" + :per-page="perPage" + :total-items="totalItems" + align="center" + class="gl-w-full gl-mt-3" + /> + + <gl-modal + ref="packageListDeleteModal" + size="sm" + modal-id="confirm-delete-pacakge" + ok-variant="danger" + @ok="deleteItemConfirmation" + @cancel="deleteItemCanceled" + > + <template #modal-title>{{ modalAction }}</template> + <template #modal-ok>{{ modalAction }}</template> + <gl-sprintf :message="$options.i18n.deleteModalContent"> + <template #name> + <strong>{{ deletePackageName }}</strong> + </template> + </gl-sprintf> + </gl-modal> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue new file mode 100644 index 00000000000..462618a7f12 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue @@ -0,0 +1,119 @@ +<script> +import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; +import { mapActions, mapState } from 'vuex'; +import createFlash from '~/flash'; +import { historyReplaceState } from '~/lib/utils/common_utils'; +import { s__ } from '~/locale'; +import { + SHOW_DELETE_SUCCESS_ALERT, + FILTERED_SEARCH_TERM, +} from '~/packages_and_registries/shared/constants'; + +import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils'; +import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue'; +import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue'; +import PackageList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue'; +import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants'; + +export default { + components: { + GlEmptyState, + GlLink, + GlSprintf, + PackageList, + InfrastructureTitle, + InfrastructureSearch, + }, + inject: { + emptyPageTitle: { + from: 'emptyPageTitle', + default: s__('PackageRegistry|There are no packages yet'), + }, + noResultsText: { + from: 'noResultsText', + default: s__( + 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', + ), + }, + }, + computed: { + ...mapState({ + emptyListIllustration: (state) => state.config.emptyListIllustration, + emptyListHelpUrl: (state) => state.config.emptyListHelpUrl, + filter: (state) => state.filter, + selectedType: (state) => state.selectedType, + packageHelpUrl: (state) => state.config.packageHelpUrl, + packagesCount: (state) => state.pagination?.total, + }), + emptySearch() { + return ( + this.filter.filter((f) => f.type !== FILTERED_SEARCH_TERM || f.value?.data).length === 0 + ); + }, + + emptyStateTitle() { + return this.emptySearch + ? this.emptyPageTitle + : s__('PackageRegistry|Sorry, your filter produced no results'); + }, + }, + mounted() { + const queryParams = getQueryParams(window.document.location.search); + const { sorting, filters } = extractFilterAndSorting(queryParams); + this.setSorting(sorting); + this.setFilter(filters); + this.requestPackagesList(); + this.checkDeleteAlert(); + }, + methods: { + ...mapActions([ + 'requestPackagesList', + 'requestDeletePackage', + 'setSelectedType', + 'setSorting', + 'setFilter', + ]), + onPageChanged(page) { + return this.requestPackagesList({ page }); + }, + onPackageDeleteRequest(item) { + return this.requestDeletePackage(item); + }, + checkDeleteAlert() { + const urlParams = new URLSearchParams(window.location.search); + const showAlert = urlParams.get(SHOW_DELETE_SUCCESS_ALERT); + if (showAlert) { + // to be refactored to use gl-alert + createFlash({ message: DELETE_PACKAGE_SUCCESS_MESSAGE, type: 'notice' }); + const cleanUrl = window.location.href.split('?')[0]; + historyReplaceState(cleanUrl); + } + }, + }, + i18n: { + widenFilters: s__('PackageRegistry|To widen your search, change or remove the filters above.'), + }, +}; +</script> + +<template> + <div> + <infrastructure-title :help-url="packageHelpUrl" :count="packagesCount" /> + <infrastructure-search @update="requestPackagesList" /> + + <package-list @page:changed="onPageChanged" @package:delete="onPackageDeleteRequest"> + <template #empty-state> + <gl-empty-state :title="emptyStateTitle" :svg-path="emptyListIllustration"> + <template #description> + <gl-sprintf v-if="!emptySearch" :message="$options.i18n.widenFilters" /> + <gl-sprintf v-else :message="noResultsText"> + <template #noPackagesLink="{ content }"> + <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> + </template> + </gl-sprintf> + </template> + </gl-empty-state> + </template> + </package-list> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js new file mode 100644 index 00000000000..7af3fc1c2db --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js @@ -0,0 +1,51 @@ +import { __ } from '~/locale'; + +export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __( + 'Something went wrong while fetching the packages list.', +); +export const DELETE_PACKAGE_SUCCESS_MESSAGE = __('Package deleted successfully'); + +export const DEFAULT_PAGE = 1; +export const DEFAULT_PAGE_SIZE = 20; + +export const GROUP_PAGE_TYPE = 'groups'; + +export const LIST_KEY_NAME = 'name'; +export const LIST_KEY_PROJECT = 'project_path'; +export const LIST_KEY_VERSION = 'version'; +export const LIST_KEY_PACKAGE_TYPE = 'type'; +export const LIST_KEY_CREATED_AT = 'created_at'; + +export const LIST_LABEL_NAME = __('Name'); +export const LIST_LABEL_PROJECT = __('Project'); +export const LIST_LABEL_VERSION = __('Version'); +export const LIST_LABEL_PACKAGE_TYPE = __('Type'); +export const LIST_LABEL_CREATED_AT = __('Published'); + +// The following is not translated because it is used to build a JavaScript exception error message +export const MISSING_DELETE_PATH_ERROR = 'Missing delete_api_path link'; + +export const SORT_FIELDS = [ + { + orderBy: LIST_KEY_NAME, + label: LIST_LABEL_NAME, + }, + { + orderBy: LIST_KEY_PROJECT, + label: LIST_LABEL_PROJECT, + }, + { + orderBy: LIST_KEY_VERSION, + label: LIST_LABEL_VERSION, + }, + { + orderBy: LIST_KEY_PACKAGE_TYPE, + label: LIST_LABEL_PACKAGE_TYPE, + }, + { + orderBy: LIST_KEY_CREATED_AT, + label: LIST_LABEL_CREATED_AT, + }, +]; + +export const TERRAFORM_SEARCH_TYPE = Object.freeze({ value: { data: 'terraform_module' } }); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js new file mode 100644 index 00000000000..488860e5bc2 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js @@ -0,0 +1,83 @@ +import Api from '~/api'; +import createFlash from '~/flash'; +import axios from '~/lib/utils/axios_utils'; +import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants'; +import { + FETCH_PACKAGES_LIST_ERROR_MESSAGE, + DELETE_PACKAGE_SUCCESS_MESSAGE, + DEFAULT_PAGE, + DEFAULT_PAGE_SIZE, + MISSING_DELETE_PATH_ERROR, + TERRAFORM_SEARCH_TYPE, +} from '../constants'; +import { getNewPaginationPage } from '../utils'; +import * as types from './mutation_types'; + +export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_STATE, data); +export const setLoading = ({ commit }, data) => commit(types.SET_MAIN_LOADING, data); +export const setSorting = ({ commit }, data) => commit(types.SET_SORTING, data); +export const setFilter = ({ commit }, data) => commit(types.SET_FILTER, data); + +export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { + commit(types.SET_PACKAGE_LIST_SUCCESS, data); + commit(types.SET_PAGINATION, headers); +}; + +export const requestPackagesList = ({ dispatch, state }, params = {}) => { + dispatch('setLoading', true); + + const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params; + const { sort, orderBy } = state.sorting; + const type = state.config.forceTerraform + ? TERRAFORM_SEARCH_TYPE + : state.filter.find((f) => f.type === 'type'); + const name = state.filter.find((f) => f.type === 'filtered-search-term'); + const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data }; + + const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages'; + + return Api[apiMethod](state.config.resourceId, { + params: { page, per_page, sort, order_by: orderBy, ...packageFilters }, + }) + .then(({ data, headers }) => { + dispatch('receivePackagesListSuccess', { data, headers }); + }) + .catch(() => { + createFlash({ + message: FETCH_PACKAGES_LIST_ERROR_MESSAGE, + }); + }) + .finally(() => { + dispatch('setLoading', false); + }); +}; + +export const requestDeletePackage = ({ dispatch, state }, { _links }) => { + if (!_links || !_links.delete_api_path) { + createFlash({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); + const error = new Error(MISSING_DELETE_PATH_ERROR); + return Promise.reject(error); + } + + dispatch('setLoading', true); + return axios + .delete(_links.delete_api_path) + .then(() => { + const { page: currentPage, perPage, total } = state.pagination; + const page = getNewPaginationPage(currentPage, perPage, total - 1); + + dispatch('requestPackagesList', { page }); + createFlash({ + message: DELETE_PACKAGE_SUCCESS_MESSAGE, + type: 'success', + }); + }) + .catch(() => { + dispatch('setLoading', false); + createFlash({ + message: DELETE_PACKAGE_ERROR_MESSAGE, + }); + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js new file mode 100644 index 00000000000..5989303280e --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/getters.js @@ -0,0 +1,5 @@ +import { beautifyPath } from '~/packages_and_registries/shared/utils'; +import { LIST_KEY_PROJECT } from '../constants'; + +export default (state) => + state.packages.map((p) => ({ ...p, projectPathName: beautifyPath(p[LIST_KEY_PROJECT]) })); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js new file mode 100644 index 00000000000..1d6a4bf831d --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/index.js @@ -0,0 +1,20 @@ +import Vue from 'vue'; +import Vuex from 'vuex'; +import * as actions from './actions'; +import getList from './getters'; +import mutations from './mutations'; +import state from './state'; + +Vue.use(Vuex); + +export const createStore = () => + new Vuex.Store({ + state, + getters: { + getList, + }, + actions, + mutations, + }); + +export default createStore(); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js new file mode 100644 index 00000000000..561ad97f7e3 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutation_types.js @@ -0,0 +1,7 @@ +export const SET_INITIAL_STATE = 'SET_INITIAL_STATE'; + +export const SET_PACKAGE_LIST_SUCCESS = 'SET_PACKAGE_LIST_SUCCESS'; +export const SET_PAGINATION = 'SET_PAGINATION'; +export const SET_MAIN_LOADING = 'SET_MAIN_LOADING'; +export const SET_SORTING = 'SET_SORTING'; +export const SET_FILTER = 'SET_FILTER'; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js new file mode 100644 index 00000000000..98165e581b0 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/mutations.js @@ -0,0 +1,33 @@ +import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils'; +import { GROUP_PAGE_TYPE } from '../constants'; +import * as types from './mutation_types'; + +export default { + [types.SET_INITIAL_STATE](state, config) { + state.config = { + ...config, + isGroupPage: config.pageType === GROUP_PAGE_TYPE, + }; + }, + + [types.SET_PACKAGE_LIST_SUCCESS](state, packages) { + state.packages = packages; + }, + + [types.SET_MAIN_LOADING](state, isLoading) { + state.isLoading = isLoading; + }, + + [types.SET_PAGINATION](state, headers) { + const normalizedHeaders = normalizeHeaders(headers); + state.pagination = parseIntPagination(normalizedHeaders); + }, + + [types.SET_SORTING](state, sorting) { + state.sorting = { ...state.sorting, ...sorting }; + }, + + [types.SET_FILTER](state, filter) { + state.filter = filter; + }, +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js new file mode 100644 index 00000000000..60f02eddc9f --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/state.js @@ -0,0 +1,54 @@ +export default () => ({ + /** + * Determine if the component is loading data from the API + */ + isLoading: false, + /** + * configuration object, set once at store creation with the following structure + * { + * resourceId: String, + * pageType: String, + * emptyListIllustration: String, + * emptyListHelpUrl: String, + * comingSoon: { projectPath: String, suggestedContributions : String } | null; + * } + */ + config: {}, + /** + * Each object in `packages` has the following structure: + * { + * id: String + * name: String, + * version: String, + * package_type: String // endpoint to request the list + * } + */ + packages: [], + /** + * Pagination object has the following structure: + * { + * perPage: Number, + * page: Number + * total: Number + * } + */ + pagination: {}, + /** + * Sorting object has the following structure: + * { + * sort: String, + * orderBy: String + * } + */ + sorting: { + sort: 'desc', + orderBy: 'created_at', + }, + /** + * The search query that is used to filter packages by name + */ + filter: [], + /** + * The selected TAB of the package types tabs + */ +}); diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js new file mode 100644 index 00000000000..537b30d2ca4 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/utils.js @@ -0,0 +1,25 @@ +import { LIST_KEY_PROJECT, SORT_FIELDS } from './constants'; + +export const sortableFields = (isGroupPage) => + SORT_FIELDS.filter((f) => f.orderBy !== LIST_KEY_PROJECT || isGroupPage); + +/** + * A small util function that works out if the delete action has deleted the + * last item on the current paginated page and if so, returns the previous + * page. This ensures the user won't end up on an empty paginated page. + * + * @param {number} currentPage The current page the user is on + * @param {number} perPage Number of items to display per page + * @param {number} totalPackages The total number of items + */ +export const getNewPaginationPage = (currentPage, perPage, totalItems) => { + if (totalItems <= perPage) { + return 1; + } + + if (currentPage > 1 && (currentPage - 1) * perPage >= totalItems) { + return currentPage - 1; + } + + return currentPage; +}; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js index 7e6e98f4fb5..1467218dd41 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list_app_bundle.js @@ -1,7 +1,7 @@ import Vue from 'vue'; import { s__ } from '~/locale'; -import PackagesListApp from '~/packages/list/components/packages_list_app.vue'; -import { createStore } from '~/packages/list/stores'; +import PackagesListApp from '~/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue'; +import { createStore } from '~/packages_and_registries/infrastructure_registry/list/stores'; import Translate from '~/vue_shared/translate'; Vue.use(Translate); @@ -18,9 +18,6 @@ export default () => { PackagesListApp, }, provide: { - titleComponent: 'InfrastructureTitle', - searchComponent: 'InfrastructureSearch', - iconComponent: 'InfrastructureIconAndName', emptyPageTitle: s__('InfrastructureRegistry|You have no Terraform modules in your project'), noResultsText: s__( 'InfrastructureRegistry|Terraform modules are the main way to package and reuse resource configurations with Terraform. Learn more about how to %{noPackagesLinkStart}create Terraform modules%{noPackagesLinkEnd} in GitLab.', diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js new file mode 100644 index 00000000000..ab52ec01d40 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/constants.js @@ -0,0 +1 @@ +export const TRACK_CATEGORY = 'UI::TerraformPackages'; diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue index 3100a1a7296..3100a1a7296 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/components/infrastructure_icon_and_name.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue new file mode 100644 index 00000000000..3c6b8344c34 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue @@ -0,0 +1,161 @@ +<script> +import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; +import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { + PACKAGE_ERROR_STATUS, + PACKAGE_DEFAULT_STATUS, +} from '~/packages_and_registries/shared/constants'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; +import InfrastructureIconAndName from '~/packages_and_registries/infrastructure_registry/shared/infrastructure_icon_and_name.vue'; + +export default { + name: 'PackageListRow', + components: { + GlButton, + GlLink, + GlSprintf, + GlTruncate, + PackageTags, + PackagePath, + PublishMethod, + ListItem, + InfrastructureIconAndName, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + mixins: [timeagoMixin], + props: { + packageEntity: { + type: Object, + required: true, + }, + packageLink: { + type: String, + required: true, + }, + disableDelete: { + type: Boolean, + default: false, + required: false, + }, + isGroup: { + type: Boolean, + default: false, + required: false, + }, + showPackageType: { + type: Boolean, + default: true, + required: false, + }, + }, + computed: { + hasPipeline() { + return Boolean(this.packageEntity.pipeline); + }, + hasProjectLink() { + return Boolean(this.packageEntity.project_path); + }, + showWarningIcon() { + return this.packageEntity.status === PACKAGE_ERROR_STATUS; + }, + disabledRow() { + return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS; + }, + disabledDeleteButton() { + return this.disabledRow || !this.packageEntity._links.delete_api_path; + }, + }, + i18n: { + erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'), + }, +}; +</script> + +<template> + <list-item data-qa-selector="package_row" :disabled="disabledRow"> + <template #left-primary> + <div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"> + <gl-link + :href="packageLink" + class="gl-text-body gl-min-w-0" + data-qa-selector="package_link" + :disabled="disabledRow" + > + <gl-truncate :text="packageEntity.name" /> + </gl-link> + + <gl-button + v-if="showWarningIcon" + v-gl-tooltip="{ title: $options.i18n.erroredPackageText }" + class="gl-hover-bg-transparent!" + icon="warning" + category="tertiary" + data-testid="warning-icon" + :aria-label="__('Warning')" + /> + + <package-tags + v-if="packageEntity.tags && packageEntity.tags.length" + class="gl-ml-3" + :tags="packageEntity.tags" + hide-label + :tag-display-limit="1" + /> + </div> + </template> + <template #left-secondary> + <div class="gl-display-flex"> + <span>{{ packageEntity.version }}</span> + + <div v-if="hasPipeline" class="gl-display-none gl-sm-display-flex gl-ml-2"> + <gl-sprintf :message="s__('PackageRegistry|published by %{author}')"> + <template #author>{{ packageEntity.pipeline.user.name }}</template> + </gl-sprintf> + </div> + + <infrastructure-icon-and-name v-if="showPackageType" /> + + <package-path + v-if="hasProjectLink" + :path="packageEntity.project_path" + :disabled="disabledRow" + /> + </div> + </template> + + <template #right-primary> + <publish-method :package-entity="packageEntity" :is-group="isGroup" /> + </template> + + <template #right-secondary> + <span> + <gl-sprintf :message="__('Created %{timestamp}')"> + <template #timestamp> + <span v-gl-tooltip :title="tooltipTitle(packageEntity.created_at)"> + {{ timeFormatted(packageEntity.created_at) }} + </span> + </template> + </gl-sprintf> + </span> + </template> + + <template v-if="!disableDelete" #right-action> + <gl-button + data-testid="action-delete" + icon="remove" + category="secondary" + variant="danger" + :title="s__('PackageRegistry|Remove package')" + :aria-label="s__('PackageRegistry|Remove package')" + :disabled="disabledDeleteButton" + @click="$emit('packageToDelete', packageEntity)" + /> + </template> + </list-item> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue index bcbeec72961..d49c1be5202 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue @@ -15,7 +15,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { objectToQuery } from '~/lib/utils/url_utility'; import { s__, __ } from '~/locale'; -import { packageTypeToTrackCategory } from '~/packages/shared/utils'; +import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; import AdditionalMetadata from '~/packages_and_registries/package_registry/components/details/additional_metadata.vue'; import DependencyRow from '~/packages_and_registries/package_registry/components/details/dependency_row.vue'; import InstallationCommands from '~/packages_and_registries/package_registry/components/details/installation_commands.vue'; @@ -304,6 +304,7 @@ export default { <template #default="{ deletePackage }"> <gl-modal ref="deleteModal" + size="sm" modal-id="delete-modal" data-testid="delete-modal" :action-primary="$options.modal.packageDeletePrimaryAction" @@ -327,6 +328,7 @@ export default { <gl-modal ref="deleteFileModal" + size="sm" modal-id="delete-file-modal" :action-primary="$options.modal.fileDeletePrimaryAction" :action-cancel="$options.modal.cancelAction" diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue index 44d7807639d..118c509828c 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_title.vue @@ -3,7 +3,7 @@ import { GlIcon, GlSprintf, GlBadge, GlResizeObserverDirective } from '@gitlab/u import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { __ } from '~/locale'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; import { PACKAGE_TYPE_NUGET } from '~/packages_and_registries/package_registry/constants'; import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue index d218a405af6..1afd1b69db0 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/version_row.vue @@ -1,8 +1,8 @@ <script> import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; -import PublishMethod from '~/packages/shared/components/publish_method.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; +import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { PACKAGE_DEFAULT_STATUS } from '../../constants'; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue index 195ff7af583..6fd96c0654f 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue @@ -1,16 +1,16 @@ <script> import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { s__, __ } from '~/locale'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { PACKAGE_ERROR_STATUS, PACKAGE_DEFAULT_STATUS, } from '~/packages_and_registries/package_registry/constants'; -import { getPackageTypeLabel } from '~/packages/shared/utils'; -import PackagePath from '~/packages/shared/components/package_path.vue'; -import PackageTags from '~/packages/shared/components/package_tags.vue'; +import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/utils'; +import PackagePath from '~/packages_and_registries/shared/components/package_path.vue'; +import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue'; import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue'; -import PackageIconAndName from '~/packages/shared/components/package_icon_and_name.vue'; +import PackageIconAndName from '~/packages_and_registries/shared/components/package_icon_and_name.vue'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; @@ -40,7 +40,7 @@ export default { }, computed: { packageType() { - return getPackageTypeLabel(this.packageEntity.packageType.toLowerCase()); + return getPackageTypeLabel(this.packageEntity.packageType); }, packageLink() { const { project, id } = this.packageEntity; @@ -64,6 +64,7 @@ export default { }, i18n: { erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'), + createdAt: __('Created %{timestamp}'), }, }; </script> @@ -127,8 +128,8 @@ export default { </template> <template #right-secondary> - <span> - <gl-sprintf :message="__('Created %{timestamp}')"> + <span data-testid="created-date"> + <gl-sprintf :message="$options.i18n.createdAt"> <template #timestamp> <timeago-tooltip :time="packageEntity.createdAt" /> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue index 2a946544c2f..298ed9bccdb 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue @@ -2,7 +2,7 @@ import { GlModal, GlSprintf, GlKeysetPagination } from '@gitlab/ui'; import { s__ } from '~/locale'; import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; -import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue'; +import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import { DELETE_PACKAGE_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_TRACKING_ACTION, @@ -124,6 +124,7 @@ export default { <gl-modal v-model="showDeleteModal" modal-id="confirm-delete-pacakge" + size="sm" ok-variant="danger" @ok="deleteItemConfirmation" @cancel="deleteItemCanceled" diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js index 9fd8880861c..ab6541e4264 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -1,4 +1,15 @@ import { s__, __ } from '~/locale'; +import { helpPagePath } from '~/helpers/help_page_helper'; + +export { + DELETE_PACKAGE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_TRACKING_ACTION, + PULL_PACKAGE_TRACKING_ACTION, + DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, +} from '~/packages_and_registries/shared/constants'; export const PACKAGE_TYPE_CONAN = 'CONAN'; export const PACKAGE_TYPE_MAVEN = 'MAVEN'; @@ -11,14 +22,6 @@ export const PACKAGE_TYPE_GENERIC = 'GENERIC'; export const PACKAGE_TYPE_DEBIAN = 'DEBIAN'; export const PACKAGE_TYPE_HELM = 'HELM'; -export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package'; -export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package'; -export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package'; -export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package'; -export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file'; -export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file'; -export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file'; - export const TRACKING_LABEL_CODE_INSTRUCTION = 'code_instruction'; export const TRACKING_LABEL_CONAN_INSTALLATION = 'conan_installation'; export const TRACKING_LABEL_MAVEN_INSTALLATION = 'maven_installation'; @@ -134,3 +137,8 @@ export const PACKAGE_TYPES = [ s__('PackageRegistry|Debian'), s__('PackageRegistry|Helm'), ]; + +// links + +export const EMPTY_LIST_HELP_URL = helpPagePath('user/packages/package_registry/index'); +export const PACKAGE_HELP_URL = helpPagePath('user/packages/index'); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql index aaf0eb54aff..66315fda9e9 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql @@ -7,20 +7,24 @@ fragment PackageData on Package { status tags { nodes { + id name } } - pipelines { + pipelines(last: 1) { nodes { + id sha ref commitPath user { + id name } } } project { + id fullPath webUrl } diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql index 14aa14e9822..08ea0938a59 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql @@ -8,6 +8,7 @@ query getPackageDetails($id: ID!) { updatedAt status project { + id path } tags(first: 10) { @@ -25,9 +26,11 @@ query getPackageDetails($id: ID!) { commitPath path user { + id name } project { + id name webUrl } @@ -86,15 +89,18 @@ query getPackageDetails($id: ID!) { } } ... on PypiMetadata { + id requiredPython } ... on ConanMetadata { + id packageChannel packageUsername recipe recipePath } ... on MavenMetadata { + id appName appGroup appVersion @@ -102,6 +108,7 @@ query getPackageDetails($id: ID!) { } ... on NugetMetadata { + id iconUrl licenseUrl projectUrl diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql index e3115365f8b..4b913590949 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql @@ -14,6 +14,7 @@ query getPackages( $before: String ) { project(fullPath: $fullPath) @skip(if: $isGroupPage) { + id packages( sort: $sort packageName: $packageName @@ -33,6 +34,7 @@ query getPackages( } } group(fullPath: $fullPath) @include(if: $isGroupPage) { + id packages( sort: $groupSort packageName: $packageName diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js new file mode 100644 index 00000000000..7ec931ff9a0 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js @@ -0,0 +1,30 @@ +import Vue from 'vue'; +import Translate from '~/vue_shared/translate'; +import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index'; +import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue'; +import createRouter from './router'; + +Vue.use(Translate); + +export default () => { + const el = document.getElementById('js-vue-packages-list'); + const { endpoint, resourceId, fullPath, pageType, emptyListIllustration } = el.dataset; + const router = createRouter(endpoint); + + const isGroupPage = pageType === 'groups'; + + return new Vue({ + el, + router, + apolloProvider, + provide: { + resourceId, + fullPath, + emptyListIllustration, + isGroupPage, + }, + render(createElement) { + return createElement(PackageRegistry); + }, + }); +}; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue new file mode 100644 index 00000000000..a14d0c32cbe --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/index.vue @@ -0,0 +1,5 @@ +<template> + <div> + <router-view /> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js deleted file mode 100644 index d797a0a5327..00000000000 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.js +++ /dev/null @@ -1,24 +0,0 @@ -import Vue from 'vue'; -import Translate from '~/vue_shared/translate'; -import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index'; -import PackagesListApp from '../components/list/app.vue'; - -Vue.use(Translate); - -export default () => { - const el = document.getElementById('js-vue-packages-list'); - - const isGroupPage = el.dataset.pageType === 'groups'; - - return new Vue({ - el, - apolloProvider, - provide: { - ...el.dataset, - isGroupPage, - }, - render(createElement) { - return createElement(PackagesListApp); - }, - }); -}; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue index 11eeaf933ff..38df701157a 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/app.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue @@ -3,19 +3,21 @@ import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import createFlash from '~/flash'; import { historyReplaceState } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; -import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants'; +import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants'; import { PROJECT_RESOURCE_TYPE, GROUP_RESOURCE_TYPE, GRAPHQL_PAGE_SIZE, DELETE_PACKAGE_SUCCESS_MESSAGE, + EMPTY_LIST_HELP_URL, + PACKAGE_HELP_URL, } from '~/packages_and_registries/package_registry/constants'; import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql'; import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue'; -import PackageTitle from './package_title.vue'; -import PackageSearch from './package_search.vue'; -import PackageList from './packages_list.vue'; +import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue'; +import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue'; +import PackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue'; export default { components: { @@ -27,13 +29,7 @@ export default { PackageSearch, DeletePackage, }, - inject: [ - 'packageHelpUrl', - 'emptyListIllustration', - 'emptyListHelpUrl', - 'isGroupPage', - 'fullPath', - ], + inject: ['emptyListIllustration', 'isGroupPage', 'fullPath'], data() { return { packages: {}, @@ -156,12 +152,16 @@ export default { 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', ), }, + links: { + EMPTY_LIST_HELP_URL, + PACKAGE_HELP_URL, + }, }; </script> <template> <div> - <package-title :help-url="packageHelpUrl" :count="packagesCount" /> + <package-title :help-url="$options.links.PACKAGE_HELP_URL" :count="packagesCount" /> <package-search @update="handleSearchUpdate" /> <delete-package @@ -185,7 +185,9 @@ export default { <gl-sprintf v-if="hasFilters" :message="$options.i18n.widenFilters" /> <gl-sprintf v-else :message="$options.i18n.noResultsText"> <template #noPackagesLink="{ content }"> - <gl-link :href="emptyListHelpUrl" target="_blank">{{ content }}</gl-link> + <gl-link :href="$options.links.EMPTY_LIST_HELP_URL" target="_blank">{{ + content + }}</gl-link> </template> </gl-sprintf> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/router.js b/app/assets/javascripts/packages_and_registries/package_registry/router.js new file mode 100644 index 00000000000..ea5b740e879 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/router.js @@ -0,0 +1,21 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import List from '~/packages_and_registries/package_registry/pages/list.vue'; + +Vue.use(VueRouter); + +export default function createRouter(base) { + const router = new VueRouter({ + base, + mode: 'history', + routes: [ + { + name: 'list', + path: '/', + component: List, + }, + ], + }); + + return router; +} diff --git a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js index 9b5a0d221b8..85a7aeb5561 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/bundle.js @@ -18,9 +18,10 @@ export default () => { el, apolloProvider, provide: { + groupPath: el.dataset.groupPath, + groupDependencyProxyPath: el.dataset.groupDependencyProxyPath, defaultExpanded: parseBoolean(el.dataset.defaultExpanded), dependencyProxyAvailable: parseBoolean(el.dataset.dependencyProxyAvailable), - groupPath: el.dataset.groupPath, }, render(createElement) { return createElement(SettingsApp); diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue index 5815c6393a7..fd62fe144b2 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue @@ -2,9 +2,14 @@ import { GlToggle, GlSprintf, GlLink } from '@gitlab/ui'; import { s__ } from '~/locale'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; +import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue'; import updateDependencyProxySettings from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_settings.mutation.graphql'; +import updateDependencyProxyImageTtlGroupPolicy from '~/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql'; import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update'; -import { updateGroupDependencyProxySettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; +import { + updateGroupDependencyProxySettingsOptimisticResponse, + updateDependencyProxyImageTtlGroupPolicyOptimisticResponse, +} from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses'; import { DEPENDENCY_PROXY_HEADER, @@ -19,21 +24,34 @@ export default { GlSprintf, GlLink, SettingsBlock, + SettingsTitles, }, i18n: { DEPENDENCY_PROXY_HEADER, DEPENDENCY_PROXY_SETTINGS_DESCRIPTION, - label: s__('DependencyProxy|Enable Proxy'), + enabledProxyLabel: s__('DependencyProxy|Enable Dependency Proxy'), + enabledProxyHelpText: s__( + 'DependencyProxy|To see the image prefix and what is in the cache, visit the %{linkStart}Dependency Proxy%{linkEnd}', + ), + storageSettingsTitle: s__('DependencyProxy|Storage settings'), + ttlPolicyEnabledLabel: s__('DependencyProxy|Clear the Dependency Proxy cache automatically'), + ttlPolicyEnabledHelpText: s__( + 'DependencyProxy|When enabled, images older than 90 days will be removed from the cache.', + ), }, links: { DEPENDENCY_PROXY_DOCS_PATH, }, - inject: ['defaultExpanded', 'groupPath'], + inject: ['defaultExpanded', 'groupPath', 'groupDependencyProxyPath'], props: { dependencyProxySettings: { type: Object, required: true, }, + dependencyProxyImageTtlPolicy: { + type: Object, + required: true, + }, isLoading: { type: Boolean, required: false, @@ -49,26 +67,35 @@ export default { this.updateSettings({ enabled }); }, }, + ttlEnabled: { + get() { + return this.dependencyProxyImageTtlPolicy.enabled; + }, + set(enabled) { + const payload = { + enabled, + ttl: 90, // hardocded TTL for the MVC version + }; + this.updateDependencyProxyImageTtlGroupPolicy(payload); + }, + }, + helpText() { + return this.enabled ? this.$options.i18n.enabledProxyHelpText : ''; + }, }, methods: { - async updateSettings(payload) { + mutationVariables(payload) { + return { + input: { + groupPath: this.groupPath, + ...payload, + }, + }; + }, + async executeMutation(config, resource) { try { - const { data } = await this.$apollo.mutate({ - mutation: updateDependencyProxySettings, - variables: { - input: { - groupPath: this.groupPath, - ...payload, - }, - }, - update: updateGroupPackageSettings(this.groupPath), - optimisticResponse: updateGroupDependencyProxySettingsOptimisticResponse({ - ...this.dependencyProxySettings, - ...payload, - }), - }); - - if (data.updateDependencyProxySettings?.errors?.length > 0) { + const { data } = await this.$apollo.mutate(config); + if (data[resource]?.errors.length > 0) { throw new Error(); } else { this.$emit('success'); @@ -77,6 +104,32 @@ export default { this.$emit('error'); } }, + async updateSettings(payload) { + const apolloConfig = { + mutation: updateDependencyProxySettings, + variables: this.mutationVariables(payload), + update: updateGroupPackageSettings(this.groupPath), + optimisticResponse: updateGroupDependencyProxySettingsOptimisticResponse({ + ...this.dependencyProxySettings, + ...payload, + }), + }; + + this.executeMutation(apolloConfig, 'updateDependencyProxySettings'); + }, + async updateDependencyProxyImageTtlGroupPolicy(payload) { + const apolloConfig = { + mutation: updateDependencyProxyImageTtlGroupPolicy, + variables: this.mutationVariables(payload), + update: updateGroupPackageSettings(this.groupPath), + optimisticResponse: updateDependencyProxyImageTtlGroupPolicyOptimisticResponse({ + ...this.dependencyProxyImageTtlPolicy, + ...payload, + }), + }; + + this.executeMutation(apolloConfig, 'updateDependencyProxyImageTtlGroupPolicy'); + }, }, }; </script> @@ -91,7 +144,11 @@ export default { <span data-testid="description"> <gl-sprintf :message="$options.i18n.DEPENDENCY_PROXY_SETTINGS_DESCRIPTION"> <template #docLink="{ content }"> - <gl-link :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH">{{ content }}</gl-link> + <gl-link + data-testid="description-link" + :href="$options.links.DEPENDENCY_PROXY_DOCS_PATH" + >{{ content }}</gl-link + > </template> </gl-sprintf> </span> @@ -101,9 +158,31 @@ export default { <gl-toggle v-model="enabled" :disabled="isLoading" - :label="$options.i18n.label" + :label="$options.i18n.enabledProxyLabel" + :help="helpText" data-qa-selector="dependency_proxy_setting_toggle" data-testid="dependency-proxy-setting-toggle" + > + <template #help> + <span class="gl-overflow-break-word gl-max-w-100vw gl-display-inline-block"> + <gl-sprintf :message="$options.i18n.enabledProxyHelpText"> + <template #link="{ content }"> + <gl-link data-testid="toggle-help-link" :href="groupDependencyProxyPath">{{ + content + }}</gl-link> + </template> + </gl-sprintf> + </span> + </template> + </gl-toggle> + + <settings-titles :title="$options.i18n.storageSettingsTitle" class="gl-my-6" /> + <gl-toggle + v-model="ttlEnabled" + :disabled="isLoading" + :label="$options.i18n.ttlPolicyEnabledLabel" + :help="$options.i18n.ttlPolicyEnabledHelpText" + data-testid="dependency-proxy-ttl-policies-toggle" /> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue index b45cedcdd66..64c12b4be6a 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue @@ -37,6 +37,9 @@ export default { dependencyProxySettings() { return this.group?.dependencyProxySetting || {}; }, + dependencyProxyImageTtlPolicy() { + return this.group?.dependencyProxyImageTtlPolicy || {}; + }, isLoading() { return this.$apollo.queries.group.loading; }, @@ -82,6 +85,7 @@ export default { <dependency-proxy-settings v-if="dependencyProxyAvailable" :dependency-proxy-settings="dependencyProxySettings" + :dependency-proxy-image-ttl-policy="dependencyProxyImageTtlPolicy" :is-loading="isLoading" @success="handleSuccess" @error="handleError" diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue index 3f0ab7686e5..1e93875c1e3 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue @@ -8,7 +8,8 @@ export default { }, subTitle: { type: String, - required: true, + required: false, + default: '', }, }, }; @@ -16,10 +17,10 @@ export default { <template> <div> - <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200"> + <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200 gl-pb-3"> {{ title }} </h5> - <p>{{ subTitle }}</p> + <p v-if="subTitle">{{ subTitle }}</p> <slot></slot> </div> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql new file mode 100644 index 00000000000..81250f52dfb --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/mutations/update_dependency_proxy_image_ttl_group_policy.mutation.graphql @@ -0,0 +1,11 @@ +mutation updateDependencyProxyImageTtlGroupPolicy( + $input: UpdateDependencyProxyImageTtlGroupPolicyInput! +) { + updateDependencyProxyImageTtlGroupPolicy(input: $input) { + dependencyProxyImageTtlPolicy { + enabled + ttl + } + errors + } +} diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql index d3edebfbe20..404d9d26d49 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql @@ -1,8 +1,13 @@ query getGroupPackagesSettings($fullPath: ID!) { group(fullPath: $fullPath) { + id dependencyProxySetting { enabled } + dependencyProxyImageTtlPolicy { + ttl + enabled + } packageSettings { mavenDuplicatesAllowed mavenDuplicateExceptionRegex diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js index fe94203f51b..c7b0899fa4c 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/cache_update.js @@ -19,6 +19,11 @@ export const updateGroupPackageSettings = (fullPath) => (client, { data: updated ...updatedData.updateDependencyProxySettings.dependencyProxySetting, }; } + if (updatedData.updateDependencyProxyImageTtlGroupPolicy) { + draftState.group.dependencyProxyImageTtlPolicy = { + ...updatedData.updateDependencyProxyImageTtlGroupPolicy.dependencyProxyImageTtlPolicy, + }; + } }); client.writeQuery({ diff --git a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js index a30d8ca0b81..92f6e117911 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/graphql/utils/optimistic_responses.js @@ -21,3 +21,15 @@ export const updateGroupDependencyProxySettingsOptimisticResponse = (changes) => }, }, }); + +export const updateDependencyProxyImageTtlGroupPolicyOptimisticResponse = (changes) => ({ + // eslint-disable-next-line @gitlab/require-i18n-strings + __typename: 'Mutation', + updateDependencyProxyImageTtlGroupPolicy: { + __typename: 'UpdateDependencyProxyImageTtlGroupPolicyPayload', + errors: [], + dependencyProxyImageTtlPolicy: { + ...changes, + }, + }, +}); diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql index c171be0ad07..6a862da92df 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql +++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql @@ -2,6 +2,7 @@ query getProjectExpirationPolicy($projectPath: ID!) { project(fullPath: $projectPath) { + id containerExpirationPolicy { ...ContainerExpirationPolicyFields } diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue new file mode 100644 index 00000000000..105f7bbe132 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue @@ -0,0 +1,17 @@ +<script> +import { GlIcon } from '@gitlab/ui'; + +export default { + name: 'PackageIconAndName', + components: { + GlIcon, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <gl-icon name="package" class="gl-ml-3 gl-mr-2" /> + <span><slot></slot></span> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue new file mode 100644 index 00000000000..6fb001e5e92 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue @@ -0,0 +1,86 @@ +<script> +import { GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui'; +import { joinPaths } from '~/lib/utils/url_utility'; + +export default { + name: 'PackagePath', + components: { + GlIcon, + GlLink, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + path: { + type: String, + required: true, + }, + disabled: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + pathPieces() { + return this.path.split('/'); + }, + root() { + // we skip the first part of the path since is the 'base' group + return this.pathPieces[1]; + }, + rootLink() { + return joinPaths(this.pathPieces[0], this.root); + }, + leaf() { + return this.pathPieces[this.pathPieces.length - 1]; + }, + deeplyNested() { + return this.pathPieces.length > 3; + }, + hasGroup() { + return this.root !== this.leaf; + }, + }, +}; +</script> + +<template> + <div data-qa-selector="package-path" class="gl-display-flex gl-align-items-center"> + <gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" /> + + <gl-link + data-testid="root-link" + class="gl-text-gray-500 gl-min-w-0" + :href="`/${rootLink}`" + :disabled="disabled" + > + {{ root }} + </gl-link> + + <template v-if="hasGroup"> + <gl-icon data-testid="root-chevron" name="chevron-right" class="gl-mx-2 gl-min-w-0" /> + + <template v-if="deeplyNested"> + <span + v-gl-tooltip="{ title: path }" + data-testid="ellipsis-icon" + class="gl-inset-border-1-gray-200 gl-rounded-base gl-px-2 gl-min-w-0" + > + <gl-icon name="ellipsis_h" /> + </span> + <gl-icon data-testid="ellipsis-chevron" name="chevron-right" class="gl-mx-2 gl-min-w-0" /> + </template> + + <gl-link + data-testid="leaf-link" + class="gl-text-gray-500 gl-min-w-0" + :href="`/${path}`" + :disabled="disabled" + > + {{ leaf }} + </gl-link> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue new file mode 100644 index 00000000000..5ec950e4d45 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/package_tags.vue @@ -0,0 +1,110 @@ +<script> +import { GlBadge, GlIcon, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { n__ } from '~/locale'; + +export default { + name: 'PackageTags', + components: { + GlBadge, + GlIcon, + GlSprintf, + }, + directives: { + GlTooltip: GlTooltipDirective, + }, + props: { + tagDisplayLimit: { + type: Number, + required: false, + default: 2, + }, + tags: { + type: Array, + required: true, + default: () => [], + }, + hideLabel: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + tagCount() { + return this.tags.length; + }, + tagsToRender() { + return this.tags.slice(0, this.tagDisplayLimit); + }, + moreTagsDisplay() { + return Math.max(0, this.tags.length - this.tagDisplayLimit); + }, + moreTagsTooltip() { + if (this.moreTagsDisplay) { + return this.tags + .slice(this.tagDisplayLimit) + .map((x) => x.name) + .join(', '); + } + + return ''; + }, + tagsDisplay() { + return n__('%d tag', '%d tags', this.tagCount); + }, + }, + methods: { + tagBadgeClass(index) { + return { + 'gl-display-none': true, + 'gl-display-flex': this.tagCount === 1, + 'd-md-flex': this.tagCount > 1, + 'gl-mr-2': index !== this.tagsToRender.length - 1, + 'gl-ml-3': !this.hideLabel && index === 0, + }; + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <div v-if="!hideLabel" data-testid="tagLabel" class="gl-display-flex gl-align-items-center"> + <gl-icon name="labels" class="gl-text-gray-500 gl-mr-3" /> + <span class="gl-font-weight-bold">{{ tagsDisplay }}</span> + </div> + + <gl-badge + v-for="(tag, index) in tagsToRender" + :key="index" + data-testid="tagBadge" + :class="tagBadgeClass(index)" + variant="info" + size="sm" + >{{ tag.name }}</gl-badge + > + + <gl-badge + v-if="moreTagsDisplay" + v-gl-tooltip + data-testid="moreBadge" + variant="muted" + :title="moreTagsTooltip" + size="sm" + class="gl-display-none gl-md-display-flex gl-ml-2" + ><gl-sprintf :message="__('+%{tags} more')"> + <template #tags> + {{ moreTagsDisplay }} + </template> + </gl-sprintf></gl-badge + > + + <gl-badge + v-if="moreTagsDisplay && hideLabel" + data-testid="moreBadge" + variant="muted" + class="gl-md-display-none gl-ml-2" + >{{ tagsDisplay }}</gl-badge + > + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue b/app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue new file mode 100644 index 00000000000..cf555f46f8c --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/packages_list_loader.vue @@ -0,0 +1,60 @@ +<script> +import { GlSkeletonLoader } from '@gitlab/ui'; + +export default { + components: { + GlSkeletonLoader, + }, + shapes: [ + { type: 'rect', width: '220', height: '10', x: '0', y: '20' }, + { type: 'rect', width: '60', height: '10', x: '305', y: '20' }, + { type: 'rect', width: '60', height: '10', x: '535', y: '20' }, + { type: 'rect', width: '100', height: '10', x: '760', y: '20' }, + { type: 'rect', width: '30', height: '30', x: '970', y: '10', ref: 'button-loader' }, + ], + rowsToRender: { + mobile: 5, + desktop: 20, + }, +}; +</script> + +<template> + <div> + <div class="gl-flex-direction-column gl-sm-display-none" data-testid="mobile-loader"> + <gl-skeleton-loader + v-for="index in $options.rowsToRender.mobile" + :key="index" + :width="500" + :height="170" + preserve-aspect-ratio="xMinYMax meet" + > + <rect width="500" height="10" x="0" y="15" rx="4" /> + <rect width="500" height="10" x="0" y="45" rx="4" /> + <rect width="500" height="10" x="0" y="75" rx="4" /> + <rect width="500" height="10" x="0" y="105" rx="4" /> + <rect width="500" height="10" x="0" y="135" rx="4" /> + </gl-skeleton-loader> + </div> + <div + class="gl-display-none gl-sm-display-flex gl-flex-direction-column" + data-testid="desktop-loader" + > + <gl-skeleton-loader + v-for="index in $options.rowsToRender.desktop" + :key="index" + :width="1000" + :height="54" + preserve-aspect-ratio="xMinYMax meet" + > + <component + :is="r.type" + v-for="(r, rIndex) in $options.shapes" + :key="rIndex" + rx="4" + v-bind="r" + /> + </gl-skeleton-loader> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue b/app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue new file mode 100644 index 00000000000..8a66a33f2ab --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/publish_method.vue @@ -0,0 +1,64 @@ +<script> +import { GlIcon, GlLink } from '@gitlab/ui'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import { getCommitLink } from '../utils'; + +export default { + name: 'PublishMethod', + components: { + ClipboardButton, + GlIcon, + GlLink, + }, + props: { + packageEntity: { + type: Object, + required: true, + }, + isGroup: { + type: Boolean, + required: false, + default: false, + }, + }, + computed: { + hasPipeline() { + return Boolean(this.packageEntity.pipeline); + }, + packageShaShort() { + return this.packageEntity.pipeline?.sha.substring(0, 8); + }, + linkToCommit() { + return getCommitLink(this.packageEntity, this.isGroup); + }, + }, +}; +</script> + +<template> + <div class="gl-display-flex gl-align-items-center"> + <template v-if="hasPipeline"> + <gl-icon name="git-merge" class="gl-mr-2" /> + <span data-testid="pipeline-ref" class="gl-mr-2">{{ packageEntity.pipeline.ref }}</span> + + <gl-icon name="commit" class="gl-mr-2" /> + <gl-link data-testid="pipeline-sha" :href="linkToCommit" class="gl-mr-2">{{ + packageShaShort + }}</gl-link> + + <clipboard-button + :text="packageEntity.pipeline.sha" + :title="__('Copy commit SHA')" + category="tertiary" + size="small" + /> + </template> + + <template v-else> + <gl-icon name="upload" class="gl-mr-2" /> + <span data-testid="manually-published"> + {{ s__('PackageRegistry|Manually Published') }} + </span> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue new file mode 100644 index 00000000000..79381f82009 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue @@ -0,0 +1,124 @@ +<script> +import { GlButton, GlFormCheckbox, GlKeysetPagination } from '@gitlab/ui'; +import { filter } from 'lodash'; +import { __ } from '~/locale'; + +export default { + name: 'RegistryList', + components: { + GlButton, + GlFormCheckbox, + GlKeysetPagination, + }, + props: { + title: { + type: String, + required: true, + }, + isLoading: { + type: Boolean, + default: false, + required: false, + }, + hiddenDelete: { + type: Boolean, + default: false, + required: false, + }, + pagination: { + type: Object, + required: false, + default: () => ({}), + }, + items: { + type: Array, + required: false, + default: () => [], + }, + idProperty: { + type: String, + required: false, + default: 'id', + }, + }, + data() { + return { + selectedReferences: {}, + }; + }, + computed: { + showPagination() { + return this.pagination.hasPreviousPage || this.pagination.hasNextPage; + }, + disableDeleteButton() { + return this.isLoading || filter(this.selectedReferences).length === 0; + }, + selectedItems() { + return this.items.filter(this.isSelected); + }, + selectAll: { + get() { + return this.items.every(this.isSelected); + }, + set(value) { + this.items.forEach((item) => { + const id = item[this.idProperty]; + this.$set(this.selectedReferences, id, value); + }); + }, + }, + }, + methods: { + selectItem(item) { + const id = item[this.idProperty]; + this.$set(this.selectedReferences, id, !this.selectedReferences[id]); + }, + isSelected(item) { + const id = item[this.idProperty]; + return this.selectedReferences[id]; + }, + }, + i18n: { + deleteSelected: __('Delete Selected'), + }, +}; +</script> + +<template> + <div> + <div class="gl-display-flex gl-justify-content-space-between gl-mb-3 gl-align-items-center"> + <gl-form-checkbox v-if="!hiddenDelete" v-model="selectAll" class="gl-ml-2 gl-pt-2"> + <span class="gl-font-weight-bold">{{ title }}</span> + </gl-form-checkbox> + + <gl-button + v-if="!hiddenDelete" + :disabled="disableDeleteButton" + category="secondary" + variant="danger" + @click="$emit('delete', selectedItems)" + > + {{ $options.i18n.deleteSelected }} + </gl-button> + </div> + + <div v-for="(item, index) in items" :key="index"> + <slot + :select-item="selectItem" + :is-selected="isSelected" + :item="item" + :first="index === 0" + ></slot> + </div> + + <div class="gl-display-flex gl-justify-content-center"> + <gl-keyset-pagination + v-if="showPagination" + v-bind="pagination" + class="gl-mt-3" + @prev="$emit('prev-page')" + @next="$emit('next-page')" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/shared/constants.js b/app/assets/javascripts/packages_and_registries/shared/constants.js index 7d2971bd8c7..afc72a2c627 100644 --- a/app/assets/javascripts/packages_and_registries/shared/constants.js +++ b/app/assets/javascripts/packages_and_registries/shared/constants.js @@ -1,3 +1,39 @@ +import { s__ } from '~/locale'; + export const FILTERED_SEARCH_TERM = 'filtered-search-term'; export const FILTERED_SEARCH_TYPE = 'type'; export const HISTORY_PIPELINES_LIMIT = 5; + +export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package'; +export const REQUEST_DELETE_PACKAGE_TRACKING_ACTION = 'request_delete_package'; +export const CANCEL_DELETE_PACKAGE_TRACKING_ACTION = 'cancel_delete_package'; +export const PULL_PACKAGE_TRACKING_ACTION = 'pull_package'; +export const DELETE_PACKAGE_FILE_TRACKING_ACTION = 'delete_package_file'; +export const REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'request_delete_package_file'; +export const CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION = 'cancel_delete_package_file'; + +export const TRACKING_ACTIONS = { + DELETE_PACKAGE: DELETE_PACKAGE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE: REQUEST_DELETE_PACKAGE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE: CANCEL_DELETE_PACKAGE_TRACKING_ACTION, + PULL_PACKAGE: PULL_PACKAGE_TRACKING_ACTION, + DELETE_PACKAGE_FILE: DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_DELETE_PACKAGE_FILE: REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + CANCEL_DELETE_PACKAGE_FILE: CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, +}; + +export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert'; +export const DELETE_PACKAGE_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while deleting the package.', +); +export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__( + 'PackageRegistry|Something went wrong while deleting the package file.', +); +export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__( + 'PackageRegistry|Package file deleted successfully', +); + +export const PACKAGE_ERROR_STATUS = 'error'; +export const PACKAGE_DEFAULT_STATUS = 'default'; +export const PACKAGE_HIDDEN_STATUS = 'hidden'; +export const PACKAGE_PROCESSING_STATUS = 'processing'; diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js index 93eb90535d1..cf18f655e79 100644 --- a/app/assets/javascripts/packages_and_registries/shared/utils.js +++ b/app/assets/javascripts/packages_and_registries/shared/utils.js @@ -28,3 +28,13 @@ export const extractFilterAndSorting = (queryObject) => { } return { filters, sorting }; }; + +export const beautifyPath = (path) => (path ? path.split('/').join(' / ') : ''); + +export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGroup = false) => { + if (isGroup) { + return `/${projectPath}/commit/${pipeline.sha}`; + } + + return `../commit/${pipeline.sha}`; +}; |