Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 13:00:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-04-20 13:00:54 +0300
commit3cccd102ba543e02725d247893729e5c73b38295 (patch)
treef36a04ec38517f5deaaacb5acc7d949688d1e187 /app/assets/javascripts/packages_and_registries
parent205943281328046ef7b4528031b90fbda70c75ac (diff)
Add latest changes from gitlab-org/gitlab@14-10-stable-eev14.10.0-rc42
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/delete_button.vue33
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/registry_header.vue17
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/expiration_policies.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue104
-rw-r--r--app/assets/javascripts/packages_and_registries/dependency_proxy/graphql/queries/get_dependency_proxy_details.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list.vue42
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue67
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue84
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js29
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js39
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/index.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js33
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/index.js78
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js200
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue0
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/index.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue177
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/router.js35
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue12
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/cleanup_policy_enabled_alert.vue54
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_list.vue3
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,