diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 13:00:54 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-04-20 13:00:54 +0300 |
commit | 3cccd102ba543e02725d247893729e5c73b38295 (patch) | |
tree | f36a04ec38517f5deaaacb5acc7d949688d1e187 /app/assets/javascripts/packages_and_registries | |
parent | 205943281328046ef7b4528031b90fbda70c75ac (diff) |
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
30 files changed, 961 insertions, 104 deletions
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue index e4a1a1a8266..bb1dac40b92 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue @@ -1,13 +1,13 @@ <script> -import { GlTooltipDirective, GlButton } from '@gitlab/ui'; +import { GlButton, GlLink, GlTooltip, GlSprintf } from '@gitlab/ui'; export default { name: 'DeleteButton', components: { GlButton, - }, - directives: { - GlTooltip: GlTooltipDirective, + GlLink, + GlTooltip, + GlSprintf, }, props: { title: { @@ -18,6 +18,11 @@ export default { type: String, required: true, }, + tooltipLink: { + type: String, + default: '', + required: false, + }, disabled: { type: Boolean, default: false, @@ -29,21 +34,12 @@ export default { required: false, }, }, - computed: { - tooltipConfiguration() { - return { - disabled: this.tooltipDisabled, - title: this.tooltipTitle, - }; - }, - }, }; </script> <template> - <div v-gl-tooltip="tooltipConfiguration"> + <div ref="deleteImageButton"> <gl-button - v-gl-tooltip :disabled="disabled" :title="title" :aria-label="title" @@ -52,5 +48,14 @@ export default { icon="remove" @click="$emit('delete')" /> + <gl-tooltip :target="() => $refs.deleteImageButton" :disabled="tooltipDisabled" placement="top"> + <gl-sprintf :message="tooltipTitle"> + <template #docLink="{ content }"> + <gl-link v-if="tooltipLink" :href="tooltipLink" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </gl-tooltip> </div> </template> 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 c1ec523574a..484903354e8 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 @@ -8,11 +8,13 @@ import ListItem from '~/vue_shared/components/registry/list_item.vue'; import { ASYNC_DELETE_IMAGE_ERROR_MESSAGE, LIST_DELETE_BUTTON_DISABLED, + LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION, REMOVE_REPOSITORY_LABEL, ROW_SCHEDULED_FOR_DELETION, CLEANUP_TIMED_OUT_ERROR_MESSAGE, IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_FAILED_DELETED_STATUS, + IMAGE_MIGRATING_STATE, ROOT_IMAGE_TEXT, } from '../../constants/index'; import DeleteButton from '../delete_button.vue'; @@ -32,6 +34,7 @@ export default { directives: { GlTooltip: GlTooltipDirective, }, + inject: ['config'], props: { item: { type: Object, @@ -44,13 +47,12 @@ export default { }, }, i18n: { - LIST_DELETE_BUTTON_DISABLED, REMOVE_REPOSITORY_LABEL, ROW_SCHEDULED_FOR_DELETION, }, computed: { disabledDelete() { - return !this.item.canDelete || this.deleting; + return !this.item.canDelete || this.deleting || this.migrating; }, id() { return getIdFromGraphQLId(this.item.id); @@ -58,6 +60,9 @@ export default { deleting() { return this.item.status === IMAGE_DELETE_SCHEDULED_STATUS; }, + migrating() { + return this.item.migrationState === IMAGE_MIGRATING_STATE; + }, failedDelete() { return this.item.status === IMAGE_FAILED_DELETED_STATUS; }, @@ -83,6 +88,11 @@ export default { routerLinkEvent() { return this.deleting ? '' : 'click'; }, + deleteButtonTooltipTitle() { + return this.migrating + ? LIST_DELETE_BUTTON_DISABLED_FOR_MIGRATION + : LIST_DELETE_BUTTON_DISABLED; + }, }, }; </script> @@ -144,8 +154,9 @@ export default { <delete-button :title="$options.i18n.REMOVE_REPOSITORY_LABEL" :disabled="disabledDelete" - :tooltip-disabled="item.canDelete" - :tooltip-title="$options.i18n.LIST_DELETE_BUTTON_DISABLED" + :tooltip-disabled="!disabledDelete" + :tooltip-link="config.containerRegistryImportingHelpPagePath" + :tooltip-title="deleteButtonTooltipTitle" @delete="$emit('delete', item)" /> </template> diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue index 6d2ff9ea7b6..154e176dc6e 100644 --- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue +++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue @@ -1,4 +1,5 @@ <script> +import { GlLink } from '@gitlab/ui'; import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility'; import { n__, sprintf } from '~/locale'; import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; @@ -9,6 +10,7 @@ import { LIST_INTRO_TEXT, EXPIRATION_POLICY_WILL_RUN_IN, EXPIRATION_POLICY_DISABLED_TEXT, + SET_UP_CLEANUP, } from '../../constants/index'; export default { @@ -16,6 +18,7 @@ export default { components: { TitleArea, MetadataItem, + GlLink, }, props: { expirationPolicy: { @@ -43,6 +46,16 @@ export default { required: false, default: false, }, + cleanupPoliciesSettingsPath: { + type: String, + default: '', + required: false, + }, + showCleanupPolicyLink: { + type: Boolean, + required: false, + default: false, + }, }, loader: { repeat: 10, @@ -51,6 +64,7 @@ export default { }, i18n: { CONTAINER_REGISTRY_TITLE, + SET_UP_CLEANUP, }, computed: { imagesCountText() { @@ -105,6 +119,9 @@ export default { :text="expirationPolicyText" size="xl" /> + <gl-link v-if="showCleanupPolicyLink" class="gl-ml-2" :href="cleanupPoliciesSettingsPath">{{ + $options.i18n.SET_UP_CLEANUP + }}</gl-link> </template> </title-area> </template> 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 40f9b09a982..e584da23edb 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 @@ -4,7 +4,7 @@ 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', + 'ContainerRegistry|Expiration policy is disabled.', ); export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted'); export const DELETE_ALERT_LINK_TEXT = s__( @@ -13,3 +13,4 @@ export const DELETE_ALERT_LINK_TEXT = s__( export const CLEANUP_TIMED_OUT_ERROR_MESSAGE = s__( 'ContainerRegistry|Cleanup timed out before it could delete all tags', ); +export const SET_UP_CLEANUP = s__('ContainerRegistry|Set up cleanup'); 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 7fa950ccfd0..c7022d6070f 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 @@ -14,6 +14,9 @@ export const LIST_INTRO_TEXT = s__( export const LIST_DELETE_BUTTON_DISABLED = s__( 'ContainerRegistry|Missing or insufficient permission, delete button disabled', ); +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.', @@ -45,6 +48,7 @@ export const EMPTY_RESULT_MESSAGE = s__( export const IMAGE_DELETE_SCHEDULED_STATUS = 'DELETE_SCHEDULED'; export const IMAGE_FAILED_DELETED_STATUS = 'DELETE_FAILED'; +export const IMAGE_MIGRATING_STATE = 'importing'; export const GRAPHQL_PAGE_SIZE = 10; export const SORT_FIELDS = [ 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 d753d33a02c..8c577cc7b17 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 @@ -1,4 +1,4 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "~/graphql_shared/fragments/page_info.fragment.graphql" query getContainerRepositoryTags( $id: ID! 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 ca5bd8d6964..a558550c91f 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 @@ -35,7 +35,7 @@ export default () => { expirationPolicy, isGroupPage, isAdmin, - showCleanupPolicyOnAlert, + showCleanupPolicyLink, showUnfinishedTagCleanupCallout, connectionError, invalidPathError, @@ -68,7 +68,7 @@ export default () => { expirationPolicy: expirationPolicy ? JSON.parse(expirationPolicy) : undefined, isGroupPage: parseBoolean(isGroupPage), isAdmin: parseBoolean(isAdmin), - showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), + showCleanupPolicyLink: parseBoolean(showCleanupPolicyLink), showUnfinishedTagCleanupCallout: parseBoolean(showUnfinishedTagCleanupCallout), connectionError: parseBoolean(connectionError), invalidPathError: parseBoolean(invalidPathError), 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 5f9e614bebb..d1cab406984 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 @@ -11,7 +11,6 @@ import { import { get } from 'lodash'; import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql'; import createFlash from '~/flash'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; import Tracking from '~/tracking'; import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; @@ -60,7 +59,6 @@ export default { GlSkeletonLoader, RegistryHeader, DeleteImage, - CleanupPolicyEnabledAlert, PersistedSearch, }, directives: { @@ -273,12 +271,6 @@ export default { </gl-sprintf> </gl-alert> - <cleanup-policy-enabled-alert - v-if="config.showCleanupPolicyOnAlert" - :project-path="config.projectPath" - :cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath" - /> - <gl-empty-state v-if="showConnectionError" :title="$options.i18n.CONNECTION_ERROR_TITLE" @@ -304,6 +296,8 @@ export default { :expiration-policy="config.expirationPolicy" :help-page-path="config.helpPagePath" :hide-expiration-policy-data="config.isGroupPage" + :cleanup-policies-settings-path="config.cleanupPoliciesSettingsPath" + :show-cleanup-policy-link="config.showCleanupPolicyLink" > <template #commands> <cli-commands 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 eb112238c11..67c2ca02d20 100644 --- a/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue +++ b/app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue @@ -1,13 +1,18 @@ <script> import { GlAlert, + GlDropdown, + GlDropdownItem, GlEmptyState, GlFormGroup, GlFormInputGroup, + GlModal, + GlModalDirective, GlSkeletonLoader, GlSprintf, } from '@gitlab/ui'; -import { s__ } from '~/locale'; +import { __, s__, n__, sprintf } from '~/locale'; +import Api from '~/api'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import TitleArea from '~/vue_shared/components/registry/title_area.vue'; import ManifestsList from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue'; @@ -22,16 +27,22 @@ import getDependencyProxyDetailsQuery from '~/packages_and_registries/dependency export default { components: { GlAlert, + GlDropdown, + GlDropdownItem, GlEmptyState, GlFormGroup, GlFormInputGroup, + GlModal, GlSkeletonLoader, GlSprintf, ClipboardButton, TitleArea, ManifestsList, }, - inject: ['groupPath', 'dependencyProxyAvailable', 'noManifestsIllustration'], + directives: { + GlModalDirective, + }, + inject: ['groupPath', 'groupId', 'dependencyProxyAvailable', 'noManifestsIllustration'], i18n: { proxyNotAvailableText: s__( 'DependencyProxy|Dependency Proxy feature is limited to public groups for now.', @@ -41,6 +52,20 @@ export default { blobCountAndSize: s__('DependencyProxy|Contains %{count} blobs of images (%{size})'), pageTitle: s__('DependencyProxy|Dependency Proxy'), noManifestTitle: s__('DependencyProxy|There are no images in the cache'), + deleteCacheAlertMessageSuccess: s__( + 'DependencyProxy|All items in the cache are scheduled for removal.', + ), + clearCache: s__('DependencyProxy|Clear cache'), + }, + confirmClearCacheModal: 'confirm-clear-cache-modal', + modalButtons: { + primary: { + text: s__('DependencyProxy|Clear cache'), + attributes: [{ variant: 'danger' }], + }, + secondary: { + text: __('Cancel'), + }, }, links: { DEPENDENCY_PROXY_DOCS_PATH, @@ -48,6 +73,8 @@ export default { data() { return { group: {}, + showDeleteCacheAlert: false, + deleteCacheAlertMessage: '', }; }, apollo: { @@ -80,6 +107,33 @@ export default { manifests() { return this.group.dependencyProxyManifests.nodes; }, + modalTitleWithCount() { + return sprintf( + n__( + 'Clear %{count} image from cache?', + 'Clear %{count} images from cache?', + this.group.dependencyProxyBlobCount, + ), + { + count: this.group.dependencyProxyBlobCount, + }, + ); + }, + modalConfirmationMessageWithCount() { + return sprintf( + n__( + 'You are about to clear %{count} image from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?', + 'You are about to clear %{count} images from the cache. Once you confirm, the next time a pipeline runs it must pull an image or tag from Docker Hub. Are you sure?', + this.group.dependencyProxyBlobCount, + ), + { + count: this.group.dependencyProxyBlobCount, + }, + ); + }, + showDeleteDropdown() { + return this.group.dependencyProxyBlobCount > 0; + }, }, methods: { fetchNextPage() { @@ -103,13 +157,47 @@ export default { }, }); }, + async submit() { + try { + await Api.deleteDependencyProxyCacheList(this.groupId); + + this.deleteCacheAlertMessage = this.$options.i18n.deleteCacheAlertMessageSuccess; + this.showDeleteCacheAlert = true; + } catch (err) { + this.deleteCacheAlertMessage = err; + this.showDeleteCacheAlert = true; + } + }, }, }; </script> <template> <div> - <title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages" /> + <gl-alert + v-if="showDeleteCacheAlert" + data-testid="delete-cache-alert" + @dismiss="showDeleteCacheAlert = false" + > + {{ deleteCacheAlertMessage }} + </gl-alert> + <title-area :title="$options.i18n.pageTitle" :info-messages="infoMessages"> + <template v-if="showDeleteDropdown" #right-actions> + <gl-dropdown + icon="ellipsis_v" + text="More actions" + :text-sr-only="true" + category="tertiary" + no-caret + > + <gl-dropdown-item + v-gl-modal-directive="$options.confirmClearCacheModal" + variant="danger" + >{{ $options.i18n.clearCache }}</gl-dropdown-item + > + </gl-dropdown> + </template> + </title-area> <gl-alert v-if="!dependencyProxyAvailable" :dismissible="false" @@ -159,5 +247,15 @@ export default { :title="$options.i18n.noManifestTitle" /> </div> + + <gl-modal + :modal-id="$options.confirmClearCacheModal" + :title="modalTitleWithCount" + :action-primary="$options.modalButtons.primary" + :action-secondary="$options.modalButtons.secondary" + @primary="submit" + > + {{ modalConfirmationMessageWithCount }} + </gl-modal> </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 9241dccb2d5..5c43b10a5e3 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 @@ -1,4 +1,4 @@ -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "~/graphql_shared/fragments/page_info.fragment.graphql" query getDependencyProxyDetails( $fullPath: ID! diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue new file mode 100644 index 00000000000..c1b5367c96a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue @@ -0,0 +1,42 @@ +<script> +import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue'; +import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue'; + +export default { + name: 'HarborList', + components: { + RegistryList, + HarborListRow, + }, + props: { + images: { + type: Array, + required: true, + }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, + pageInfo: { + type: Object, + required: true, + }, + }, +}; +</script> + +<template> + <registry-list + :items="images" + :hidden-delete="true" + :pagination="pageInfo" + id-property="name" + @prev-page="$emit('prev-page')" + @next-page="$emit('next-page')" + > + <template #default="{ item }"> + <harbor-list-row :item="item" :metadata-loading="metadataLoading" /> + </template> + </registry-list> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue new file mode 100644 index 00000000000..086b9c73d75 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue @@ -0,0 +1,67 @@ +<script> +import { sprintf } from '~/locale'; +import TitleArea from '~/vue_shared/components/registry/title_area.vue'; +import { + HARBOR_REGISTRY_TITLE, + LIST_INTRO_TEXT, + imagesCountInfoText, +} from '~/packages_and_registries/harbor_registry/constants'; +import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue'; + +export default { + name: 'HarborListHeader', + components: { + TitleArea, + MetadataItem, + }, + props: { + imagesCount: { + type: Number, + default: 0, + required: false, + }, + helpPagePath: { + type: String, + default: '', + required: false, + }, + metadataLoading: { + type: Boolean, + required: false, + default: false, + }, + }, + i18n: { + HARBOR_REGISTRY_TITLE, + }, + computed: { + imagesCountText() { + const pluralisedString = imagesCountInfoText(this.imagesCount); + return sprintf(pluralisedString, { count: this.imagesCount }); + }, + infoMessages() { + return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }]; + }, + }, +}; +</script> + +<template> + <title-area + :title="$options.i18n.HARBOR_REGISTRY_TITLE" + :info-messages="infoMessages" + :metadata-loading="metadataLoading" + > + <template #right-actions> + <slot name="commands"></slot> + </template> + <template #metadata-count> + <metadata-item + v-if="imagesCount" + data-testid="images-count" + icon="container-image" + :text="imagesCountText" + /> + </template> + </title-area> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue new file mode 100644 index 00000000000..258472fe16e --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue @@ -0,0 +1,84 @@ +<script> +import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui'; +import { n__ } from '~/locale'; + +import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; +import ListItem from '~/vue_shared/components/registry/list_item.vue'; + +export default { + name: 'HarborListRow', + components: { + ClipboardButton, + GlSprintf, + GlIcon, + ListItem, + GlSkeletonLoader, + }, + props: { + item: { + type: Object, + required: true, + }, + metadataLoading: { + type: Boolean, + default: false, + required: false, + }, + }, + computed: { + id() { + return this.item.id; + }, + artifactCountText() { + return n__( + 'HarborRegistry|%{count} Tag', + 'HarborRegistry|%{count} Tags', + this.item.artifactCount, + ); + }, + imageName() { + return this.item.name; + }, + }, +}; +</script> + +<template> + <list-item v-bind="$attrs"> + <template #left-primary> + <router-link + class="gl-text-body gl-font-weight-bold" + data-testid="details-link" + data-qa-selector="registry_image_content" + :to="{ name: 'details', params: { id } }" + > + {{ imageName }} + </router-link> + <clipboard-button + v-if="item.location" + :text="item.location" + :title="item.location" + category="tertiary" + /> + </template> + <template #left-secondary> + <template v-if="!metadataLoading"> + <span class="gl-display-flex gl-align-items-center" data-testid="tags-count"> + <gl-icon name="tag" class="gl-mr-2" /> + <gl-sprintf :message="artifactCountText"> + <template #count> + {{ item.artifactCount }} + </template> + </gl-sprintf> + </span> + </template> + + <div v-else class="gl-w-full"> + <gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet"> + <circle cx="6" cy="8" r="6" /> + <rect x="16" y="4" width="100" height="8" rx="4" /> + </gl-skeleton-loader> + </div> + </template> + </list-item> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js new file mode 100644 index 00000000000..a7891821755 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js @@ -0,0 +1,29 @@ +import { s__, __ } from '~/locale'; + +export const ROOT_IMAGE_TEXT = s__('HarborRegistry|Root image'); +export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') }; + +export const ASCENDING_ORDER = 'asc'; +export const DESCENDING_ORDER = 'desc'; + +export const NAME_SORT_FIELD_KEY = 'name'; +export const UPDATED_SORT_FIELD_KEY = 'update_time'; +export const CREATED_SORT_FIELD_KEY = 'creation_time'; + +export const SORT_FIELD_MAPPING = { + NAME: NAME_SORT_FIELD_KEY, + UPDATED: UPDATED_SORT_FIELD_KEY, + CREATED: CREATED_SORT_FIELD_KEY, +}; + +/* eslint-disable @gitlab/require-i18n-strings */ +export const dockerBuildCommand = (repositoryUrl) => { + return `docker build -t ${repositoryUrl} .`; +}; +export const dockerPushCommand = (repositoryUrl) => { + return `docker push ${repositoryUrl}`; +}; +export const dockerLoginCommand = (registryHostUrlWithPort) => { + return `docker login ${registryHostUrlWithPort}`; +}; +/* eslint-enable @gitlab/require-i18n-strings */ diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js new file mode 100644 index 00000000000..2519f6b74a2 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js @@ -0,0 +1,39 @@ +import { s__, __ } from '~/locale'; + +export const UPDATED_AT = s__('HarborRegistry|Last updated %{time}'); + +export const MISSING_OR_DELETED_IMAGE_TITLE = s__( + 'HarborRegistry|The image repository could not be found.', +); + +export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__( + 'HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.', +); + +export const NO_TAGS_TITLE = s__('HarborRegistry|This image has no active tags'); + +export const NO_TAGS_MESSAGE = s__( + `HarborRegistry|The last tag related to this image was recently removed. +This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. +If you have any questions, contact your administrator.`, +); + +export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results'); + +export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__( + 'HarborRegistry|Please try different search criteria', +); + +export const DIGEST_LABEL = s__('HarborRegistry|Digest: %{imageId}'); +export const CREATED_AT_LABEL = s__('HarborRegistry|Published %{timeInfo}'); +export const PUBLISHED_DETAILS_ROW_TEXT = s__( + 'HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}', +); +export const MANIFEST_DETAILS_ROW_TEST = s__('HarborRegistry|Manifest digest: %{digest}'); +export const CONFIGURATION_DETAILS_ROW_TEST = s__('HarborRegistry|Configuration digest: %{digest}'); +export const MISSING_MANIFEST_WARNING_TOOLTIP = s__( + 'HarborRegistry|Invalid tag: missing manifest digest', +); + +export const NOT_AVAILABLE_TEXT = __('N/A'); +export const NOT_AVAILABLE_SIZE = __('0 bytes'); diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js new file mode 100644 index 00000000000..22f462e0b97 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js @@ -0,0 +1,3 @@ +export * from './common'; +export * from './list'; +export * from './details'; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js new file mode 100644 index 00000000000..a6cd59918ff --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js @@ -0,0 +1,33 @@ +import { s__, __, n__ } from '~/locale'; +import { NAME_SORT_FIELD } from './common'; + +// Translations strings + +export const HARBOR_REGISTRY_TITLE = s__('HarborRegistry|Harbor Registry'); + +export const CONNECTION_ERROR_TITLE = s__('HarborRegistry|Harbor connection error'); +export const CONNECTION_ERROR_MESSAGE = s__( + `HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`, +); +export const LIST_INTRO_TEXT = s__( + `HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`, +); + +export const imagesCountInfoText = (count) => { + return n__( + 'HarborRegistry|%{count} Image repository', + 'HarborRegistry|%{count} Image repositories', + count, + ); +}; + +export const EMPTY_RESULT_TITLE = s__('HarborRegistry|Sorry, your filter produced no results.'); +export const EMPTY_RESULT_MESSAGE = s__( + 'HarborRegistry|To widen your search, change or remove the filters above.', +); + +export const SORT_FIELDS = [ + { orderBy: 'UPDATED', label: __('Updated') }, + { orderBy: 'CREATED', label: __('Created') }, + NAME_SORT_FIELD, +]; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js new file mode 100644 index 00000000000..ecfefead61a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js @@ -0,0 +1,78 @@ +import { GlToast } from '@gitlab/ui'; +import Vue from 'vue'; +import { parseBoolean } from '~/lib/utils/common_utils'; +import PerformancePlugin from '~/performance/vue_performance_plugin'; +import Translate from '~/vue_shared/translate'; +import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue'; +import { renderBreadcrumb } from '~/packages_and_registries/shared/utils'; +import { helpPagePath } from '~/helpers/help_page_helper'; +import { + dockerBuildCommand, + dockerPushCommand, + dockerLoginCommand, +} from '~/packages_and_registries/harbor_registry/constants'; +import createRouter from './router'; +import HarborRegistryExplorer from './pages/index.vue'; + +Vue.use(Translate); +Vue.use(GlToast); + +Vue.use(PerformancePlugin, { + components: [ + 'RegistryListPage', + 'ListHeader', + 'ImageListRow', + 'RegistryDetailsPage', + 'DetailsHeader', + 'TagsList', + ], +}); + +export default (id) => { + const el = document.getElementById(id); + + if (!el) { + return null; + } + + const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset; + + const breadCrumbState = Vue.observable({ + name: '', + updateName(value) { + this.name = value; + }, + }); + + const router = createRouter(endpoint, breadCrumbState); + + const attachMainComponent = () => { + return new Vue({ + el, + router, + provide() { + return { + breadCrumbState, + config: { + ...config, + connectionError: parseBoolean(connectionError), + invalidPathError: parseBoolean(invalidPathError), + isGroupPage: parseBoolean(isGroupPage), + helpPagePath: helpPagePath('user/packages/container_registry/index'), + }, + dockerBuildCommand: dockerBuildCommand(config.repositoryUrl), + dockerPushCommand: dockerPushCommand(config.repositoryUrl), + dockerLoginCommand: dockerLoginCommand(config.registryHostUrlWithPort), + }; + }, + render(createElement) { + return createElement(HarborRegistryExplorer); + }, + }); + }; + + return { + attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb), + attachMainComponent, + }; +}; diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js b/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js new file mode 100644 index 00000000000..50c7df1483c --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js @@ -0,0 +1,200 @@ +const mockRequestFn = (mockData) => { + return new Promise((resolve) => { + setTimeout(() => { + resolve(mockData); + }, 2000); + }); +}; +export const harborListResponse = () => { + const harborListResponseData = { + repositories: [ + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 26, + name: 'shao/flinkx1', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 27, + name: 'shao/flinkx2', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + }, + ], + totalCount: 3, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + }, + }; + + return mockRequestFn(harborListResponseData); +}; + +export const getHarborRegistryImageDetail = () => { + const harborRegistryImageDetailData = { + artifactCount: 1, + creationTime: '2022-03-02T06:35:53.205Z', + id: 25, + name: 'shao/flinkx', + projectId: 21, + pullCount: 0, + updateTime: '2022-03-02T06:35:53.205Z', + location: 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas', + tagsCount: 10, + }; + + return mockRequestFn(harborRegistryImageDetailData); +}; + +export const harborTagsResponse = () => { + const harborTagsResponseData = { + tags: [ + { + digest: 'sha256:7f386a1844faf341353e1c20f2f39f11f397604fedc475435d13f756eeb235d1', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + name: '02310e655103823920157bc4410ea361dc638bc2cda59667d2cb1f2a988e264c', + revision: 'f53bde3d44699e04e11cf15fb415046a0913e2623d878d89bc21adb2cbda5255', + shortRevision: 'f53bde3d4', + createdAt: '2022-03-02T23:59:05+00:00', + totalSize: '6623124', + }, + { + digest: 'sha256:4554416b84c4568fe93086620b637064ed029737aabe7308b96d50e3d9d92ed7', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + name: '02deb4dddf177212b50e883d5e4f6c03731fad1a18cd27261736cd9dbba79160', + revision: 'e1fe52d8bab66d71bd54a6b8784d3b9edbc68adbd6ea87f5fa44d9974144ef9e', + shortRevision: 'e1fe52d8b', + createdAt: '2022-02-10T01:09:56+00:00', + totalSize: '920760', + }, + { + digest: 'sha256:14f37b60e52b9ce0e9f8f7094b311d265384798592f783487c30aaa3d58e6345', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + name: '03bc5971bab1e849ba52a20a31e7273053f22b2ddb1d04bd6b77d53a2635727a', + revision: 'c72770c6eb93c421bc496964b4bffc742b1ec2e642cdab876be7afda1856029f', + shortRevision: 'c72770c6e', + createdAt: '2021-12-22T04:48:48+00:00', + totalSize: '48609053', + }, + { + digest: 'sha256:e925e3b8277ea23f387ed5fba5e78280cfac7cfb261a78cf046becf7b6a3faae', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + name: '03f495bc5714bff78bb14293320d336afdf47fd47ddff0c3c5f09f8da86d5d19', + revision: '1ac2a43194f4e15166abdf3f26e6ec92215240490b9cac834d63de1a3d87494a', + shortRevision: '1ac2a4319', + createdAt: '2022-03-09T11:02:27+00:00', + totalSize: '35141894', + }, + { + digest: 'sha256:7d8303fd5c077787a8c879f8f66b69e2b5605f48ccd3f286e236fb0749fcc1ca', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + name: '05a4e58231e54b70aab2d6f22ba4fbe10e48aa4ddcbfef11c5662241c2ae4fda', + revision: 'cf8fee086701016e1a84e6824f0c896951fef4cce9d4745459558b87eec3232c', + shortRevision: 'cf8fee086', + createdAt: '2022-01-21T11:31:43+00:00', + totalSize: '48716070', + }, + { + digest: 'sha256:b33611cefe20e4a41a6e0dce356a5d7ef3c177ea7536a58652f5b3a4f2f83549', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + name: '093d2746876997723541aec8b88687a4cdb3b5bbb0279c5089b7891317741a9a', + revision: '1a4b48198b13d55242c5164e64d41c4e9f75b5d9506bc6e0efc1534dd0dd1f15', + shortRevision: '1a4b48198', + createdAt: '2022-01-21T11:31:51+00:00', + totalSize: '6623127', + }, + { + digest: 'sha256:d25c3c020e2dbd4711a67b9fe308f4cbb7b0bb21815e722a02f91c570dc5d519', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + name: '09698b3fae81dfd6e02554dbc82930f304a6356c8f541c80e8598a42aed985f7', + revision: '03e2e2777dde01c30469ee8c710973dd08a7a4f70494d7dc1583c24b525d7f61', + shortRevision: '03e2e2777', + createdAt: '2022-03-02T23:58:20+00:00', + totalSize: '911377', + }, + { + digest: 'sha256:fb760e4d2184e9e8e39d6917534d4610fe01009734698a5653b2de1391ba28f4', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + name: '09b830c3eaf80d547f3b523d8e242a2c411085c349dab86c520f36c7b7644f95', + revision: '350e78d60646bf6967244448c6aaa14d21ecb9a0c6cf87e9ff0361cbe59b9012', + shortRevision: '350e78d60', + createdAt: '2022-01-19T13:49:14+00:00', + totalSize: '48710241', + }, + { + digest: 'sha256:407250f380cea92729cbc038c420e74900f53b852e11edc6404fe75a0fd2c402', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + name: '0d03504a17b467eafc8c96bde70af26c74bd459a32b7eb2dd189dd6b3c121557', + revision: '76038370b7f3904364891457c4a6a234897255e6b9f45d0a852bf3a7e5257e18', + shortRevision: '76038370b', + createdAt: '2022-01-24T12:56:22+00:00', + totalSize: '280065', + }, + { + digest: 'sha256:ada87f25218542951ce6720c27f3d0758e90c2540bd129f5cfb9e15b31e07b07', + location: + 'registry.gitlab.com/gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + path: + 'gitlab-org/gitlab/gitlab-ee-qa/cache:0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + name: '0eb20a4a7cac2ebea821d420b3279654fe550fd8502f1785c1927aa84e5949eb', + revision: '3d4b49a7bbb36c48bb721f4d0e76e7950bec3878ee29cdfdd6da39f575d6d37f', + shortRevision: '3d4b49a7b', + createdAt: '2022-02-17T17:37:52+00:00', + totalSize: '48655767', + }, + ], + totalCount: 10, + pageInfo: { + hasNextPage: false, + hasPreviousPage: true, + }, + }; + + return mockRequestFn(harborTagsResponseData); +}; 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 new file mode 100644 index 00000000000..e69de29bb2d --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue new file mode 100644 index 00000000000..dca63e1a569 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue @@ -0,0 +1,5 @@ +<template> + <div> + <router-view ref="router-view" /> + </div> +</template> 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 new file mode 100644 index 00000000000..7aaef2ed57a --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue @@ -0,0 +1,177 @@ +<script> +import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui'; +import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue'; +import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue'; +import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue'; +import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants'; +import { + SORT_FIELDS, + CONNECTION_ERROR_TITLE, + CONNECTION_ERROR_MESSAGE, + EMPTY_RESULT_TITLE, + EMPTY_RESULT_MESSAGE, +} from '~/packages_and_registries/harbor_registry/constants'; +import Tracking from '~/tracking'; +import { harborListResponse } from '../mock_api'; + +export default { + name: 'HarborListPage', + components: { + HarborListHeader, + HarborList, + GlSkeletonLoader, + GlEmptyState, + GlSprintf, + GlLink, + PersistedSearch, + CliCommands: () => + import( + /* webpackChunkName: 'harbor_registry_components' */ '~/packages_and_registries/shared/components/cli_commands.vue' + ), + }, + mixins: [Tracking.mixin()], + inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'], + loader: { + repeat: 10, + width: 1000, + height: 40, + }, + i18n: { + CONNECTION_ERROR_TITLE, + CONNECTION_ERROR_MESSAGE, + EMPTY_RESULT_TITLE, + EMPTY_RESULT_MESSAGE, + }, + searchConfig: SORT_FIELDS, + data() { + return { + images: [], + totalCount: 0, + pageInfo: {}, + filter: [], + isLoading: true, + sorting: null, + name: null, + }; + }, + computed: { + showCommands() { + return !this.isLoading && !this.config?.isGroupPage && this.images?.length; + }, + showConnectionError() { + return this.config.connectionError || this.config.invalidPathError; + }, + }, + methods: { + fetchHarborImages() { + // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777 + this.isLoading = true; + + harborListResponse() + .then((res) => { + this.images = res?.repositories || []; + this.totalCount = res?.totalCount || 0; + this.pageInfo = res?.pageInfo || {}; + this.isLoading = false; + }) + .catch(() => {}); + }, + handleSearchUpdate({ sort, filters }) { + this.sorting = sort; + + const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM); + this.name = search?.value?.data; + + this.fetchHarborImages(); + }, + fetchPrevPage() { + // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777 + this.fetchHarborImages(); + }, + fetchNextPage() { + // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777 + this.fetchHarborImages(); + }, + }, +}; +</script> + +<template> + <div> + <gl-empty-state + v-if="showConnectionError" + :title="$options.i18n.CONNECTION_ERROR_TITLE" + :svg-path="config.containersErrorImage" + > + <template #description> + <p> + <gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE"> + <template #docLink="{ content }"> + <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank"> + {{ content }} + </gl-link> + </template> + </gl-sprintf> + </p> + </template> + </gl-empty-state> + <template v-else> + <harbor-list-header + :metadata-loading="isLoading" + :images-count="totalCount" + :help-page-path="config.helpPagePath" + > + <template #commands> + <cli-commands + v-if="showCommands" + :docker-build-command="dockerBuildCommand" + :docker-push-command="dockerPushCommand" + :docker-login-command="dockerLoginCommand" + /> + </template> + </harbor-list-header> + <persisted-search + :sortable-fields="$options.searchConfig" + :default-order="$options.searchConfig[0].orderBy" + default-sort="desc" + @update="handleSearchUpdate" + /> + + <div v-if="isLoading" class="gl-mt-5"> + <gl-skeleton-loader + v-for="index in $options.loader.repeat" + :key="index" + :width="$options.loader.width" + :height="$options.loader.height" + preserve-aspect-ratio="xMinYMax meet" + > + <rect width="500" x="10" y="10" height="20" rx="4" /> + <circle cx="525" cy="20" r="10" /> + <rect x="960" y="0" width="40" height="40" rx="4" /> + </gl-skeleton-loader> + </div> + <template v-else> + <template v-if="images.length > 0 || name"> + <harbor-list + v-if="images.length" + :images="images" + :meta-data-loading="isLoading" + :page-info="pageInfo" + @prev-page="fetchPrevPage" + @next-page="fetchNextPage" + /> + <gl-empty-state + v-else + :svg-path="config.noContainersImage" + data-testid="emptySearch" + :title="$options.i18n.EMPTY_RESULT_TITLE" + > + <template #description> + {{ $options.i18n.EMPTY_RESULT_MESSAGE }} + </template> + </gl-empty-state> + </template> + </template> + </template> + </div> +</template> diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js new file mode 100644 index 00000000000..572dd382be3 --- /dev/null +++ b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js @@ -0,0 +1,35 @@ +import Vue from 'vue'; +import VueRouter from 'vue-router'; +import { HARBOR_REGISTRY_TITLE } from './constants/index'; +import List from './pages/list.vue'; +import Details from './pages/details.vue'; + +Vue.use(VueRouter); + +export default function createRouter(base, breadCrumbState) { + const router = new VueRouter({ + base, + mode: 'history', + routes: [ + { + name: 'list', + path: '/', + component: List, + meta: { + nameGenerator: () => HARBOR_REGISTRY_TITLE, + root: true, + }, + }, + { + name: 'details', + path: '/:id', + component: Details, + meta: { + nameGenerator: () => breadCrumbState.name, + }, + }, + ], + }); + + return router; +} 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 488860e5bc2..408d34fbe93 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 @@ -26,6 +26,7 @@ export const receivePackagesListSuccess = ({ commit }, { data, headers }) => { export const requestPackagesList = ({ dispatch, state }, params = {}) => { dispatch('setLoading', true); + // eslint-disable-next-line camelcase const { page = DEFAULT_PAGE, per_page = DEFAULT_PAGE_SIZE } = params; const { sort, orderBy } = state.sorting; const type = state.config.forceTerraform diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue index c27083261b5..7a88e04d1f9 100644 --- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue +++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue @@ -99,7 +99,6 @@ export default { <local-storage-sync storage-key="package_registry_list_sorting" :value="sorting" - as-json @input="updateSorting" > <url-sync> 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 4b913590949..5bde5f08e56 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,5 +1,5 @@ #import "~/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql" -#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" +#import "~/graphql_shared/fragments/page_info.fragment.graphql" query getPackages( $fullPath: ID! 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 7be3bba7cae..854c88b2ad3 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 @@ -9,7 +9,6 @@ import { UNAVAILABLE_ADMIN_FEATURE_TEXT, } from '~/packages_and_registries/settings/project/constants'; import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql'; -import CleanupPolicyEnabledAlert from '~/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue'; import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue'; import SettingsForm from './settings_form.vue'; @@ -18,19 +17,11 @@ export default { components: { SettingsBlock, SettingsForm, - CleanupPolicyEnabledAlert, GlAlert, GlSprintf, GlLink, }, - inject: [ - 'projectPath', - 'isAdmin', - 'adminSettingsPath', - 'enableHistoricEntries', - 'helpPagePath', - 'showCleanupPolicyOnAlert', - ], + inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'], i18n: { UNAVAILABLE_FEATURE_TITLE, UNAVAILABLE_FEATURE_INTRO_TEXT, @@ -87,7 +78,6 @@ export default { <template> <section data-testid="registry-settings-app"> - <cleanup-policy-enabled-alert v-if="showCleanupPolicyOnAlert" :project-path="projectPath" /> <settings-block :collapsible="false"> <template #title> {{ __('Clean up image tags') }}</template> <template #description> diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js index 2a3e2c28fa6..17c33073668 100644 --- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js +++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js @@ -20,7 +20,6 @@ export default () => { adminSettingsPath, tagsRegexHelpPagePath, helpPagePath, - showCleanupPolicyOnAlert, } = el.dataset; return new Vue({ el, @@ -35,7 +34,6 @@ export default () => { adminSettingsPath, tagsRegexHelpPagePath, helpPagePath, - showCleanupPolicyOnAlert: parseBoolean(showCleanupPolicyOnAlert), }, render(createElement) { return createElement('registry-settings-app', {}); diff --git a/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue b/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue deleted file mode 100644 index d51c62e0623..00000000000 --- a/app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue +++ /dev/null @@ -1,54 +0,0 @@ -<script> -import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui'; -import { s__ } from '~/locale'; -import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; - -export default { - components: { - GlAlert, - GlLink, - GlSprintf, - LocalStorageSync, - }, - props: { - projectPath: { - type: String, - required: true, - }, - cleanupPoliciesSettingsPath: { - type: String, - required: false, - default: '', - }, - }, - data() { - return { - dismissed: false, - }; - }, - computed: { - storageKey() { - return `cleanup_policy_enabled_for_project_${this.projectPath}`; - }, - }, - i18n: { - message: s__( - 'ContainerRegistry|Cleanup policies are now available for this project. %{linkStart}Click here to get started.%{linkEnd}', - ), - }, -}; -</script> - -<template> - <local-storage-sync v-model="dismissed" :storage-key="storageKey"> - <gl-alert v-if="!dismissed" class="gl-mt-2" dismissible @dismiss="dismissed = true"> - <gl-sprintf :message="$options.i18n.message"> - <template #link="{ content }"> - <gl-link v-if="cleanupPoliciesSettingsPath" :href="cleanupPoliciesSettingsPath">{{ - content - }}</gl-link> - </template> - </gl-sprintf> - </gl-alert> - </local-storage-sync> -</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 79381f82009..cc345fda7e8 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 @@ -13,7 +13,8 @@ export default { props: { title: { type: String, - required: true, + default: '', + required: false, }, isLoading: { type: Boolean, |