diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2023-05-17 19:05:49 +0300 |
commit | 43a25d93ebdabea52f99b05e15b06250cd8f07d7 (patch) | |
tree | dceebdc68925362117480a5d672bcff122fb625b /app/assets/javascripts/packages_and_registries | |
parent | 20c84b99005abd1c82101dfeff264ac50d2df211 (diff) |
Add latest changes from gitlab-org/gitlab@16-0-stable-eev16.0.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
60 files changed, 813 insertions, 460 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/delete_modal.vue index 2da8ca2d8a8..7922ff9cce3 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/delete_modal.vue @@ -1,12 +1,13 @@ <script> import { GlModal, GlSprintf, GlFormInput } from '@gitlab/ui'; -import { n__ } from '~/locale'; +import { __, n__ } from '~/locale'; import { REMOVE_TAG_CONFIRMATION_TEXT, REMOVE_TAGS_CONFIRMATION_TEXT, DELETE_IMAGE_CONFIRMATION_TITLE, DELETE_IMAGE_CONFIRMATION_TEXT, -} from '../../constants'; +} from '../constants'; +import { getImageName } from '../utils'; export default { components: { @@ -28,12 +29,13 @@ export default { }, data() { return { - projectPath: '', + inputImageName: '', }; }, computed: { - imageProjectPath() { - return this.itemsToBeDeleted[0]?.project?.path; + imageName() { + const [item] = this.itemsToBeDeleted; + return getImageName(item); }, modalTitle() { if (this.deleteImage) { @@ -49,7 +51,7 @@ export default { if (this.deleteImage) { return { message: DELETE_IMAGE_CONFIRMATION_TEXT, - item: this.imageProjectPath, + item: this.imageName, }; } if (this.itemsToBeDeleted.length > 1) { @@ -66,7 +68,13 @@ export default { }; }, disablePrimaryButton() { - return this.deleteImage && this.projectPath !== this.imageProjectPath; + return this.deleteImage && this.inputImageName !== this.imageName; + }, + primaryActionProps() { + return { + text: __('Delete'), + attributes: { variant: 'danger', disabled: this.disablePrimaryButton }, + }; }, }, methods: { @@ -74,25 +82,25 @@ export default { this.$refs.deleteModal.show(); }, }, + modal: { + cancelAction: { + text: __('Cancel'), + }, + }, }; </script> <template> <gl-modal ref="deleteModal" - modal-id="delete-tag-modal" + modal-id="delete-modal" ok-variant="danger" size="sm" - :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { - text: __('Delete'), - attributes: [{ variant: 'danger' }, { disabled: disablePrimaryButton }], - } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" - :action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { - text: __('Cancel'), - } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" + :action-primary="primaryActionProps" + :action-cancel="$options.modal.cancelAction" @primary="$emit('confirmDelete')" @cancel="$emit('cancelDelete')" - @change="projectPath = ''" + @change="inputImageName = ''" > <template #modal-title>{{ modalTitle }}</template> <p v-if="modalDescription" data-testid="description"> @@ -106,7 +114,7 @@ export default { </gl-sprintf> </p> <div v-if="deleteImage"> - <gl-form-input v-model="projectPath" /> + <gl-form-input v-model="inputImageName" /> </div> </gl-modal> </template> 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 5d77ff9dc0d..da88f768c03 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 @@ -4,9 +4,10 @@ import { sprintf, n__, s__ } from '~/locale'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import timeagoMixin from '~/vue_shared/mixins/timeago'; +import { formatDate } from '~/lib/utils/datetime_utility'; import { numberToHumanSize } from '~/lib/utils/number_utils'; import { - UPDATED_AT, + CREATED_AT, CLEANUP_UNSCHEDULED_TEXT, CLEANUP_SCHEDULED_TEXT, CLEANUP_ONGOING_TEXT, @@ -24,6 +25,7 @@ import { } from '../../constants/index'; import getContainerRepositoryMetadata from '../../graphql/queries/get_container_repository_metadata.query.graphql'; +import { getImageName } from '../../utils'; export default { name: 'DetailsHeader', @@ -65,11 +67,11 @@ export default { visibilityIcon() { return this.imageDetails?.project?.visibility === 'public' ? 'eye' : 'eye-slash'; }, - timeAgo() { - return this.timeFormatted(this.imageDetails.updatedAt); + formattedCreatedAtDate() { + return formatDate(this.imageDetails.createdAt, 'mmm d, yyyy HH:MM', true); }, - updatedText() { - return sprintf(UPDATED_AT, { time: this.timeAgo }); + createdText() { + return sprintf(CREATED_AT, { time: this.formattedCreatedAtDate }); }, tagCountText() { if (this.$apollo.queries.containerRepository.loading) { @@ -99,7 +101,7 @@ export default { return !this.imageDetails.name ? ROOT_IMAGE_TOOLTIP : ''; }, imageName() { - return this.imageDetails.name || this.imageDetails.project?.path; + return getImageName(this.imageDetails); }, formattedSize() { const { size } = this.imageDetails; @@ -145,9 +147,9 @@ export default { <template #metadata-updated> <metadata-item :icon="visibilityIcon" - :text="updatedText" + :text="createdText" size="xl" - data-testid="updated-and-visibility" + data-testid="created-and-visibility" /> </template> <template #right-actions> 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 c10d8be69a0..9ea1958a0d1 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,14 +1,18 @@ <script> import { GlEmptyState } from '@gitlab/ui'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { n__ } from '~/locale'; import { joinPaths } from '~/lib/utils/url_utility'; +import Tracking from '~/tracking'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; - import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import { + ALERT_SUCCESS_TAG, + ALERT_DANGER_TAG, + ALERT_SUCCESS_TAGS, + ALERT_DANGER_TAGS, REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE, GRAPHQL_PAGE_SIZE, @@ -20,19 +24,22 @@ import { NO_TAGS_MATCHING_FILTERS_DESCRIPTION, } from '../../constants/index'; import getContainerRepositoryTagsQuery from '../../graphql/queries/get_container_repository_tags.query.graphql'; +import deleteContainerRepositoryTagsMutation from '../../graphql/mutations/delete_container_repository_tags.mutation.graphql'; +import DeleteModal from '../delete_modal.vue'; import TagsListRow from './tags_list_row.vue'; export default { name: 'TagsList', components: { + DeleteModal, GlEmptyState, TagsListRow, TagsLoader, RegistryList, PersistedSearch, }, + mixins: [Tracking.mixin()], inject: ['config'], - props: { id: { type: [Number, String], @@ -77,6 +84,8 @@ export default { return { containerRepository: {}, filters: {}, + itemsToBeDeleted: [], + mutationLoading: false, sort: null, }; }, @@ -87,6 +96,9 @@ export default { tags() { return this.containerRepository?.tags?.nodes || []; }, + hideBulkDelete() { + return !this.containerRepository?.canDelete; + }, tagsPageInfo() { return this.containerRepository?.tags?.pageInfo; }, @@ -98,14 +110,16 @@ export default { sort: this.sort, }; }, - showMultiDeleteButton() { - return this.tags.some((tag) => tag.canDelete) && !this.isMobile; - }, hasNoTags() { return this.tags.length === 0; }, isLoading() { - return this.isImageLoading || this.$apollo.queries.containerRepository.loading || !this.sort; + return ( + this.isImageLoading || + this.$apollo.queries.containerRepository.loading || + this.mutationLoading || + !this.sort + ); }, hasFilters() { return this.filters?.name; @@ -116,17 +130,61 @@ export default { emptyStateDescription() { return this.hasFilters ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : NO_TAGS_MESSAGE; }, + tracking() { + return { + label: + this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', + }; + }, }, methods: { + deleteTags(toBeDeleted) { + this.itemsToBeDeleted = toBeDeleted; + this.track('click_button'); + this.$refs.deleteModal.show(); + }, + confirmDelete() { + this.handleDeleteTag(); + }, + async handleDeleteTag() { + this.track('confirm_delete'); + const { itemsToBeDeleted } = this; + this.mutationLoading = true; + try { + const { data } = await this.$apollo.mutate({ + mutation: deleteContainerRepositoryTagsMutation, + variables: { + id: this.queryVariables.id, + tagNames: itemsToBeDeleted.map((item) => item.name), + }, + awaitRefetchQueries: true, + refetchQueries: [ + { + query: getContainerRepositoryTagsQuery, + variables: this.queryVariables, + }, + ], + }); + if (data?.destroyContainerRepositoryTags?.errors[0]) { + throw new Error(); + } + this.$emit( + 'delete', + itemsToBeDeleted.length === 1 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS, + ); + this.itemsToBeDeleted = []; + } catch (e) { + this.$emit('delete', itemsToBeDeleted.length === 1 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS); + } finally { + this.mutationLoading = false; + } + }, fetchNextPage() { this.$apollo.queries.containerRepository.fetchMore({ variables: { after: this.tagsPageInfo?.endCursor, first: GRAPHQL_PAGE_SIZE, }, - updateQuery(_, { fetchMoreResult }) { - return fetchMoreResult; - }, }); }, fetchPreviousPage() { @@ -136,9 +194,6 @@ export default { before: this.tagsPageInfo?.startCursor, last: GRAPHQL_PAGE_SIZE, }, - updateQuery(_, { fetchMoreResult }) { - return fetchMoreResult; - }, }); }, handleSearchUpdate({ sort, filters }) { @@ -186,13 +241,14 @@ export default { /> <template v-else> <registry-list + :hidden-delete="hideBulkDelete" :title="listTitle" :pagination="tagsPageInfo" :items="tags" id-property="name" @prev-page="fetchPreviousPage" @next-page="fetchNextPage" - @delete="$emit('delete', $event)" + @delete="deleteTags" > <template #default="{ selectItem, isSelected, item, first }"> <tags-list-row @@ -202,10 +258,17 @@ export default { :is-mobile="isMobile" :disabled="disabled" @select="selectItem(item)" - @delete="$emit('delete', [item])" + @delete="deleteTags([item])" /> </template> </registry-list> + + <delete-modal + ref="deleteModal" + :items-to-be-deleted="itemsToBeDeleted" + @confirmDelete="confirmDelete" + @cancel="track('cancel_delete')" + /> </template> </template> </div> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue index 38b601ac3ec..8e89128a382 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue @@ -109,9 +109,6 @@ export default { isInvalidTag() { return !this.tag.digest; }, - isDeleteDisabled() { - return this.disabled || !this.tag.canDelete; - }, }, }; </script> @@ -179,16 +176,16 @@ export default { </gl-sprintf> </span> </template> - <template #right-action> + <template v-if="tag.canDelete" #right-action> <gl-dropdown - :disabled="isDeleteDisabled" + :disabled="disabled" icon="ellipsis_v" :text="$options.i18n.MORE_ACTIONS_TEXT" :text-sr-only="true" category="tertiary" no-caret right - :class="{ 'gl-opacity-0 gl-pointer-events-none': isDeleteDisabled }" + :class="{ 'gl-opacity-0 gl-pointer-events-none': disabled }" data-testid="additional-actions" data-qa-selector="more_actions_menu" > diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue index 4f89d217623..f6f816f435c 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue @@ -1,5 +1,5 @@ <script> -import { GlTooltipDirective, GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui'; +import { GlTooltipDirective, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui'; import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { n__ } from '~/locale'; import Tracking from '~/tracking'; @@ -28,7 +28,6 @@ export default { DeleteButton, GlSprintf, GlButton, - GlIcon, ListItem, GlSkeletonLoader, CleanupStatus, @@ -80,8 +79,8 @@ export default { }, tagsCountText() { return n__( - 'ContainerRegistry|%{count} Tag', - 'ContainerRegistry|%{count} Tags', + 'ContainerRegistry|%{count} tag', + 'ContainerRegistry|%{count} tags', this.item.tagsCount, ); }, @@ -152,7 +151,6 @@ export default { <span v-if="deleting">{{ $options.i18n.ROW_SCHEDULED_FOR_DELETION }}</span> <template v-else> <span class="gl-display-flex gl-align-items-center" data-testid="tags-count"> - <gl-icon name="tag" class="gl-mr-2" /> <gl-sprintf :message="tagsCountText"> <template #count> {{ item.tagsCount }} diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js index 7bb69363743..7ac803a8ece 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js @@ -65,7 +65,7 @@ export const MISSING_MANIFEST_WARNING_TOOLTIP = s__( 'ContainerRegistry|Invalid tag: missing manifest digest', ); -export const UPDATED_AT = s__('ContainerRegistry|Last updated %{time}'); +export const CREATED_AT = s__('ContainerRegistry|Created %{time}'); export const NOT_AVAILABLE_TEXT = __('Not applicable.'); export const NOT_AVAILABLE_SIZE = __('0 bytes'); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js index 9d0ecfd2dcb..71538ea5a07 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js @@ -1,14 +1,10 @@ import { s__ } from '~/locale'; -export const EXPIRATION_POLICY_WILL_RUN_IN = s__( - 'ContainerRegistry|Expiration policy will run in %{time}', -); -export const EXPIRATION_POLICY_DISABLED_TEXT = s__( - 'ContainerRegistry|Expiration policy is disabled.', -); +export const EXPIRATION_POLICY_WILL_RUN_IN = s__('ContainerRegistry|Cleanup will run in %{time}'); +export const EXPIRATION_POLICY_DISABLED_TEXT = s__('ContainerRegistry|Cleanup is not scheduled.'); export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted'); export const DELETE_ALERT_LINK_TEXT = s__( - 'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}', + 'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}run cleanup now manually%{adminLinkEnd} or you can wait for the next scheduled run of the cleanup policy. %{docLinkStart}More information%{docLinkEnd}', ); export const PARTIAL_CLEANUP_CONTINUE_MESSAGE = s__( 'ContainerRegistry|The cleanup will continue within %{time}. %{linkStart}Learn more%{linkEnd}', diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js index f2aa4916f48..89cdbf6acba 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js @@ -4,6 +4,7 @@ import { NAME_SORT_FIELD } from './common'; // Translations strings export const CONTAINER_REGISTRY_TITLE = s__('ContainerRegistry|Container Registry'); +export const SETTINGS_TEXT = s__('ContainerRegistry|Configure in settings'); export const CONNECTION_ERROR_TITLE = s__('ContainerRegistry|Docker connection error'); export const CONNECTION_ERROR_MESSAGE = s__( `ContainerRegistry|We are having trouble connecting to the Container Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`, @@ -15,9 +16,6 @@ export const LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION = s__( `ContainerRegistry|Image repository temporarily cannot be marked for deletion. Please try again in a few minutes. %{docLinkStart}More details%{docLinkEnd}`, ); export const REMOVE_REPOSITORY_LABEL = s__('ContainerRegistry|Remove repository'); -export const REMOVE_REPOSITORY_MODAL_TEXT = s__( - 'ContainerRegistry|You are about to remove repository %{title}. Once you confirm, this repository will be permanently deleted.', -); export const ROW_SCHEDULED_FOR_DELETION = s__( `ContainerRegistry|This image repository is scheduled for deletion`, ); diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js index 850dca07a3f..f9820df4a12 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/index.js @@ -6,7 +6,6 @@ Vue.use(VueApollo); export const mergeVariables = (existing, incoming) => { if (!incoming) return existing; - if (!existing) return incoming; return incoming; }; 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 e2036d9e63d..eae663acb48 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 @@ -7,7 +7,6 @@ query getContainerRepositoryDetails($id: ContainerRepositoryID!) { location canDelete createdAt - updatedAt expirationPolicyStartedAt expirationPolicyCleanupStatus project { 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 e57ac2a9efe..a0a80600603 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 @@ -12,6 +12,7 @@ query getContainerRepositoryTags( containerRepository(id: $id) { id tagsCount + canDelete tags(after: $after, before: $before, first: $first, last: $last, name: $name, sort: $sort) { nodes { digest diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js index a558550c91f..afddf78203d 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js @@ -36,6 +36,7 @@ export default () => { isGroupPage, isAdmin, showCleanupPolicyLink, + showContainerRegistrySettings, showUnfinishedTagCleanupCallout, connectionError, invalidPathError, @@ -69,6 +70,7 @@ export default () => { isGroupPage: parseBoolean(isGroupPage), isAdmin: parseBoolean(isAdmin), showCleanupPolicyLink: parseBoolean(showCleanupPolicyLink), + showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings), showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout), connectionError: parseBoolean(connectionError), invalidPathError: parseBoolean(invalidPathError), 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 83c0d2cdfca..3126af69c2c 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 @@ -1,36 +1,28 @@ <script> import { GlResizeObserverDirective, GlEmptyState } from '@gitlab/ui'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { joinPaths } from '~/lib/utils/url_utility'; import Tracking from '~/tracking'; import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; import DeleteImage from '../components/delete_image.vue'; import DeleteAlert from '../components/details_page/delete_alert.vue'; -import DeleteModal from '../components/details_page/delete_modal.vue'; +import DeleteModal from '../components/delete_modal.vue'; import DetailsHeader from '../components/details_page/details_header.vue'; import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue'; import StatusAlert from '../components/details_page/status_alert.vue'; import TagsList from '../components/details_page/tags_list.vue'; import { - ALERT_SUCCESS_TAG, - ALERT_DANGER_TAG, - ALERT_SUCCESS_TAGS, - ALERT_DANGER_TAGS, ALERT_DANGER_IMAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE, UNFINISHED_STATUS, MISSING_OR_DELETED_IMAGE_BREADCRUMB, - GRAPHQL_PAGE_SIZE, MISSING_OR_DELETED_IMAGE_TITLE, MISSING_OR_DELETED_IMAGE_MESSAGE, } 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'; -import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; export default { name: 'RegistryDetailsPage', @@ -76,7 +68,6 @@ export default { mutationLoading: false, deleteAlertType: null, hidePartialCleanupWarning: false, - deleteImageAlert: false, }; }, computed: { @@ -97,8 +88,7 @@ export default { }, tracking() { return { - label: - this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', + label: 'registry_image_delete', }; }, pageActionsAreDisabled() { @@ -112,57 +102,8 @@ export default { : MISSING_OR_DELETED_IMAGE_BREADCRUMB; this.breadCrumbState.updateName(name); }, - deleteTags(toBeDeleted) { - this.deleteImageAlert = false; - this.itemsToBeDeleted = toBeDeleted; - this.track('click_button'); - this.$refs.deleteModal.show(); - }, confirmDelete() { - if (this.deleteImageAlert) { - this.$refs.deleteImage.doDelete(); - } else { - this.handleDeleteTag(); - } - }, - async handleDeleteTag() { - this.track('confirm_delete'); - const { itemsToBeDeleted } = this; - this.itemsToBeDeleted = []; - this.mutationLoading = true; - try { - const { data } = await this.$apollo.mutate({ - mutation: deleteContainerRepositoryTagsMutation, - variables: { - id: this.queryVariables.id, - tagNames: itemsToBeDeleted.map((i) => i.name), - }, - awaitRefetchQueries: true, - refetchQueries: [ - { - query: getContainerRepositoryTagsQuery, - variables: { ...this.queryVariables, first: GRAPHQL_PAGE_SIZE }, - }, - { - query: getContainerRepositoriesDetails, - variables: { - fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, - isGroupPage: this.config.isGroupPage, - }, - }, - ], - }); - - if (data?.destroyContainerRepositoryTags?.errors[0]) { - throw new Error(); - } - this.deleteAlertType = - itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS; - } catch (e) { - this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS; - } - - this.mutationLoading = false; + this.$refs.deleteImage.doDelete(); }, handleResize() { this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs'; @@ -174,7 +115,6 @@ export default { }); }, deleteImage() { - this.deleteImageAlert = true; this.itemsToBeDeleted = [{ ...this.containerRepository }]; this.$refs.deleteModal.show(); }, @@ -185,6 +125,9 @@ export default { this.itemsToBeDeleted = []; this.mutationLoading = true; }, + showAlert(alertType) { + this.deleteAlertType = alertType; + }, }, }; </script> @@ -222,7 +165,7 @@ export default { :is-image-loading="isLoading" :is-mobile="isMobile" :disabled="pageActionsAreDisabled" - @delete="deleteTags" + @delete="showAlert" /> <delete-image @@ -237,7 +180,7 @@ export default { <delete-modal ref="deleteModal" :items-to-be-deleted="itemsToBeDeleted" - :delete-image="deleteImageAlert" + delete-image @confirmDelete="confirmDelete" @cancel="track('cancel_delete')" /> 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 8a038d7c974..fe29fa8fdd7 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 @@ -1,8 +1,8 @@ <script> import { + GlButton, GlEmptyState, GlTooltipDirective, - GlModal, GlSprintf, GlLink, GlAlert, @@ -10,31 +10,33 @@ import { } from '@gitlab/ui'; import { get } from 'lodash'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; +import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import Tracking from '~/tracking'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; import DeleteImage from '../components/delete_image.vue'; import RegistryHeader from '../components/list_page/registry_header.vue'; +import DeleteModal from '../components/delete_modal.vue'; import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, CONNECTION_ERROR_TITLE, CONNECTION_ERROR_MESSAGE, - REMOVE_REPOSITORY_MODAL_TEXT, - REMOVE_REPOSITORY_LABEL, EMPTY_RESULT_TITLE, EMPTY_RESULT_MESSAGE, GRAPHQL_PAGE_SIZE, FETCH_IMAGES_LIST_ERROR_MESSAGE, SORT_FIELDS, + SETTINGS_TEXT, } from '../constants/index'; import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql'; export default { name: 'RegistryListPage', components: { + GlButton, GlEmptyState, ProjectEmptyState: () => import( @@ -52,7 +54,7 @@ export default { import( /* webpackChunkName: 'container_registry_components' */ '~/packages_and_registries/shared/components/cli_commands.vue' ), - GlModal, + DeleteModal, GlSprintf, GlLink, GlAlert, @@ -74,10 +76,9 @@ export default { i18n: { CONNECTION_ERROR_TITLE, CONNECTION_ERROR_MESSAGE, - REMOVE_REPOSITORY_MODAL_TEXT, - REMOVE_REPOSITORY_LABEL, EMPTY_RESULT_TITLE, EMPTY_RESULT_MESSAGE, + SETTINGS_TEXT, }, searchConfig: SORT_FIELDS, apollo: { @@ -144,8 +145,11 @@ export default { } return []; }, + itemsToBeDeleted() { + return this.itemToDelete?.id ? [this.itemToDelete] : []; + }, graphqlResource() { - return this.config.isGroupPage ? 'group' : 'project'; + return this.config.isGroupPage ? WORKSPACE_GROUP : WORKSPACE_PROJECT; }, queryVariables() { return { @@ -306,6 +310,13 @@ export default { :docker-push-command="dockerPushCommand" :docker-login-command="dockerLoginCommand" /> + <gl-button + v-if="config.showContainerRegistrySettings" + v-gl-tooltip="$options.i18n.SETTINGS_TEXT" + icon="settings" + :href="config.cleanupPoliciesSettingsPath" + :aria-label="$options.i18n.SETTINGS_TEXT" + /> </template> </registry-header> <persisted-search @@ -367,26 +378,13 @@ export default { @end="mutationLoading = false" > <template #default="{ doDelete }"> - <gl-modal + <delete-modal ref="deleteModal" - size="sm" - modal-id="delete-image-modal" - :action-primary="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ { - text: __('Remove'), - attributes: { variant: 'danger' }, - } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */" - @primary="doDelete" + :items-to-be-deleted="itemsToBeDeleted" + delete-image + @confirmDelete="doDelete" @cancel="track('cancel_delete')" - > - <template #modal-title>{{ $options.i18n.REMOVE_REPOSITORY_LABEL }}</template> - <p> - <gl-sprintf :message="$options.i18n.REMOVE_REPOSITORY_MODAL_TEXT"> - <template #title> - <b>{{ itemToDelete.path }}</b> - </template> - </gl-sprintf> - </p> - </gl-modal> + /> </template> </delete-image> </template> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js index ffdaf9f2f17..751ab5180a1 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/utils.js @@ -1,5 +1,9 @@ import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility'; +export const getImageName = (image = {}) => { + return image.name || image.project?.path; +}; + export const timeTilRun = (time) => { if (!time) return ''; 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 45dc217b9e3..732d544816b 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -1,6 +1,7 @@ <script> import { GlAlert, + GlButton, GlDropdown, GlDropdownItem, GlEmptyState, @@ -8,8 +9,8 @@ import { GlFormInputGroup, GlModal, GlModalDirective, - GlSkeletonLoader, GlSprintf, + GlTooltipDirective, } from '@gitlab/ui'; import { __, s__, n__, sprintf } from '~/locale'; import Api from '~/api'; @@ -24,13 +25,13 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency export default { components: { GlAlert, + GlButton, GlDropdown, GlDropdownItem, GlEmptyState, GlFormGroup, GlFormInputGroup, GlModal, - GlSkeletonLoader, GlSprintf, ClipboardButton, TitleArea, @@ -38,8 +39,9 @@ export default { }, directives: { GlModalDirective, + GlTooltip: GlTooltipDirective, }, - inject: ['groupPath', 'groupId', 'noManifestsIllustration', 'canClearCache'], + inject: ['groupPath', 'groupId', 'noManifestsIllustration', 'canClearCache', 'settingsPath'], i18n: { proxyImagePrefix: s__('DependencyProxy|Dependency Proxy image prefix'), copyImagePrefixText: s__('DependencyProxy|Copy prefix'), @@ -50,12 +52,13 @@ export default { 'DependencyProxy|All items in the cache are scheduled for removal.', ), clearCache: s__('DependencyProxy|Clear cache'), + settingsText: s__('DependencyProxy|Configure in settings'), }, confirmClearCacheModal: 'confirm-clear-cache-modal', modalButtons: { primary: { text: s__('DependencyProxy|Clear cache'), - attributes: [{ variant: 'danger' }], + attributes: { variant: 'danger' }, }, secondary: { text: __('Cancel'), @@ -114,10 +117,13 @@ export default { ); }, showDeleteDropdown() { - return this.group.dependencyProxyManifests?.nodes.length > 0 && this.canClearCache; + return this.manifests?.length > 0 && this.canClearCache; + }, + dependencyProxyImagePrefix() { + return this.group.dependencyProxyImagePrefix; }, showDependencyProxyImagePrefix() { - return this.group.dependencyProxyImagePrefix?.length > 0; + return this.dependencyProxyImagePrefix?.length > 0; }, }, methods: { @@ -167,8 +173,9 @@ export default { {{ deleteCacheAlertMessage }} </gl-alert> <title-area :title="$options.i18n.pageTitle"> - <template v-if="showDeleteDropdown" #right-actions> + <template #right-actions> <gl-dropdown + v-if="showDeleteDropdown" icon="ellipsis_v" text="More actions" :text-sr-only="true" @@ -181,6 +188,14 @@ export default { >{{ $options.i18n.clearCache }}</gl-dropdown-item > </gl-dropdown> + <gl-button + v-if="canClearCache" + v-gl-tooltip="$options.i18n.settingsText" + icon="settings" + data-testid="settings-link" + :href="settingsPath" + :aria-label="$options.i18n.settingsText" + /> </template> </title-area> @@ -208,23 +223,21 @@ export default { </template> </gl-form-group> - <gl-skeleton-loader v-if="$apollo.queries.group.loading" /> - - <div v-else data-testid="main-area"> - <manifests-list - v-if="manifests && manifests.length" - :manifests="manifests" - :pagination="pageInfo" - @prev-page="fetchPreviousPage" - @next-page="fetchNextPage" - /> + <manifests-list + v-if="manifests && manifests.length" + :dependency-proxy-image-prefix="dependencyProxyImagePrefix" + :loading="$apollo.queries.group.loading" + :manifests="manifests" + :pagination="pageInfo" + @prev-page="fetchPreviousPage" + @next-page="fetchNextPage" + /> - <gl-empty-state - v-else - :svg-path="noManifestsIllustration" - :title="$options.i18n.noManifestTitle" - /> - </div> + <gl-empty-state + v-else + :svg-path="noManifestsIllustration" + :title="$options.i18n.noManifestTitle" + /> <gl-modal :modal-id="$options.confirmClearCacheModal" diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue index 1bbd0c32dc4..254fd578cf1 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifest_row.vue @@ -3,11 +3,16 @@ import { GlIcon, GlSprintf } from '@gitlab/ui'; import { MANIFEST_PENDING_DESTRUCTION_STATUS } from '~/packages_and_registries/dependency_proxy/constants'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import { s__ } from '~/locale'; +const SHORT_DIGEST_START_INDEX = 7; +const SHORT_DIGEST_END_INDEX = 14; + export default { name: 'ManifestRow', components: { + ClipboardButton, GlIcon, GlSprintf, ListItem, @@ -18,13 +23,25 @@ export default { type: Object, required: true, }, + dependencyProxyImagePrefix: { + type: String, + default: '', + required: false, + }, }, computed: { name() { - return this.manifest?.imageName.split(':')[0]; + if (this.containsDigestInImageName) { + return this.manifest?.imageName.split(':')[0]; + } + return this.manifest?.imageName; }, - version() { - return this.manifest?.imageName.split(':')[1]; + imageCopyText() { + const name = this.manifest?.imageName.replace(':sha256:', '@sha256:') ?? ''; + return `${this.dependencyProxyImagePrefix}/${name}`; + }, + containsDigestInImageName() { + return this.manifest?.imageName.includes(':sha256:'); }, isErrorStatus() { return this.manifest?.status === MANIFEST_PENDING_DESTRUCTION_STATUS; @@ -32,9 +49,16 @@ export default { disabledRowStyle() { return this.isErrorStatus ? 'gl-font-weight-normal gl-text-gray-500' : ''; }, + shortDigest() { + // digest is in the format `sha256:995efde2e81b21d1ea7066aa77a59298a62a9e9fbb4b77f36c189774ec9b1089` + // for short digest, remove sha256: from the string, and show only the first 7 char + return this.manifest.digest?.substring(SHORT_DIGEST_START_INDEX, SHORT_DIGEST_END_INDEX); + }, }, i18n: { cachedAgoMessage: s__('DependencyProxy|Cached %{time}'), + copyImagePathTitle: s__('DependencyProxy|Copy image path'), + digestLabel: s__('DependencyProxy|Digest: %{shortDigest}'), scheduledForDeletion: s__('DependencyProxy|Scheduled for deletion'), }, }; @@ -44,9 +68,21 @@ export default { <list-item :disabled="isErrorStatus"> <template #left-primary> <span :class="disabledRowStyle">{{ name }}</span> + <clipboard-button + class="gl-ml-2" + :text="imageCopyText" + :title="$options.i18n.copyImagePathTitle" + category="tertiary" + /> </template> <template #left-secondary> - {{ version }} + <span data-testid="manifest-row-short-digest"> + <gl-sprintf :message="$options.i18n.digestLabel"> + <template #shortDigest> + {{ shortDigest }} + </template> + </gl-sprintf> + </span> <span v-if="isErrorStatus" class="gl-ml-4" data-testid="status" ><gl-icon name="clock" /> {{ $options.i18n.scheduledForDeletion }}</span > diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue index 005c8feea3a..9870841f1ff 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/components/manifests_list.vue @@ -1,5 +1,5 @@ <script> -import { GlKeysetPagination } from '@gitlab/ui'; +import { GlKeysetPagination, GlSkeletonLoader } from '@gitlab/ui'; import { s__ } from '~/locale'; import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue'; @@ -8,6 +8,7 @@ export default { components: { ManifestRow, GlKeysetPagination, + GlSkeletonLoader, }, props: { manifests: { @@ -19,6 +20,16 @@ export default { type: Object, required: true, }, + loading: { + type: Boolean, + required: false, + default: () => false, + }, + dependencyProxyImagePrefix: { + type: String, + default: '', + required: false, + }, }, i18n: { listTitle: s__('DependencyProxy|Image list'), @@ -34,19 +45,27 @@ export default { <template> <div class="gl-mt-6"> <h3 class="gl-font-base">{{ $options.i18n.listTitle }}</h3> - <div - class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column" - > - <manifest-row v-for="(manifest, index) in manifests" :key="index" :manifest="manifest" /> - </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')" - /> + <gl-skeleton-loader v-if="loading" /> + <div v-else data-testid="main-area"> + <div + class="gl-border-t-1 gl-border-gray-100 gl-border-t-solid gl-display-flex gl-flex-direction-column" + > + <manifest-row + v-for="(manifest, index) in manifests" + :key="index" + :dependency-proxy-image-prefix="dependencyProxyImagePrefix" + :manifest="manifest" + /> + </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> </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 c1597625964..db0e596ba64 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 @@ -19,6 +19,7 @@ query getDependencyProxyDetails( nodes { id createdAt + digest imageName status } diff --git a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js index 428d6d6cd75..74444d2c7ec 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/index.js @@ -11,7 +11,7 @@ export const initDependencyProxyApp = () => { if (!el) { return null; } - const { groupPath, groupId, noManifestsIllustration, canClearCache } = el.dataset; + const { groupPath, groupId, noManifestsIllustration, canClearCache, settingsPath } = el.dataset; return new Vue({ el, apolloProvider, @@ -20,6 +20,7 @@ export const initDependencyProxyApp = () => { groupId, noManifestsIllustration, canClearCache: parseBoolean(canClearCache), + settingsPath, }, render(createElement) { return createElement(app); diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue index bafcd78ad5d..bff32a124bc 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue @@ -9,7 +9,7 @@ import { TAG_LABEL, } from '~/packages_and_registries/harbor_registry/constants/index'; import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue'; import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue'; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue index 1323d347d10..8bc1ecba5fe 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue @@ -4,7 +4,7 @@ import TagsList from '~/packages_and_registries/harbor_registry/components/tags/ import { getHarborTags } from '~/rest_api'; import { FETCH_TAGS_ERROR_MESSAGE } from '~/packages_and_registries/harbor_registry/constants'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { formatPagination } from '~/packages_and_registries/harbor_registry/utils'; export default { diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue index 931a99649cb..1d8cb0f1360 100644 --- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue @@ -12,7 +12,7 @@ import { dockerPushCommand, dockerLoginCommand, } from '~/packages_and_registries/harbor_registry/utils'; -import { createAlert } from '~/flash'; +import { createAlert } from '~/alert'; import { SORT_FIELDS, CONNECTION_ERROR_TITLE, 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 fd099ee4e69..fdc58e4bd05 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 @@ -122,15 +122,15 @@ export default { modal: { packageDeletePrimaryAction: { text: __('Delete'), - attributes: [ - { variant: 'danger' }, - { category: 'primary' }, - { 'data-qa-selector': 'delete_modal_button' }, - ], + attributes: { + variant: 'danger', + category: 'primary', + 'data-qa-selector': 'delete_modal_button', + }, }, fileDeletePrimaryAction: { text: __('Delete'), - attributes: [{ variant: 'danger' }, { category: 'primary' }], + attributes: { variant: 'danger', category: 'primary' }, }, cancelAction: { text: __('Cancel'), 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 223f427ce0e..62c4f96eff7 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 @@ -1,5 +1,5 @@ import Api from '~/api'; -import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert'; import { DELETE_PACKAGE_ERROR_MESSAGE, DELETE_PACKAGE_FILE_ERROR_MESSAGE, diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue index 9bab08b8548..a9d076afb92 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue @@ -36,7 +36,7 @@ export default { }, }, i18n: { - LIST_TITLE_TEXT: s__('InfrastructureRegistry|Infrastructure Registry'), + LIST_TITLE_TEXT: s__('InfrastructureRegistry|Terraform Module Registry'), LIST_INTRO_TEXT: s__( 'InfrastructureRegistry|Publish and share your modules. %{docLinkStart}More information%{docLinkEnd}', ), 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 index 0aeeb2c3d15..6ea1fff9ef0 100644 --- 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 @@ -1,7 +1,7 @@ <script> import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; import { mapActions, mapState } from 'vuex'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { createAlert, VARIANT_INFO } from '~/alert'; import { historyReplaceState } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants'; 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 index 7af3fc1c2db..05673215a66 100644 --- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js +++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/constants.js @@ -6,7 +6,6 @@ export const FETCH_PACKAGES_LIST_ERROR_MESSAGE = __( 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'; 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 index 7a452abdc26..122123f49cd 100644 --- 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 @@ -1,13 +1,13 @@ import Api from '~/api'; -import { createAlert, VARIANT_SUCCESS } from '~/flash'; +import { createAlert, VARIANT_SUCCESS } from '~/alert'; import axios from '~/lib/utils/axios_utils'; import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants'; import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants'; +import { DEFAULT_PAGE_SIZE } from '~/vue_shared/issuable/list/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'; diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue index 011a2668a8b..0c3494ea812 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/delete_modal.vue @@ -1,39 +1,71 @@ <script> -import { GlModal } from '@gitlab/ui'; -import { __, n__ } from '~/locale'; +import { GlLink, GlModal, GlSprintf } from '@gitlab/ui'; +import { __ } from '~/locale'; import { + DELETE_MODAL_CONTENT, + DELETE_MODAL_TITLE, + DELETE_PACKAGES_MODAL_DESCRIPTION, DELETE_PACKAGES_MODAL_TITLE, DELETE_PACKAGE_MODAL_PRIMARY_ACTION, + DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT, + DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT, + DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION, + DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION, + REQUEST_FORWARDING_HELP_PAGE_PATH, } from '~/packages_and_registries/package_registry/constants'; export default { name: 'DeleteModal', - i18n: { - DELETE_PACKAGES_MODAL_TITLE, - }, components: { + GlLink, GlModal, + GlSprintf, }, props: { itemsToBeDeleted: { type: Array, required: true, }, + showRequestForwardingContent: { + type: Boolean, + required: false, + default: false, + }, }, computed: { - description() { - return n__( - 'PackageRegistry|You are about to delete 1 package. This operation is irreversible.', - `PackageRegistry|You are about to delete %d packages. This operation is irreversible.`, - this.itemsToBeDeleted.length, - ); + itemToBeDeleted() { + return this.itemsToBeDeleted.length === 1 ? this.itemsToBeDeleted[0] : null; + }, + title() { + return this.itemToBeDeleted ? DELETE_MODAL_TITLE : DELETE_PACKAGES_MODAL_TITLE; + }, + packageDescription() { + return this.showRequestForwardingContent + ? DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT + : DELETE_MODAL_CONTENT; + }, + packagesDescription() { + return this.showRequestForwardingContent + ? DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT + : DELETE_PACKAGES_MODAL_DESCRIPTION; + }, + packagesDeletePrimaryActionProps() { + let text = DELETE_PACKAGE_MODAL_PRIMARY_ACTION; + + if (this.showRequestForwardingContent) { + if (this.itemToBeDeleted) { + text = DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION; + } else { + text = DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION; + } + } + return { + text, + attributes: { variant: 'danger', category: 'primary' }, + }; }, }, modal: { - packagesDeletePrimaryAction: { - text: DELETE_PACKAGE_MODAL_PRIMARY_ACTION, - attributes: [{ variant: 'danger' }, { category: 'primary' }], - }, cancelAction: { text: __('Cancel'), }, @@ -43,6 +75,9 @@ export default { this.$refs.deleteModal.show(); }, }, + links: { + REQUEST_FORWARDING_HELP_PAGE_PATH, + }, }; </script> @@ -51,12 +86,33 @@ export default { ref="deleteModal" size="sm" modal-id="delete-packages-modal" - :action-primary="$options.modal.packagesDeletePrimaryAction" + :action-primary="packagesDeletePrimaryActionProps" :action-cancel="$options.modal.cancelAction" - :title="$options.i18n.DELETE_PACKAGES_MODAL_TITLE" + :title="title" @primary="$emit('confirm')" @cancel="$emit('cancel')" > - <span>{{ description }}</span> + <p> + <gl-sprintf v-if="itemToBeDeleted" :message="packageDescription"> + <template v-if="showRequestForwardingContent" #docLink="{ content }"> + <gl-link :href="$options.links.REQUEST_FORWARDING_HELP_PAGE_PATH">{{ content }}</gl-link> + </template> + <template #version> + <strong>{{ itemToBeDeleted.version }}</strong> + </template> + <template #name> + <strong>{{ itemToBeDeleted.name }}</strong> + </template> + </gl-sprintf> + <gl-sprintf v-else :message="packagesDescription"> + <template v-if="showRequestForwardingContent" #docLink="{ content }"> + <gl-link :href="$options.links.REQUEST_FORWARDING_HELP_PAGE_PATH">{{ content }}</gl-link> + </template> + + <template #count> + {{ itemsToBeDeleted.length }} + </template> + </gl-sprintf> + </p> </gl-modal> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue index 4510c7a7322..95b83d87792 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue @@ -13,6 +13,7 @@ import { TRACKING_LABEL_CODE_INSTRUCTION, TRACKING_LABEL_MAVEN_INSTALLATION, MAVEN_HELP_PATH, + MAVEN_INSTALLATION_COMMAND, } from '~/packages_and_registries/package_registry/constants'; import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue'; @@ -55,11 +56,6 @@ export default { <version>${this.appVersion}</version> </dependency>`; }, - - mavenInstallationCommand() { - return `mvn dependency:get -Dartifact=${this.appGroup}:${this.appName}:${this.appVersion}`; - }, - mavenSetupXml() { return `<repositories> <repository> @@ -135,6 +131,7 @@ export default { { value: 'groovy', label: s__('PackageRegistry|Gradle Groovy DSL') }, { value: 'kotlin', label: s__('PackageRegistry|Gradle Kotlin DSL') }, ], + MAVEN_INSTALLATION_COMMAND, }; </script> @@ -164,8 +161,9 @@ export default { /> <code-instruction + class="gl-w-20 gl-mt-5" :label="s__('PackageRegistry|Maven Command')" - :instruction="mavenInstallationCommand" + :instruction="$options.MAVEN_INSTALLATION_COMMAND" :copy-text="s__('PackageRegistry|Copy Maven command')" :tracking-action="$options.tracking.TRACKING_ACTION_COPY_MAVEN_COMMAND" :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue index d982df4f984..482249bc252 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_versions_list.vue @@ -1,19 +1,29 @@ <script> +import { GlAlert } from '@gitlab/ui'; +import * as Sentry from '@sentry/browser'; import { n__ } from '~/locale'; import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue'; import { + CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION, CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + DELETE_PACKAGE_VERSION_TRACKING_ACTION, DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, + FETCH_PACKAGE_VERSIONS_ERROR_MESSAGE, + GRAPHQL_PAGE_SIZE, + REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION, } from '~/packages_and_registries/package_registry/constants'; import Tracking from '~/tracking'; +import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; +import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql'; export default { components: { DeleteModal, + GlAlert, VersionRow, PackagesListLoader, RegistryList, @@ -25,49 +35,139 @@ export default { required: false, default: false, }, - versions: { - type: Array, - required: true, - default: () => [], + count: { + type: Number, + required: false, + default: 0, }, - pageInfo: { - type: Object, - required: true, + isMutationLoading: { + type: Boolean, + required: false, + default: false, }, - isLoading: { + isRequestForwardingEnabled: { type: Boolean, required: false, default: false, }, + packageId: { + type: String, + required: true, + }, }, data() { return { itemsToBeDeleted: [], + packageVersions: {}, + fetchPackageVersionsError: false, }; }, + apollo: { + packageVersions: { + query: getPackageVersionsQuery, + variables() { + return this.queryVariables; + }, + skip() { + return this.isListEmpty; + }, + update(data) { + return data.package?.versions ?? {}; + }, + error(error) { + this.fetchPackageVersionsError = true; + Sentry.captureException(error); + }, + }, + }, computed: { + itemToBeDeleted() { + return this.itemsToBeDeleted.length === 1 ? this.itemsToBeDeleted[0] : null; + }, + isListEmpty() { + return this.count === 0; + }, + isLoading() { + return this.$apollo.queries.packageVersions.loading || this.isMutationLoading; + }, + pageInfo() { + return this.packageVersions?.pageInfo ?? {}; + }, listTitle() { return n__('%d version', '%d versions', this.versions.length); }, - isListEmpty() { - return this.versions.length === 0; + queryVariables() { + return { + id: this.packageId, + first: GRAPHQL_PAGE_SIZE, + }; + }, + tracking() { + const category = this.itemToBeDeleted + ? packageTypeToTrackCategory(this.itemToBeDeleted.packageType) + : undefined; + return { + category, + }; + }, + versions() { + return this.packageVersions?.nodes ?? []; }, }, methods: { deleteItemsCanceled() { - this.track(CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + if (this.itemToBeDeleted) { + this.track(CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION); + } else { + this.track(CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + } + this.itemsToBeDeleted = []; }, deleteItemsConfirmation() { this.$emit('delete', this.itemsToBeDeleted); - this.track(DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + if (this.itemToBeDeleted) { + this.track(DELETE_PACKAGE_VERSION_TRACKING_ACTION); + } else { + this.track(DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + } this.itemsToBeDeleted = []; }, setItemsToBeDeleted(items) { this.itemsToBeDeleted = items; - this.track(REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + if (items.length === 1) { + this.track(REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION); + } else { + this.track(REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION); + } this.$refs.deletePackagesModal.show(); }, + fetchPreviousVersionsPage() { + const variables = { + ...this.queryVariables, + first: null, + last: GRAPHQL_PAGE_SIZE, + before: this.pageInfo?.startCursor, + }; + this.$apollo.queries.packageVersions.fetchMore({ + variables, + }); + }, + fetchNextVersionsPage() { + const variables = { + ...this.queryVariables, + first: GRAPHQL_PAGE_SIZE, + last: null, + after: this.pageInfo?.endCursor, + }; + + this.$apollo.queries.packageVersions.fetchMore({ + variables, + }); + }, + }, + i18n: { + errorMessage: FETCH_PACKAGE_VERSIONS_ERROR_MESSAGE, }, }; </script> @@ -76,6 +176,9 @@ export default { <div v-if="isLoading"> <packages-list-loader /> </div> + <gl-alert v-else-if="fetchPackageVersionsError" variant="danger" :dismissible="false">{{ + $options.i18n.errorMessage + }}</gl-alert> <slot v-else-if="isListEmpty" name="empty-state"></slot> <div v-else> <registry-list @@ -85,17 +188,15 @@ export default { :pagination="pageInfo" :title="listTitle" @delete="setItemsToBeDeleted" - @prev-page="$emit('prev-page')" - @next-page="$emit('next-page')" + @prev-page="fetchPreviousVersionsPage" + @next-page="fetchNextVersionsPage" > <template #default="{ first, item, isSelected, selectItem }"> - <!-- `first` prop is used to decide whether to show the top border - for the first element. We want to show the top border only when - user has permission to bulk delete versions. --> <version-row - :first="canDestroy && first" + :first="first" :package-entity="item" :selected="isSelected(item)" + @delete="setItemsToBeDeleted([item])" @select="selectItem(item)" /> </template> @@ -104,6 +205,7 @@ export default { <delete-modal ref="deletePackagesModal" :items-to-be-deleted="itemsToBeDeleted" + :show-request-forwarding-content="isRequestForwardingEnabled" @confirm="deleteItemsConfirmation" @cancel="deleteItemsCanceled" /> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue index fdc6e75c932..ea6ebb614f4 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue @@ -28,6 +28,9 @@ export default { }, }, computed: { + isPrivatePackage() { + return !this.packageEntity.publicPackage; + }, pypiPipCommand() { // eslint-disable-next-line @gitlab/require-i18n-strings return `pip install ${this.packageEntity.name} --index-url ${this.packageEntity.pypiUrl}`; @@ -75,7 +78,7 @@ password = <your personal access token>`; :tracking-action="$options.tracking.TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND" :tracking-label="$options.tracking.TRACKING_LABEL_CODE_INSTRUCTION" /> - <template #description> + <template v-if="isPrivatePackage" #description> <gl-sprintf :message="$options.i18n.tokenText"> <template #link="{ content }"> <gl-link 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 9f8f6328970..37a6fe75f15 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,5 +1,7 @@ <script> import { + GlDropdown, + GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, @@ -13,6 +15,7 @@ import PublishMethod from '~/packages_and_registries/shared/components/publish_m import ListItem from '~/vue_shared/components/registry/list_item.vue'; import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue'; import { + DELETE_PACKAGE_TEXT, ERRORED_PACKAGE_TEXT, ERROR_PUBLISHING, PACKAGE_ERROR_STATUS, @@ -22,6 +25,8 @@ import { export default { name: 'PackageVersionRow', components: { + GlDropdown, + GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, @@ -58,6 +63,7 @@ export default { }, }, i18n: { + deletePackage: DELETE_PACKAGE_TEXT, erroredPackageText: ERRORED_PACKAGE_TEXT, errorPublishing: ERROR_PUBLISHING, warningText: WARNING_TEXT, @@ -121,5 +127,20 @@ export default { </gl-sprintf> </span> </template> + + <template v-if="packageEntity.canDestroy" #right-action> + <gl-dropdown + data-testid="delete-dropdown" + icon="ellipsis_v" + :text="$options.i18n.moreActions" + :text-sr-only="true" + category="tertiary" + no-caret + > + <gl-dropdown-item data-testid="action-delete" variant="danger" @click="$emit('delete')">{{ + $options.i18n.deletePackage + }}</gl-dropdown-item> + </gl-dropdown> + </template> </list-item> </template> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_packages.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_packages.vue index 0914c013108..b7e66d20e78 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_packages.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/functional/delete_packages.vue @@ -1,6 +1,6 @@ <script> import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql'; -import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert'; import { DELETE_PACKAGE_ERROR_MESSAGE, 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 16f21bfe61d..4ec83a869b3 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 @@ -8,9 +8,10 @@ import { GlTooltipDirective, GlTruncate, } from '@gitlab/ui'; -import { s__, __ } from '~/locale'; +import { __ } from '~/locale'; import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { + DELETE_PACKAGE_TEXT, ERRORED_PACKAGE_TEXT, ERROR_PUBLISHING, PACKAGE_ERROR_STATUS, @@ -21,7 +22,6 @@ import { getPackageTypeLabel } from '~/packages_and_registries/package_registry/ 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_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'; @@ -38,7 +38,6 @@ export default { PackagePath, PublishMethod, ListItem, - PackageIconAndName, TimeagoTooltip, }, directives: { @@ -91,7 +90,7 @@ export default { i18n: { erroredPackageText: ERRORED_PACKAGE_TEXT, createdAt: __('Created %{timestamp}'), - deletePackage: s__('PackageRegistry|Delete package'), + deletePackage: DELETE_PACKAGE_TEXT, errorPublishing: ERROR_PUBLISHING, warning: WARNING_TEXT, moreActions: __('More actions'), @@ -150,9 +149,7 @@ export default { </gl-sprintf> </div> - <package-icon-and-name> - {{ packageType }} - </package-icon-and-name> + <span class="gl-ml-2" data-testid="package-type">· {{ packageType }}</span> <package-path v-if="isGroupPage" diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue index 440e11a99f2..05359128af4 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_title.vue @@ -39,5 +39,8 @@ export default { <template #metadata-amount> <metadata-item v-if="showPackageCount" icon="package" :text="packageAmountText" /> </template> + <template #right-actions> + <slot name="settings-link"></slot> + </template> </title-area> </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 486ab4fdc99..effed4891d8 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 @@ -1,7 +1,6 @@ <script> import { GlAlert } from '@gitlab/ui'; import { s__, sprintf, n__ } from '~/locale'; -import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue'; import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue'; import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue'; import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; @@ -14,16 +13,24 @@ import { CANCEL_DELETE_PACKAGE_TRACKING_ACTION, CANCEL_DELETE_PACKAGES_TRACKING_ACTION, PACKAGE_ERROR_STATUS, + PACKAGE_TYPE_MAVEN, + PACKAGE_TYPE_NPM, + PACKAGE_TYPE_PYPI, } from '~/packages_and_registries/package_registry/constants'; import { packageTypeToTrackCategory } from '~/packages_and_registries/package_registry/utils'; import Tracking from '~/tracking'; +const forwardingFieldToPackageTypeMapping = { + mavenPackageRequestsForwarding: PACKAGE_TYPE_MAVEN, + npmPackageRequestsForwarding: PACKAGE_TYPE_NPM, + pypiPackageRequestsForwarding: PACKAGE_TYPE_PYPI, +}; + export default { name: 'PackagesList', components: { GlAlert, DeleteModal, - DeletePackageModal, PackagesListLoader, PackagesListRow, RegistryList, @@ -44,16 +51,27 @@ export default { type: Object, required: true, }, + groupSettings: { + type: Object, + required: false, + default: () => ({}), + }, }, data() { return { - itemToBeDeleted: null, itemsToBeDeleted: [], errorPackages: [], }; }, computed: { + itemToBeDeleted() { + if (this.itemsToBeDeleted.length === 1) { + const [itemToBeDeleted] = this.itemsToBeDeleted; + return itemToBeDeleted; + } + return null; + }, listTitle() { return n__('%d package', '%d packages', this.list.length); }, @@ -77,6 +95,15 @@ export default { showErrorPackageAlert() { return this.errorPackages.length > 0; }, + packageTypesWithForwardingEnabled() { + return Object.keys(this.groupSettings) + .filter((field) => this.groupSettings[field]) + .map((field) => forwardingFieldToPackageTypeMapping[field]); + }, + isRequestForwardingEnabled() { + const selectedPackageTypes = new Set(this.itemsToBeDeleted.map((item) => item.packageType)); + return this.packageTypesWithForwardingEnabled.some((type) => selectedPackageTypes.has(type)); + }, }, watch: { list(newVal) { @@ -88,40 +115,36 @@ export default { this.list.length > 0 ? this.list.filter((pkg) => pkg.status === PACKAGE_ERROR_STATUS) : []; }, methods: { - setItemToBeDeleted(item) { - this.itemToBeDeleted = { ...item }; - this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION); - }, setItemsToBeDeleted(items) { + this.itemsToBeDeleted = items; if (items.length === 1) { - const [item] = items; - this.setItemToBeDeleted(item); - return; + this.track(REQUEST_DELETE_PACKAGE_TRACKING_ACTION); + } else { + this.track(REQUEST_DELETE_PACKAGES_TRACKING_ACTION); } - this.itemsToBeDeleted = items; - this.track(REQUEST_DELETE_PACKAGES_TRACKING_ACTION); this.$refs.deletePackagesModal.show(); }, deleteItemsConfirmation() { this.$emit('delete', this.itemsToBeDeleted); - this.track(DELETE_PACKAGES_TRACKING_ACTION); + + if (this.itemToBeDeleted) { + this.track(DELETE_PACKAGE_TRACKING_ACTION); + } else { + this.track(DELETE_PACKAGES_TRACKING_ACTION); + } + this.itemsToBeDeleted = []; }, deleteItemsCanceled() { - this.track(CANCEL_DELETE_PACKAGES_TRACKING_ACTION); + if (this.itemToBeDeleted) { + this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION); + } else { + this.track(CANCEL_DELETE_PACKAGES_TRACKING_ACTION); + } this.itemsToBeDeleted = []; }, - deleteItemConfirmation() { - this.$emit('delete', [this.itemToBeDeleted]); - this.track(DELETE_PACKAGE_TRACKING_ACTION); - this.itemToBeDeleted = null; - }, - deleteItemCanceled() { - this.track(CANCEL_DELETE_PACKAGE_TRACKING_ACTION); - this.itemToBeDeleted = null; - }, showConfirmationModal() { - this.setItemToBeDeleted(this.errorPackages[0]); + this.setItemsToBeDeleted([this.errorPackages[0]]); }, }, i18n: { @@ -165,21 +188,16 @@ export default { :first="first" :package-entity="item" :selected="isSelected(item)" - @delete="setItemToBeDeleted(item)" + @delete="setItemsToBeDeleted([item])" @select="selectItem(item)" /> </template> </registry-list> - <delete-package-modal - :item-to-be-deleted="itemToBeDeleted" - @ok="deleteItemConfirmation" - @cancel="deleteItemCanceled" - /> - <delete-modal ref="deletePackagesModal" :items-to-be-deleted="itemsToBeDeleted" + :show-request-forwarding-content="isRequestForwardingEnabled" @confirm="deleteItemsConfirmation" @cancel="deleteItemsCanceled" /> 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 d979ae5c08c..b4276d69ed6 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js @@ -27,15 +27,8 @@ export const PACKAGE_TYPE_DEBIAN = 'DEBIAN'; export const PACKAGE_TYPE_HELM = 'HELM'; export const TRACKING_LABEL_CODE_INSTRUCTION = 'code_instruction'; -export const TRACKING_LABEL_CONAN_INSTALLATION = 'conan_installation'; export const TRACKING_LABEL_MAVEN_INSTALLATION = 'maven_installation'; -export const TRACKING_LABEL_NPM_INSTALLATION = 'npm_installation'; -export const TRACKING_LABEL_NUGET_INSTALLATION = 'nuget_installation'; -export const TRACKING_LABEL_PYPI_INSTALLATION = 'pypi_installation'; -export const TRACKING_LABEL_COMPOSER_INSTALLATION = 'composer_installation'; - -export const TRACKING_ACTION_INSTALLATION = 'installation'; -export const TRACKING_ACTION_REGISTRY_SETUP = 'registry_setup'; +export const MAVEN_INSTALLATION_COMMAND = 'mvn install'; export const TRACKING_ACTION_COPY_CONAN_COMMAND = 'copy_conan_command'; export const TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND = 'copy_conan_setup_command'; @@ -68,7 +61,6 @@ export const TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND = export const TRACKING_LABEL_PACKAGE_ASSET = 'package_assets'; -export const TRACKING_ACTION_DOWNLOAD_PACKAGE_ASSET = 'download_package_asset'; export const TRACKING_ACTION_EXPAND_PACKAGE_ASSET = 'expand_package_asset'; export const TRACKING_ACTION_COPY_PACKAGE_ASSET_SHA = 'copy_package_asset_sha'; @@ -119,6 +111,14 @@ export const DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'delete_package_versions' export const REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'request_delete_package_versions'; export const CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION = 'cancel_delete_package_versions'; +export const DELETE_PACKAGE_VERSION_TRACKING_ACTION = 'delete_package_version'; +export const REQUEST_DELETE_PACKAGE_VERSION_TRACKING_ACTION = 'request_delete_package_version'; +export const CANCEL_DELETE_PACKAGE_VERSION_TRACKING_ACTION = 'cancel_delete_package_version'; + +export const FETCH_PACKAGE_VERSIONS_ERROR_MESSAGE = s__( + 'PackageRegistry|Failed to load version data', +); + export const DELETE_PACKAGES_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while deleting packages.', ); @@ -126,7 +126,23 @@ export const DELETE_PACKAGES_SUCCESS_MESSAGE = s__('PackageRegistry|Packages del export const DELETE_PACKAGES_MODAL_TITLE = s__('PackageRegistry|Delete packages'); export const DELETE_PACKAGE_MODAL_PRIMARY_ACTION = s__('PackageRegistry|Permanently delete'); +export const DELETE_PACKAGES_MODAL_DESCRIPTION = s__( + 'PackageRegistry|You are about to delete %{count} packages. This operation is irreversible.', +); +export const DELETE_PACKAGE_WITH_REQUEST_FORWARDING_PRIMARY_ACTION = s__( + 'PackageRegistry|Yes, delete package', +); +export const DELETE_PACKAGES_WITH_REQUEST_FORWARDING_PRIMARY_ACTION = s__( + 'PackageRegistry|Yes, delete selected packages', +); +export const DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT = s__( + 'PackageRegistry|Deleting this package while request forwarding is enabled for the project can pose a security risk. Do you want to delete %{name} version %{version} anyway? %{docLinkStart}What are the risks?%{docLinkEnd}', +); +export const DELETE_PACKAGES_REQUEST_FORWARDING_MODAL_CONTENT = s__( + 'PackageRegistry|Some of the selected package formats allow request forwarding. Deleting a package while request forwarding is enabled for the project can pose a security risk. Do you want to proceed with deleting the selected packages? %{docLinkStart}What are the risks?%{docLinkEnd}', +); +export const DELETE_PACKAGE_TEXT = s__('PackageRegistry|Delete package'); export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully'); export const DELETE_PACKAGE_ERROR_MESSAGE = s__( 'PackageRegistry|Something went wrong while deleting the package.', @@ -142,8 +158,6 @@ export const PACKAGE_REGISTRY_TITLE = __('Package Registry'); export const PACKAGE_ERROR_STATUS = 'ERROR'; export const PACKAGE_DEFAULT_STATUS = 'DEFAULT'; -export const PACKAGE_HIDDEN_STATUS = 'HIDDEN'; -export const PACKAGE_PROCESSING_STATUS = 'PROCESSING'; export const NPM_PACKAGE_MANAGER = 'npm'; export const YARN_PACKAGE_MANAGER = 'yarn'; @@ -151,8 +165,6 @@ export const YARN_PACKAGE_MANAGER = 'yarn'; export const PROJECT_PACKAGE_ENDPOINT_TYPE = 'project'; export const INSTANCE_PACKAGE_ENDPOINT_TYPE = 'instance'; -export const PROJECT_RESOURCE_TYPE = 'project'; -export const GROUP_RESOURCE_TYPE = 'group'; export const GRAPHQL_PAGE_SIZE = 20; export const LIST_KEY_NAME = 'name'; @@ -214,5 +226,9 @@ export const NUGET_HELP_PATH = helpPagePath('user/packages/nuget_repository/inde export const PYPI_HELP_PATH = helpPagePath('user/packages/pypi_repository/index'); export const COMPOSER_HELP_PATH = helpPagePath('user/packages/composer_repository/index'); export const PERSONAL_ACCESS_TOKEN_HELP_URL = helpPagePath('user/profile/personal_access_tokens'); +export const REQUEST_FORWARDING_HELP_PAGE_PATH = helpPagePath( + 'user/packages/package_registry/supported_functionality', + { anchor: 'deleting-packages' }, +); export const GRAPHQL_PACKAGE_PIPELINES_PAGE_SIZE = 10; 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 2d405f3e9cc..bcd90b7bee5 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 @@ -12,7 +12,7 @@ fragment PackageData on Package { name } } - pipelines(last: 1) { + pipelines(first: 1) { nodes { id sha diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_group_settings.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_group_settings.fragment.graphql new file mode 100644 index 00000000000..db05f497b7f --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_group_settings.fragment.graphql @@ -0,0 +1,8 @@ +fragment GroupPackageSettings on Group { + id + packageSettings { + mavenPackageRequestsForwarding + npmPackageRequestsForwarding + pypiPackageRequestsForwarding + } +} diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js index 56f95fa2c1f..39e5da54509 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/index.js @@ -4,6 +4,27 @@ import createDefaultClient from '~/lib/graphql'; Vue.use(VueApollo); +export const mergeVariables = (existing, incoming) => { + if (!incoming) return existing; + return incoming; +}; + export const apolloProvider = new VueApollo({ - defaultClient: createDefaultClient(), + defaultClient: createDefaultClient( + {}, + { + cacheConfig: { + typePolicies: { + PackageDetailsType: { + fields: { + versions: { + keyArgs: false, + merge: mergeVariables, + }, + }, + }, + }, + }, + }, + ), }); 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 109d535469b..984996b829a 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 @@ -1,10 +1,6 @@ -query getPackageDetails( - $id: PackagesPackageID! - $first: Int - $last: Int - $after: String - $before: String -) { +#import "~/packages_and_registries/package_registry/graphql/fragments/package_group_settings.fragment.graphql" + +query getPackageDetails($id: PackagesPackageID!) { package(id: $id) { id name @@ -15,6 +11,7 @@ query getPackageDetails( updatedAt status canDestroy + publicPackage npmUrl mavenUrl conanUrl @@ -28,6 +25,9 @@ query getPackageDetails( path name fullPath + group { + ...GroupPackageSettings + } } tags(first: 10) { nodes { @@ -61,31 +61,8 @@ query getPackageDetails( downloadPath } } - versions(after: $after, before: $before, first: $first, last: $last) { + versions { count - nodes { - id - name - canDestroy - createdAt - version - status - _links { - webPath - } - tags(first: 1) { - nodes { - id - name - } - } - } - pageInfo { - hasNextPage - hasPreviousPage - endCursor - startCursor - } } dependencyLinks { nodes { diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql new file mode 100644 index 00000000000..a4119ac5821 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql @@ -0,0 +1,38 @@ +query getPackageVersions( + $id: PackagesPackageID! + $first: Int + $last: Int + $after: String + $before: String +) { + package(id: $id) { + id + versions(after: $after, before: $before, first: $first, last: $last) { + count + nodes { + id + name + canDestroy + createdAt + packageType + version + status + _links { + webPath + } + tags(first: 1) { + nodes { + id + name + } + } + } + pageInfo { + hasNextPage + hasPreviousPage + endCursor + startCursor + } + } + } +} 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 5bde5f08e56..f25f24cbc5f 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 @@ -1,4 +1,5 @@ #import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql" +#import "~/packages_and_registries/package_registry/graphql/fragments/package_group_settings.fragment.graphql" #import "~/graphql_shared/fragments/page_info.fragment.graphql" query getPackages( @@ -32,6 +33,9 @@ query getPackages( ...PageInfo } } + group { + ...GroupPackageSettings + } } group(fullPath: $fullPath) @include(if: $isGroupPage) { id @@ -52,5 +56,6 @@ query getPackages( ...PageInfo } } + ...GroupPackageSettings } } diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js index 15ed98122a0..e2f8d239bae 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/index.js +++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js @@ -19,6 +19,7 @@ export default () => { npmInstanceUrl, projectListUrl, groupListUrl, + settingsPath, } = el.dataset; const isGroupPage = pageType === 'groups'; @@ -48,6 +49,7 @@ export default () => { projectListUrl, groupListUrl, breadCrumbState, + settingsPath, }, render(createElement) { return createElement(PackageRegistry); diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue index 4591c2eca87..6d4979ac785 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue @@ -2,6 +2,7 @@ import { GlBadge, GlButton, + GlLink, GlModal, GlModalDirective, GlTooltipDirective, @@ -10,7 +11,7 @@ import { GlTabs, GlSprintf, } from '@gitlab/ui'; -import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash'; +import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/alert'; import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants'; import { convertToGraphQLId } from '~/graphql_shared/utils'; import { numberToHumanSize } from '~/lib/utils/number_utils'; @@ -37,6 +38,7 @@ import { DELETE_PACKAGE_FILE_TRACKING_ACTION, DELETE_PACKAGE_FILES_TRACKING_ACTION, REQUEST_DELETE_PACKAGE_FILE_TRACKING_ACTION, + REQUEST_FORWARDING_HELP_PAGE_PATH, CANCEL_DELETE_PACKAGE_FILE_TRACKING_ACTION, SHOW_DELETE_SUCCESS_ALERT, FETCH_PACKAGE_DETAILS_ERROR_MESSAGE, @@ -44,6 +46,7 @@ import { DELETE_PACKAGE_FILE_SUCCESS_MESSAGE, DELETE_PACKAGE_FILES_ERROR_MESSAGE, DELETE_PACKAGE_FILES_SUCCESS_MESSAGE, + DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT, DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION, DELETE_MODAL_TITLE, DELETE_MODAL_CONTENT, @@ -54,6 +57,7 @@ import { import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql'; import getPackageDetails from '~/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql'; +import getPackageVersionsQuery from '~/packages_and_registries/package_registry/graphql/queries/get_package_versions.query.graphql'; import Tracking from '~/tracking'; export default { @@ -63,6 +67,7 @@ export default { GlButton, GlEmptyState, GlModal, + GlLink, GlTab, GlTabs, GlSprintf, @@ -123,6 +128,11 @@ export default { }, }, computed: { + deleteModalContent() { + return this.isRequestForwardingEnabled + ? DELETE_PACKAGE_REQUEST_FORWARDING_MODAL_CONTENT + : this.deletePackageModalContent; + }, projectName() { return this.packageEntity.project?.name; }, @@ -135,7 +145,6 @@ export default { queryVariables() { return { id: convertToGraphQLId(TYPENAME_PACKAGES_PACKAGE, this.packageId), - first: GRAPHQL_PAGE_SIZE, }; }, packageFiles() { @@ -147,9 +156,6 @@ export default { isLoading() { return this.$apollo.queries.packageEntity.loading; }, - isVersionsLoading() { - return this.isLoading || this.versionsMutationLoading; - }, packageFilesLoading() { return this.isLoading || this.mutationLoading; }, @@ -161,18 +167,24 @@ export default { category: packageTypeToTrackCategory(this.packageType), }; }, - versionPageInfo() { - return this.packageEntity?.versions?.pageInfo ?? {}; - }, packageDependencies() { return this.packageEntity.dependencyLinks?.nodes || []; }, + packageVersionsCount() { + return this.packageEntity.versions?.count ?? 0; + }, showDependencies() { return this.packageType === PACKAGE_TYPE_NUGET; }, showFiles() { return this.packageType !== PACKAGE_TYPE_COMPOSER; }, + groupSettings() { + return this.packageEntity.project?.group?.packageSettings ?? {}; + }, + isRequestForwardingEnabled() { + return this.groupSettings[`${this.packageType.toLowerCase()}PackageRequestsForwarding`]; + }, showMetadata() { return [ PACKAGE_TYPE_COMPOSER, @@ -190,6 +202,17 @@ export default { }, ]; }, + refetchVersionsQueryData() { + return [ + { + query: getPackageVersionsQuery, + variables: { + id: this.queryVariables.id, + first: GRAPHQL_PAGE_SIZE, + }, + }, + ]; + }, }, methods: { formatSize(size) { @@ -274,34 +297,6 @@ export default { resetDeleteModalContent() { this.deletePackageModalContent = DELETE_MODAL_CONTENT; }, - updateQuery(_, { fetchMoreResult }) { - return fetchMoreResult; - }, - fetchPreviousVersionsPage() { - const variables = { - ...this.queryVariables, - first: null, - last: GRAPHQL_PAGE_SIZE, - before: this.versionPageInfo?.startCursor, - }; - this.$apollo.queries.packageEntity.fetchMore({ - variables, - updateQuery: this.updateQuery, - }); - }, - fetchNextVersionsPage() { - const variables = { - ...this.queryVariables, - first: GRAPHQL_PAGE_SIZE, - last: null, - after: this.versionPageInfo?.endCursor, - }; - - this.$apollo.queries.packageEntity.fetchMore({ - variables, - updateQuery: this.updateQuery, - }); - }, }, i18n: { DELETE_MODAL_TITLE, @@ -311,22 +306,25 @@ export default { ), otherVersionsTabTitle: s__('PackageRegistry|Other versions'), }, + links: { + REQUEST_FORWARDING_HELP_PAGE_PATH, + }, modal: { packageDeletePrimaryAction: { text: s__('PackageRegistry|Permanently delete'), - attributes: [ - { variant: 'danger' }, - { category: 'primary' }, - { 'data-qa-selector': 'delete_modal_button' }, - ], + attributes: { + variant: 'danger', + category: 'primary', + 'data-qa-selector': 'delete_modal_button', + }, }, fileDeletePrimaryAction: { text: __('Delete'), - attributes: [{ variant: 'danger' }, { category: 'primary' }], + attributes: { variant: 'danger', category: 'primary' }, }, filesDeletePrimaryAction: { text: s__('PackageRegistry|Permanently delete assets'), - attributes: [{ variant: 'danger' }, { category: 'primary' }], + attributes: { variant: 'danger', category: 'primary' }, }, cancelAction: { text: __('Cancel'), @@ -403,12 +401,12 @@ export default { <template #title> <span>{{ $options.i18n.otherVersionsTabTitle }}</span> <gl-badge size="sm" class="gl-tab-counter-badge" data-testid="other-versions-badge">{{ - packageEntity.versions.count + packageVersionsCount }}</gl-badge> </template> <delete-packages - :refetch-queries="refetchQueriesData" + :refetch-queries="refetchVersionsQueryData" show-success-alert @start="versionsMutationLoading = true" @end="versionsMutationLoading = false" @@ -416,12 +414,11 @@ export default { <template #default="{ deletePackages }"> <package-versions-list :can-destroy="packageEntity.canDestroy" - :is-loading="isVersionsLoading" - :page-info="versionPageInfo" - :versions="packageEntity.versions.nodes" + :count="packageVersionsCount" + :is-mutation-loading="versionsMutationLoading" + :is-request-forwarding-enabled="isRequestForwardingEnabled" + :package-id="packageEntity.id" @delete="deletePackages" - @prev-page="fetchPreviousVersionsPage" - @next-page="fetchNextVersionsPage" > <template #empty-state> <p class="gl-mt-3" data-testid="no-versions-message"> @@ -451,15 +448,23 @@ export default { @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)" > <template #modal-title>{{ $options.i18n.DELETE_MODAL_TITLE }}</template> - <gl-sprintf :message="deletePackageModalContent"> - <template #version> - <strong>{{ packageEntity.version }}</strong> - </template> + <p> + <gl-sprintf :message="deleteModalContent"> + <template v-if="isRequestForwardingEnabled" #docLink="{ content }"> + <gl-link :href="$options.links.REQUEST_FORWARDING_HELP_PAGE_PATH">{{ + content + }}</gl-link> + </template> - <template #name> - <strong>{{ packageEntity.name }}</strong> - </template> - </gl-sprintf> + <template #version> + <strong>{{ packageEntity.version }}</strong> + </template> + + <template #name> + <strong>{{ packageEntity.name }}</strong> + </template> + </gl-sprintf> + </p> </gl-modal> </template> </delete-packages> diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue index 31c76c95e45..044ce4e6413 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue @@ -1,12 +1,11 @@ <script> -import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui'; -import { createAlert, VARIANT_INFO } from '~/flash'; +import { GlButton, GlEmptyState, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui'; +import { createAlert, VARIANT_INFO } from '~/alert'; +import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants'; import { historyReplaceState } from '~/lib/utils/common_utils'; import { s__ } from '~/locale'; 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, @@ -20,6 +19,7 @@ import PackageList from '~/packages_and_registries/package_registry/components/l export default { components: { + GlButton, GlEmptyState, GlLink, GlSprintf, @@ -28,23 +28,26 @@ export default { PackageSearch, DeletePackages, }, - inject: ['emptyListIllustration', 'isGroupPage', 'fullPath'], + directives: { + GlTooltip: GlTooltipDirective, + }, + inject: ['emptyListIllustration', 'isGroupPage', 'fullPath', 'settingsPath'], data() { return { - packages: {}, + packagesResource: {}, sort: '', filters: {}, mutationLoading: false, }; }, apollo: { - packages: { + packagesResource: { query: getPackagesQuery, variables() { return this.queryVariables; }, update(data) { - return data[this.graphqlResource].packages; + return data[this.graphqlResource] ?? {}; }, skip() { return !this.sort; @@ -52,6 +55,14 @@ export default { }, }, computed: { + packages() { + return this.packagesResource?.packages ?? {}; + }, + groupSettings() { + return this.isGroupPage + ? this.packagesResource?.packageSettings ?? {} + : this.packagesResource?.group?.packageSettings ?? {}; + }, queryVariables() { return { isGroupPage: this.isGroupPage, @@ -64,7 +75,7 @@ export default { }; }, graphqlResource() { - return this.isGroupPage ? GROUP_RESOURCE_TYPE : PROJECT_RESOURCE_TYPE; + return this.isGroupPage ? WORKSPACE_GROUP : WORKSPACE_PROJECT; }, pageInfo() { return this.packages?.pageInfo ?? {}; @@ -84,7 +95,7 @@ export default { : this.$options.i18n.noResultsTitle; }, isLoading() { - return this.$apollo.queries.packages.loading || this.mutationLoading; + return this.$apollo.queries.packagesResource.loading || this.mutationLoading; }, refetchQueriesData() { return [ @@ -124,7 +135,7 @@ export default { after: this.pageInfo?.endCursor, }; - this.$apollo.queries.packages.fetchMore({ + this.$apollo.queries.packagesResource.fetchMore({ variables, updateQuery: this.updateQuery, }); @@ -137,7 +148,7 @@ export default { before: this.pageInfo?.startCursor, }; - this.$apollo.queries.packages.fetchMore({ + this.$apollo.queries.packagesResource.fetchMore({ variables, updateQuery: this.updateQuery, }); @@ -150,6 +161,7 @@ export default { noResultsText: s__( 'PackageRegistry|Learn how to %{noPackagesLinkStart}publish and share your packages%{noPackagesLinkEnd} with GitLab.', ), + settingsText: s__('PackageRegistry|Configure in settings'), }, links: { EMPTY_LIST_HELP_URL, @@ -160,7 +172,16 @@ export default { <template> <div> - <package-title :help-url="$options.links.PACKAGE_HELP_URL" :count="packagesCount" /> + <package-title :help-url="$options.links.PACKAGE_HELP_URL" :count="packagesCount"> + <template v-if="settingsPath" #settings-link> + <gl-button + v-gl-tooltip="$options.i18n.settingsText" + icon="settings" + :href="settingsPath" + :aria-label="$options.i18n.settingsText" + /> + </template> + </package-title> <package-search class="gl-mb-5" @update="handleSearchUpdate" /> <delete-packages @@ -171,6 +192,7 @@ export default { > <template #default="{ deletePackages }"> <package-list + :group-settings="groupSettings" :list="packages.nodes" :is-loading="isLoading" :page-info="pageInfo" 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 36eb65c623b..4c25c0f97de 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 @@ -72,7 +72,7 @@ export default { </script> <template> - <div> + <div data-testid="packages-and-registries-group-settings"> <gl-alert v-if="alertMessage" variant="warning" class="gl-mt-4" @dismiss="dismissAlert"> {{ alertMessage }} </gl-alert> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue index b7d7f0aaca7..ab88d9e8936 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue +++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_forwarding_settings.vue @@ -1,12 +1,14 @@ <script> -import { GlButton } from '@gitlab/ui'; +import { GlButton, GlLink, GlSprintf } from '@gitlab/ui'; import { isEqual } from 'lodash'; import { + PACKAGE_FORWARDING_SECURITY_DESCRIPTION, PACKAGE_FORWARDING_SETTINGS_HEADER, PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, PACKAGE_FORWARDING_FORM_BUTTON, PACKAGE_FORWARDING_FIELDS, MAVEN_FORWARDING_FIELDS, + REQUEST_FORWARDING_HELP_PAGE_PATH, } from '~/packages_and_registries/settings/group/constants'; import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql'; @@ -20,12 +22,15 @@ export default { name: 'PackageForwardingSettings', i18n: { PACKAGE_FORWARDING_FORM_BUTTON, + PACKAGE_FORWARDING_SECURITY_DESCRIPTION, PACKAGE_FORWARDING_SETTINGS_HEADER, PACKAGE_FORWARDING_SETTINGS_DESCRIPTION, }, components: { ForwardingSettings, GlButton, + GlLink, + GlSprintf, SettingsBlock, }, mixins: [glFeatureFlagsMixin()], @@ -150,6 +155,9 @@ export default { this.$set(this.workingCopy, type, value); }, }, + links: { + REQUEST_FORWARDING_HELP_PAGE_PATH, + }, }; </script> @@ -157,9 +165,14 @@ export default { <settings-block> <template #title> {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_HEADER }}</template> <template #description> - <span data-testid="description"> + <span class="gl-display-block gl-mb-2" data-testid="description"> {{ $options.i18n.PACKAGE_FORWARDING_SETTINGS_DESCRIPTION }} </span> + <gl-sprintf :message="$options.i18n.PACKAGE_FORWARDING_SECURITY_DESCRIPTION"> + <template #docLink="{ content }"> + <gl-link :href="$options.links.REQUEST_FORWARDING_HELP_PAGE_PATH">{{ content }}</gl-link> + </template> + </gl-sprintf> </template> <template #default> <form @submit.prevent="submit"> diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js index c93cd7f7d78..fa73c01c5c4 100644 --- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js @@ -17,6 +17,9 @@ export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__( 'PackageRegistry|Publish packages if their name or version matches this regex.', ); +export const PACKAGE_FORWARDING_SECURITY_DESCRIPTION = s__( + 'PackageRegistry|There are security risks if packages are deleted while request forwarding is enabled. %{docLinkStart}What are the risks?%{docLinkEnd}', +); export const PACKAGE_FORWARDING_SETTINGS_HEADER = s__('PackageRegistry|Package forwarding'); export const PACKAGE_FORWARDING_SETTINGS_DESCRIPTION = s__( 'PackageRegistry|Forward package requests to a public registry if the packages are not found in the GitLab package registry.', @@ -78,8 +81,8 @@ export const MAVEN_FORWARDING_FIELDS = { // Parameters -export const PACKAGES_DOCS_PATH = helpPagePath('user/packages/index'); -export const MAVEN_DUPLICATES_ALLOWED = 'mavenDuplicatesAllowed'; -export const MAVEN_DUPLICATE_EXCEPTION_REGEX = 'mavenDuplicateExceptionRegex'; - export const DEPENDENCY_PROXY_DOCS_PATH = helpPagePath('user/packages/dependency_proxy/index'); +export const REQUEST_FORWARDING_HELP_PAGE_PATH = helpPagePath( + 'user/packages/package_registry/supported_functionality', + { anchor: 'deleting-packages' }, +); diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue index 11d8732426d..c13d49b5379 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue @@ -225,9 +225,6 @@ export default { <template #strong="{ content }"> <strong>{{ content }}</strong> </template> - <template #secondStrong="{ content }"> - <strong>{{ content }}</strong> - </template> </gl-sprintf> </p> <expiration-dropdown @@ -264,9 +261,6 @@ export default { <template #strong="{ content }"> <strong>{{ content }}</strong> </template> - <template #secondStrong="{ content }"> - <strong>{{ content }}</strong> - </template> </gl-sprintf> </p> <expiration-dropdown @@ -312,7 +306,7 @@ export default { > {{ __('Cancel') }} </gl-button> - <span class="gl-font-style-italic gl-text-gray-400">{{ + <span class="gl-font-style-italic gl-text-gray-500">{{ $options.i18n.EXPIRATION_POLICY_FOOTER_NOTE }}</span> </div> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue index f06e3a41bd0..0bbb501011a 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_dropdown.vue @@ -64,7 +64,7 @@ export default { </gl-form-select> </div> <template v-if="description" #description> - <span data-testid="description" class="gl-text-gray-400"> + <span data-testid="description" class="gl-text-gray-500"> {{ description }} </span> </template> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue index 3fbbfd75ffb..749650e1060 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_input.vue @@ -101,7 +101,7 @@ export default { trim /> <template #description> - <span data-testid="description" class="gl-text-gray-400"> + <span data-testid="description" class="gl-text-gray-500"> <gl-sprintf :message="description"> <template #link="{ content }"> <gl-link :href="tagsRegexHelpPagePath">{{ content }}</gl-link> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue index 7a9ea7c0bf7..35fc0910a16 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/expiration_toggle.vue @@ -8,7 +8,7 @@ import { export default { i18n: { - toggleLabel: s__('ContainerRegistry|Enable expiration policy'), + toggleLabel: s__('ContainerRegistry|Enable cleanup policy'), }, components: { GlFormGroup, diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue index 2c1368262f2..4cc9cc190e8 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue +++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue @@ -42,7 +42,7 @@ export default { </script> <template> - <div> + <div data-testid="packages-and-registries-project-settings"> <gl-alert v-if="showAlert" variant="success" diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js index 731fb3e4c45..05616a0a4f6 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js @@ -1,6 +1,6 @@ import { s__, __ } from '~/locale'; -export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up image tags`); +export const CONTAINER_CLEANUP_POLICY_TITLE = s__('ContainerRegistry|Cleanup policies'); export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__( `ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`, ); @@ -29,7 +29,7 @@ export const TEXT_AREA_INVALID_FEEDBACK = s__( export const KEEP_HEADER_TEXT = s__('ContainerRegistry|Keep these tags'); export const KEEP_INFO_TEXT = s__( - 'ContainerRegistry|Tags that match these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{secondStrongStart}latest%{secondStrongEnd} tag is always kept.', + 'ContainerRegistry|Tags that match %{strongStart}any of%{strongEnd} these rules are %{strongStart}kept%{strongEnd}, even if they match a removal rule below. The %{strongStart}latest%{strongEnd} tag is always kept.', ); export const KEEP_N_LABEL = s__('ContainerRegistry|Keep the most recent:'); export const NAME_REGEX_KEEP_LABEL = s__('ContainerRegistry|Keep tags matching:'); 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 deleted file mode 100644 index 105f7bbe132..00000000000 --- a/app/assets/javascripts/packages_and_registries/shared/components/package_icon_and_name.vue +++ /dev/null @@ -1,17 +0,0 @@ -<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/registry_list.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue index 7485f8282ee..1c8f80972df 100644 --- a/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue +++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue @@ -125,7 +125,7 @@ export default { :select-item="selectItem" :is-selected="isSelected" :item="item" - :first="index === 0" + :first="!hiddenDelete && index === 0" ></slot> </div> diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js index 76623377d90..adffab277cc 100644 --- a/app/assets/javascripts/packages_and_registries/shared/utils.js +++ b/app/assets/javascripts/packages_and_registries/shared/utils.js @@ -55,15 +55,6 @@ export const renderBreadcrumb = (router, apolloProvider, RegistryBreadcrumb) => RegistryBreadcrumb, }, render(createElement) { - // FIXME(@tnir): this is a workaround until the MR gets merged: - // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115 - const parentEl = breadCrumbEl.parentElement.parentElement; - if (parentEl) { - parentEl.classList.remove('breadcrumbs-container'); - parentEl.classList.add('gl-display-flex'); - parentEl.classList.add('w-100'); - } - // End of FIXME(@tnir) return createElement('registry-breadcrumb', { class: breadCrumbEl.className, props: { |