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:
Diffstat (limited to 'app/assets/javascripts/packages_and_registries')
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue95
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue133
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue47
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue68
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue12
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue32
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue54
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue82
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue74
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js17
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js46
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js14
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/index.js43
-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.vue156
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue103
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue169
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/router.js13
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/utils.js84
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue103
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue79
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue129
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue112
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue70
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue32
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue39
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/constants.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js41
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js4
46 files changed, 1573 insertions, 628 deletions
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
new file mode 100644
index 00000000000..b55204de875
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import {
+ NO_ARTIFACTS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+} from '~/packages_and_registries/harbor_registry/constants';
+import ArtifactsListRow from '~/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue';
+
+export default {
+ name: 'TagsList',
+ components: {
+ GlEmptyState,
+ ArtifactsListRow,
+ TagsLoader,
+ RegistryList,
+ },
+ inject: ['noContainersImage'],
+ props: {
+ artifacts: {
+ type: Array,
+ required: true,
+ },
+ filter: {
+ type: String,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ tags: [],
+ tagsPageInfo: {},
+ };
+ },
+ computed: {
+ hasNoTags() {
+ return this.artifacts.length === 0;
+ },
+ emptyStateTitle() {
+ return this.filter ? NO_TAGS_MATCHING_FILTERS_TITLE : NO_ARTIFACTS_TITLE;
+ },
+ emptyStateDescription() {
+ return this.filter ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : '';
+ },
+ },
+ methods: {
+ fetchNextPage() {
+ this.$emit('next-page');
+ },
+ fetchPreviousPage() {
+ this.$emit('prev-page');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <tags-loader v-if="isLoading" />
+ <template v-else>
+ <gl-empty-state
+ v-if="hasNoTags"
+ :title="emptyStateTitle"
+ :svg-path="noContainersImage"
+ :description="emptyStateDescription"
+ class="gl-mx-auto gl-my-0"
+ />
+ <template v-else>
+ <registry-list
+ :pagination="pageInfo"
+ :items="artifacts"
+ :hidden-delete="true"
+ id-property="name"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ >
+ <template #default="{ item }">
+ <artifacts-list-row :artifact="item" />
+ </template>
+ </registry-list>
+ </template>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue
new file mode 100644
index 00000000000..b489f126f75
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/artifacts_list_row.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { n__ } from '~/locale';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import {
+ DIGEST_LABEL,
+ CREATED_AT_LABEL,
+ NOT_AVAILABLE_TEXT,
+ NOT_AVAILABLE_SIZE,
+} from '~/packages_and_registries/harbor_registry/constants';
+import { artifactPullCommand } from '~/packages_and_registries/harbor_registry/utils';
+
+export default {
+ name: 'TagsListRow',
+ components: {
+ GlSprintf,
+ GlIcon,
+ ListItem,
+ ClipboardButton,
+ TimeAgoTooltip,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['repositoryUrl', 'harborIntegrationProjectName'],
+ props: {
+ artifact: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ digestLabel: DIGEST_LABEL,
+ createdAtLabel: CREATED_AT_LABEL,
+ },
+ computed: {
+ formattedSize() {
+ return this.artifact.size
+ ? numberToHumanSize(Number(this.artifact.size))
+ : NOT_AVAILABLE_SIZE;
+ },
+ tagsCountText() {
+ const count = this.artifact?.tags.length ? this.artifact?.tags.length : 0;
+
+ return n__('%d tag', '%d tags', count);
+ },
+ shortDigest() {
+ // remove sha256: from the string, and show only the first 7 char
+ const PREFIX_LENGTH = 'sha256:'.length;
+ const DIGEST_LENGTH = 7;
+ return (
+ this.artifact.digest?.substring(PREFIX_LENGTH, PREFIX_LENGTH + DIGEST_LENGTH) ??
+ NOT_AVAILABLE_TEXT
+ );
+ },
+ getPullCommand() {
+ if (this.artifact?.digest) {
+ const { image } = this.$route.params;
+ return artifactPullCommand({
+ digest: this.artifact.digest,
+ imageName: image,
+ repositoryUrl: this.repositoryUrl,
+ harborProjectName: this.harborIntegrationProjectName,
+ });
+ }
+
+ return '';
+ },
+ linkTo() {
+ const { project, image } = this.$route.params;
+
+ return { name: 'tags', params: { project, image, digest: this.artifact.digest } };
+ },
+ },
+};
+</script>
+
+<template>
+ <list-item v-bind="$attrs">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center">
+ <router-link
+ class="gl-text-body gl-font-weight-bold gl-word-break-all"
+ data-testid="name"
+ :to="linkTo"
+ >
+ {{ artifact.digest }}
+ </router-link>
+ <clipboard-button
+ v-if="getPullCommand"
+ :title="getPullCommand"
+ :text="getPullCommand"
+ category="tertiary"
+ />
+ </div>
+ </template>
+
+ <template #left-secondary>
+ <span class="gl-mr-3" data-testid="size">
+ {{ formattedSize }}
+ </span>
+ <span id="tagsCount" class="gl-display-flex gl-align-items-center" data-testid="tags-count">
+ <gl-icon name="tag" class="gl-mr-2" />
+ {{ tagsCountText }}
+ </span>
+ </template>
+ <template #right-primary>
+ <span data-testid="time">
+ <gl-sprintf :message="$options.i18n.createdAtLabel">
+ <template #timeInfo>
+ <time-ago-tooltip :time="artifact.pushTime" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ <template #right-secondary>
+ <span data-testid="digest">
+ <gl-sprintf :message="$options.i18n.digestLabel">
+ <template #imageId>{{ shortDigest }}</template>
+ </gl-sprintf>
+ </span>
+ <clipboard-button
+ v-if="artifact.digest"
+ :title="artifact.digest"
+ :text="artifact.digest"
+ category="tertiary"
+ />
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue
new file mode 100644
index 00000000000..bfb097601d5
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/details/details_header.vue
@@ -0,0 +1,47 @@
+<script>
+import { isEmpty } from 'lodash';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import {
+ ROOT_IMAGE_TEXT,
+ EMPTY_ARTIFACTS_LABEL,
+ artifactsLabel,
+} from '~/packages_and_registries/harbor_registry/constants/index';
+
+export default {
+ name: 'DetailsHeader',
+ components: { TitleArea, MetadataItem },
+ mixins: [timeagoMixin],
+ props: {
+ imagesDetail: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ artifactCountText() {
+ if (isEmpty(this.imagesDetail)) {
+ return EMPTY_ARTIFACTS_LABEL;
+ }
+ return artifactsLabel(this.imagesDetail.artifactCount);
+ },
+ repositoryFullName() {
+ return this.imagesDetail.name || ROOT_IMAGE_TEXT;
+ },
+ },
+};
+</script>
+
+<template>
+ <title-area>
+ <template #title>
+ <span data-testid="title">
+ {{ repositoryFullName }}
+ </span>
+ </template>
+ <template #metadata-tags-count>
+ <metadata-item icon="package" :text="artifactCountText" data-testid="artifacts-count" />
+ </template>
+ </title-area>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue
new file mode 100644
index 00000000000..ac1df5cf93f
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/harbor_registry_breadcrumb.vue
@@ -0,0 +1,68 @@
+<script>
+// Since app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
+// can only handle two levels of breadcrumbs, but we have three levels here.
+// So we extended the registry_breadcrumb.vue component with harbor_registry_breadcrumb.vue to support multiple levels of breadcrumbs
+import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
+import { isArray, last } from 'lodash';
+
+export default {
+ components: {
+ GlBreadcrumb,
+ GlIcon,
+ },
+ computed: {
+ rootRoute() {
+ return this.$router.options.routes.find((r) => r.meta.root);
+ },
+ isRootRoute() {
+ return this.$route.name === this.rootRoute.name;
+ },
+ currentRoute() {
+ const currentName = this.$route.meta.nameGenerator();
+ const currentHref = this.$route.meta.hrefGenerator();
+ let routeInfoList = [
+ {
+ text: currentName,
+ to: currentHref,
+ },
+ ];
+
+ if (isArray(currentName) && isArray(currentHref)) {
+ routeInfoList = currentName.map((name, index) => {
+ return {
+ text: name,
+ to: currentHref[index],
+ };
+ });
+ }
+
+ return routeInfoList;
+ },
+ isLoaded() {
+ return this.isRootRoute || last(this.currentRoute).text;
+ },
+ allCrumbs() {
+ let crumbs = [
+ {
+ text: this.rootRoute.meta.nameGenerator(),
+ to: this.rootRoute.path,
+ },
+ ];
+ if (!this.isRootRoute) {
+ crumbs = crumbs.concat(this.currentRoute);
+ }
+ return crumbs;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-breadcrumb :key="isLoaded" :items="allCrumbs">
+ <template #separator>
+ <span class="gl-mx-n5">
+ <gl-icon name="chevron-lg-right" :size="8" />
+ </span>
+ </template>
+ </gl-breadcrumb>
+</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
index 086b9c73d75..db66ebef937 100644
--- 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
@@ -5,6 +5,7 @@ import {
HARBOR_REGISTRY_TITLE,
LIST_INTRO_TEXT,
imagesCountInfoText,
+ HARBOR_REGISTRY_HELP_PAGE_PATH,
} from '~/packages_and_registries/harbor_registry/constants';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
@@ -20,11 +21,6 @@ export default {
default: 0,
required: false,
},
- helpPagePath: {
- type: String,
- default: '',
- required: false,
- },
metadataLoading: {
type: Boolean,
required: false,
@@ -32,7 +28,7 @@ export default {
},
},
i18n: {
- HARBOR_REGISTRY_TITLE,
+ harborRegistryTitle: HARBOR_REGISTRY_TITLE,
},
computed: {
imagesCountText() {
@@ -40,7 +36,7 @@ export default {
return sprintf(pluralisedString, { count: this.imagesCount });
},
infoMessages() {
- return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
+ return [{ text: LIST_INTRO_TEXT, link: HARBOR_REGISTRY_HELP_PAGE_PATH }];
},
},
};
@@ -48,7 +44,7 @@ export default {
<template>
<title-area
- :title="$options.i18n.HARBOR_REGISTRY_TITLE"
+ :title="$options.i18n.harborRegistryTitle"
:info-messages="infoMessages"
:metadata-loading="metadataLoading"
>
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
index 258472fe16e..bfe0c250dd9 100644
--- 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
@@ -1,15 +1,14 @@
<script>
-import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
+import { GlIcon, 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';
+import { getNameFromParams } from '~/packages_and_registries/harbor_registry/utils';
export default {
name: 'HarborListRow',
components: {
ClipboardButton,
- GlSprintf,
GlIcon,
ListItem,
GlSkeletonLoader,
@@ -26,19 +25,18 @@ export default {
},
},
computed: {
- id() {
- return this.item.id;
+ linkTo() {
+ const { projectName, imageName } = getNameFromParams(this.item.name);
+
+ return { name: 'details', params: { project: projectName, image: imageName } };
},
artifactCountText() {
return n__(
- 'HarborRegistry|%{count} Tag',
- 'HarborRegistry|%{count} Tags',
+ 'HarborRegistry|%d artifact',
+ 'HarborRegistry|%d artifacts',
this.item.artifactCount,
);
},
- imageName() {
- return this.item.name;
- },
},
};
</script>
@@ -50,9 +48,9 @@ export default {
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
data-qa-selector="registry_image_content"
- :to="{ name: 'details', params: { id } }"
+ :to="linkTo"
>
- {{ imageName }}
+ {{ item.name }}
</router-link>
<clipboard-button
v-if="item.location"
@@ -63,13 +61,9 @@ export default {
</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 class="gl-display-flex gl-align-items-center" data-testid="artifacts-count">
+ <gl-icon name="package" class="gl-mr-2" />
+ {{ artifactCountText }}
</span>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue
new file mode 100644
index 00000000000..e7f6989c49f
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_header.vue
@@ -0,0 +1,54 @@
+<script>
+import { isEmpty } from 'lodash';
+import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import timeagoMixin from '~/vue_shared/mixins/timeago';
+import {
+ EMPTY_TAG_LABEL,
+ tagsCountText,
+} from '~/packages_and_registries/harbor_registry/constants';
+
+export default {
+ name: 'TagsHeader',
+ components: {
+ TitleArea,
+ MetadataItem,
+ },
+ mixins: [timeagoMixin],
+ props: {
+ artifactDetail: {
+ type: Object,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ tagsLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ tagCountText() {
+ if (isEmpty(this.pageInfo)) {
+ return EMPTY_TAG_LABEL;
+ }
+ return tagsCountText(this.pageInfo.total);
+ },
+ },
+};
+</script>
+
+<template>
+ <title-area :metadata-loading="tagsLoading">
+ <template #title>
+ <span class="gl-word-break-all" data-testid="title">
+ {{ artifactDetail.digest }}
+ </span>
+ </template>
+ <template #metadata-tags-count>
+ <metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
+ </template>
+ </title-area>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
new file mode 100644
index 00000000000..b34d3a950c0
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list.vue
@@ -0,0 +1,82 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import TagsListRow from '~/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue';
+import {
+ NO_ARTIFACTS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
+} from '~/packages_and_registries/harbor_registry/constants';
+
+export default {
+ name: 'TagsList',
+ components: {
+ GlEmptyState,
+ TagsLoader,
+ TagsListRow,
+ RegistryList,
+ },
+ inject: ['noContainersImage'],
+ props: {
+ tags: {
+ type: Array,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ hasNoTags() {
+ return this.tags.length === 0;
+ },
+ emptyStateTitle() {
+ return this.filter ? NO_TAGS_MATCHING_FILTERS_TITLE : NO_ARTIFACTS_TITLE;
+ },
+ emptyStateDescription() {
+ return this.filter ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : '';
+ },
+ },
+ methods: {
+ fetchNextPage() {
+ this.$emit('next-page');
+ },
+ fetchPreviousPage() {
+ this.$emit('prev-page');
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <tags-loader v-if="isLoading" />
+ <gl-empty-state
+ v-else-if="hasNoTags"
+ :title="emptyStateTitle"
+ :svg-path="noContainersImage"
+ :description="emptyStateDescription"
+ class="gl-mx-auto gl-my-0"
+ />
+ <registry-list
+ v-else
+ :pagination="pageInfo"
+ :items="tags"
+ hidden-delete
+ id-property="name"
+ @prev-page="fetchPreviousPage"
+ @next-page="fetchNextPage"
+ >
+ <template #default="{ item }">
+ <tags-list-row :tag="item" />
+ </template>
+ </registry-list>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue
new file mode 100644
index 00000000000..63e046c1abc
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/components/tags/tags_list_row.vue
@@ -0,0 +1,74 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { CREATED_AT_LABEL } from '~/packages_and_registries/harbor_registry/constants';
+import { tagPullCommand } from '~/packages_and_registries/harbor_registry/utils';
+
+export default {
+ name: 'TagsListRow',
+ components: {
+ GlSprintf,
+ ListItem,
+ ClipboardButton,
+ TimeAgoTooltip,
+ },
+ inject: ['harborIntegrationProjectName', 'repositoryUrl'],
+ props: {
+ tag: {
+ type: Object,
+ required: true,
+ },
+ },
+ i18n: {
+ createdAtLabel: CREATED_AT_LABEL,
+ },
+ methods: {
+ getPullCommand(tagName) {
+ if (tagName) {
+ const { image } = this.$route.params;
+
+ return tagPullCommand({
+ imageName: image,
+ tag: tagName,
+ repositoryUrl: this.repositoryUrl,
+ harborProjectName: this.harborIntegrationProjectName,
+ });
+ }
+
+ return '';
+ },
+ },
+};
+</script>
+
+<template>
+ <list-item v-bind="$attrs">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center">
+ <div
+ data-testid="name"
+ class="gl-text-overflow-ellipsis gl-overflow-hidden gl-white-space-nowrap"
+ >
+ {{ tag.name }}
+ </div>
+ <clipboard-button
+ :title="getPullCommand(tag.name)"
+ :text="getPullCommand(tag.name)"
+ category="tertiary"
+ />
+ </div>
+ </template>
+
+ <template #right-primary>
+ <span data-testid="time">
+ <gl-sprintf :message="$options.i18n.createdAtLabel">
+ <template #timeInfo>
+ <time-ago-tooltip :time="tag.pushTime" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </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
index a7891821755..7f3c3da02b0 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/common.js
@@ -1,4 +1,5 @@
import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const ROOT_IMAGE_TEXT = s__('HarborRegistry|Root image');
export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') };
@@ -16,14 +17,8 @@ export const SORT_FIELD_MAPPING = {
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 */
+export const DEFAULT_PER_PAGE = 10;
+
+export const HARBOR_REGISTRY_HELP_PAGE_PATH = helpPagePath(
+ 'user/packages/harbor_container_registry/index',
+);
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
index b62c51bd208..5b4b85ec31e 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/details.js
@@ -1,22 +1,10 @@
-import { s__, __ } from '~/locale';
+import { s__, __, n__ } 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 FETCH_ARTIFACT_LIST_ERROR_MESSAGE = s__(
+ 'HarborRegistry|Something went wrong while fetching the artifact list.',
);
-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_ARTIFACTS_TITLE = s__('HarborRegistry|This image has no artifacts');
export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results');
@@ -26,14 +14,24 @@ export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__(
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 = __('Not applicable.');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
+
+export const TOKEN_TYPE_TAG_NAME = 'tag_name';
+
+export const FETCH_TAGS_ERROR_MESSAGE = s__(
+ 'HarborRegistry|Something went wrong while fetching the tags.',
+);
+
+export const TAG_LABEL = s__('HarborRegistry|Tag');
+export const EMPTY_TAG_LABEL = s__('HarborRegistry|-- tags');
+
+export const EMPTY_ARTIFACTS_LABEL = s__('HarborRegistry|-- artifacts');
+export const artifactsLabel = (count) => {
+ return n__('%d artifact', '%d artifacts', count);
+};
+
+export const tagsCountText = (count) => {
+ return n__('%d tag', '%d tags', count);
+};
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
index a6cd59918ff..33950993125 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/constants/list.js
@@ -7,8 +7,13 @@ 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}.`,
+ `HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the documentation%{docLinkEnd}.`,
);
+
+export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
+ 'HarborRegistry|Something went wrong while fetching the repository list.',
+);
+
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}`,
);
@@ -26,6 +31,13 @@ export const EMPTY_RESULT_MESSAGE = s__(
'HarborRegistry|To widen your search, change or remove the filters above.',
);
+export const EMPTY_IMAGES_TITLE = s__(
+ 'HarborRegistry|There are no harbor images stored for this project',
+);
+export const EMPTY_IMAGES_MESSAGE = s__(
+ 'HarborRegistry|With the Harbor Registry, every project can connect to a harbor space to store its Docker images.',
+);
+
export const SORT_FIELDS = [
{ orderBy: 'UPDATED', label: __('Updated') },
{ orderBy: 'CREATED', label: __('Created') },
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
index ecfefead61a..6185e4c7bc6 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/index.js
@@ -3,14 +3,8 @@ 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 RegistryBreadcrumb from '~/packages_and_registries/harbor_registry/components/harbor_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';
@@ -35,13 +29,27 @@ export default (id) => {
return null;
}
- const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset;
+ const {
+ endpoint,
+ connectionError,
+ invalidPathError,
+ isGroupPage,
+ noContainersImage,
+ containersErrorImage,
+ repositoryUrl,
+ harborIntegrationProjectName,
+ projectName,
+ } = el.dataset;
const breadCrumbState = Vue.observable({
name: '',
+ href: '',
updateName(value) {
this.name = value;
},
+ updateHref(value) {
+ this.href = value;
+ },
});
const router = createRouter(endpoint, breadCrumbState);
@@ -53,16 +61,15 @@ export default (id) => {
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),
+ endpoint,
+ connectionError: parseBoolean(connectionError),
+ invalidPathError: parseBoolean(invalidPathError),
+ isGroupPage: parseBoolean(isGroupPage),
+ repositoryUrl,
+ harborIntegrationProjectName,
+ projectName,
+ containersErrorImage,
+ noContainersImage,
};
},
render(createElement) {
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
deleted file mode 100644
index 50c7df1483c..00000000000
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/mock_api.js
+++ /dev/null
@@ -1,200 +0,0 @@
-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
index e69de29bb2d..c6ab746b9f4 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -0,0 +1,156 @@
+<script>
+import { GlFilteredSearchToken } from '@gitlab/ui';
+import {
+ NAME_SORT_FIELD,
+ ROOT_IMAGE_TEXT,
+ DEFAULT_PER_PAGE,
+ FETCH_ARTIFACT_LIST_ERROR_MESSAGE,
+ TOKEN_TYPE_TAG_NAME,
+ TAG_LABEL,
+} from '~/packages_and_registries/harbor_registry/constants/index';
+import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { createAlert } from '~/flash';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import ArtifactsList from '~/packages_and_registries/harbor_registry/components/details/artifacts_list.vue';
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import DetailsHeader from '~/packages_and_registries/harbor_registry/components/details/details_header.vue';
+import {
+ extractSortingDetail,
+ parseFilter,
+ formatPagination,
+} from '~/packages_and_registries/harbor_registry/utils';
+import { getHarborArtifacts } from '~/rest_api';
+
+export default {
+ name: 'HarborDetailsPage',
+ components: {
+ ArtifactsList,
+ TagsLoader,
+ DetailsHeader,
+ PersistedSearch,
+ },
+ inject: ['endpoint', 'breadCrumbState'],
+ searchConfig: { nameSortFields: [NAME_SORT_FIELD] },
+ tokens: [
+ {
+ type: TOKEN_TYPE_TAG_NAME,
+ icon: 'tag',
+ title: TAG_LABEL,
+ unique: true,
+ token: GlFilteredSearchToken,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ],
+ data() {
+ return {
+ artifactsList: [],
+ pageInfo: {},
+ mutationLoading: false,
+ deleteAlertType: null,
+ isLoading: true,
+ filterString: '',
+ sorting: null,
+ };
+ },
+ computed: {
+ currentPage() {
+ return this.pageInfo.page || 1;
+ },
+ imagesDetail() {
+ return {
+ name: this.fullName,
+ artifactCount: this.pageInfo?.total || 0,
+ };
+ },
+ fullName() {
+ const { project, image } = this.$route.params;
+
+ if (project && image) {
+ return `${project}/${image}`;
+ }
+ return '';
+ },
+ },
+ mounted() {
+ this.updateBreadcrumb();
+ },
+ methods: {
+ updateBreadcrumb() {
+ const name = this.fullName || ROOT_IMAGE_TEXT;
+ this.breadCrumbState.updateName(name);
+ this.breadCrumbState.updateHref(this.$route.path);
+ },
+ handleSearchUpdate({ sort, filters }) {
+ this.sorting = sort;
+ this.filterString = parseFilter(filters, 'digest');
+
+ this.fetchArtifacts(1);
+ },
+ fetchPrevPage() {
+ const prevPageNum = this.currentPage - 1;
+ this.fetchArtifacts(prevPageNum);
+ },
+ fetchNextPage() {
+ const nextPageNum = this.currentPage + 1;
+ this.fetchArtifacts(nextPageNum);
+ },
+ fetchArtifacts(requestPage) {
+ this.isLoading = true;
+
+ const { orderBy, sort } = extractSortingDetail(this.sorting);
+ const sortOptions = `${orderBy} ${sort}`;
+
+ const { image } = this.$route.params;
+
+ const params = {
+ requestPath: this.endpoint,
+ repoName: image,
+ limit: DEFAULT_PER_PAGE,
+ page: requestPage,
+ sort: sortOptions,
+ search: this.filterString,
+ };
+
+ getHarborArtifacts(params)
+ .then((res) => {
+ this.pageInfo = formatPagination(res.headers);
+
+ this.artifactsList = (res?.data || []).map((artifact) => {
+ return convertObjectPropsToCamelCase(artifact);
+ });
+ })
+ .catch(() => {
+ createAlert({ message: FETCH_ARTIFACT_LIST_ERROR_MESSAGE });
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-my-3">
+ <details-header :images-detail="imagesDetail" />
+ <persisted-search
+ class="gl-mb-5"
+ :sortable-fields="$options.searchConfig.nameSortFields"
+ :default-order="$options.searchConfig.nameSortFields[0].orderBy"
+ default-sort="asc"
+ :tokens="$options.tokens"
+ @update="handleSearchUpdate"
+ />
+ <tags-loader v-if="isLoading" />
+ <artifacts-list
+ v-else
+ :filter="filterString"
+ :is-loading="isLoading"
+ :artifacts="artifactsList"
+ :page-info="pageInfo"
+ @prev-page="fetchPrevPage"
+ @next-page="fetchNextPage"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue
new file mode 100644
index 00000000000..1323d347d10
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/harbor_tags.vue
@@ -0,0 +1,103 @@
+<script>
+import TagsHeader from '~/packages_and_registries/harbor_registry/components/tags/tags_header.vue';
+import TagsList from '~/packages_and_registries/harbor_registry/components/tags/tags_list.vue';
+import { getHarborTags } from '~/rest_api';
+import { FETCH_TAGS_ERROR_MESSAGE } from '~/packages_and_registries/harbor_registry/constants';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { createAlert } from '~/flash';
+import { formatPagination } from '~/packages_and_registries/harbor_registry/utils';
+
+export default {
+ name: 'HarborTagsPage',
+ components: {
+ TagsHeader,
+ TagsList,
+ },
+ inject: ['endpoint', 'breadCrumbState'],
+ data() {
+ return {
+ tagsLoading: false,
+ pageInfo: {},
+ tags: [],
+ };
+ },
+ computed: {
+ currentPage() {
+ return this.pageInfo?.page || 1;
+ },
+ artifactDetail() {
+ const { project, image, digest } = this.$route.params;
+
+ return {
+ project,
+ image,
+ digest,
+ };
+ },
+ },
+ mounted() {
+ this.updateBreadcrumb();
+ this.fetchTagsData();
+ },
+ methods: {
+ updateBreadcrumb() {
+ const artifactPath = `${this.artifactDetail.project}/${this.artifactDetail.image}`;
+ const nameList = [artifactPath, this.artifactDetail.digest];
+ const hrefList = [`/${artifactPath}`, this.$route.path];
+
+ this.breadCrumbState.updateName(nameList);
+ this.breadCrumbState.updateHref(hrefList);
+ },
+ fetchPrevPage() {
+ const prevPageNum = this.currentPage - 1;
+ this.fetchTagsData(prevPageNum);
+ },
+ fetchNextPage() {
+ const nextPageNum = this.currentPage + 1;
+ this.fetchTagsData(nextPageNum);
+ },
+ fetchTagsData(requestPage) {
+ this.tagsLoading = true;
+
+ const params = {
+ page: requestPage,
+ requestPath: this.endpoint,
+ repoName: this.artifactDetail.image,
+ digest: this.artifactDetail.digest,
+ };
+
+ getHarborTags(params)
+ .then((res) => {
+ this.pageInfo = formatPagination(res.headers);
+
+ this.tags = (res?.data || []).map((tagInfo) => {
+ return convertObjectPropsToCamelCase(tagInfo);
+ });
+ })
+ .catch(() => {
+ createAlert({ message: FETCH_TAGS_ERROR_MESSAGE });
+ })
+ .finally(() => {
+ this.tagsLoading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <tags-header
+ :artifact-detail="artifactDetail"
+ :page-info="pageInfo"
+ :tags-loading="tagsLoading"
+ />
+ <tags-list
+ :tags="tags"
+ :is-loading="tagsLoading"
+ :page-info="pageInfo"
+ @prev-page="fetchPrevPage"
+ @next-page="fetchNextPage"
+ />
+ </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
index 9c69059c968..931a99649cb 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/list.vue
@@ -1,19 +1,32 @@
<script>
import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
-import { escape } from 'lodash';
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 { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ extractSortingDetail,
+ formatPagination,
+ parseFilter,
+ dockerBuildCommand,
+ dockerPushCommand,
+ dockerLoginCommand,
+} from '~/packages_and_registries/harbor_registry/utils';
+import { createAlert } from '~/flash';
import {
SORT_FIELDS,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
+ DEFAULT_PER_PAGE,
+ FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ EMPTY_IMAGES_TITLE,
+ EMPTY_IMAGES_MESSAGE,
+ HARBOR_REGISTRY_HELP_PAGE_PATH,
} from '~/packages_and_registries/harbor_registry/constants';
import Tracking from '~/tracking';
-import { harborListResponse } from '../mock_api';
+import { getHarborRepositoriesList } from '~/rest_api';
export default {
name: 'HarborListPage',
@@ -31,19 +44,28 @@ export default {
),
},
mixins: [Tracking.mixin()],
- inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
+ inject: [
+ 'endpoint',
+ 'repositoryUrl',
+ 'harborIntegrationProjectName',
+ 'projectName',
+ 'isGroupPage',
+ 'connectionError',
+ 'invalidPathError',
+ 'containersErrorImage',
+ 'noContainersImage',
+ ],
loader: {
repeat: 10,
width: 1000,
height: 40,
},
i18n: {
- CONNECTION_ERROR_TITLE,
- CONNECTION_ERROR_MESSAGE,
- EMPTY_RESULT_TITLE,
- EMPTY_RESULT_MESSAGE,
+ connectionErrorTitle: CONNECTION_ERROR_TITLE,
+ connectionErrorMessage: CONNECTION_ERROR_MESSAGE,
},
searchConfig: SORT_FIELDS,
+ helpPagePath: HARBOR_REGISTRY_HELP_PAGE_PATH,
data() {
return {
images: [],
@@ -56,42 +78,81 @@ export default {
};
},
computed: {
+ dockerCommand() {
+ return {
+ build: dockerBuildCommand({
+ repositoryUrl: this.repositoryUrl,
+ harborProjectName: this.harborIntegrationProjectName,
+ projectName: this.projectName,
+ }),
+ push: dockerPushCommand({
+ repositoryUrl: this.repositoryUrl,
+ harborProjectName: this.harborIntegrationProjectName,
+ projectName: this.projectName,
+ }),
+ login: dockerLoginCommand(this.repositoryUrl),
+ };
+ },
showCommands() {
- return !this.isLoading && !this.config?.isGroupPage && this.images?.length;
+ return !this.isLoading && !this.isGroupPage && this.images?.length;
},
showConnectionError() {
- return this.config.connectionError || this.config.invalidPathError;
+ return this.connectionError || this.invalidPathError;
+ },
+ currentPage() {
+ return this.pageInfo.page || 1;
+ },
+ emptyStateTexts() {
+ return {
+ title: this.name ? EMPTY_RESULT_TITLE : EMPTY_IMAGES_TITLE,
+ message: this.name ? EMPTY_RESULT_MESSAGE : EMPTY_IMAGES_MESSAGE,
+ };
},
},
methods: {
- fetchHarborImages() {
- // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
+ fetchHarborImages(requestPage) {
this.isLoading = true;
- harborListResponse()
+ const { orderBy, sort } = extractSortingDetail(this.sorting);
+ const sortOptions = `${orderBy} ${sort}`;
+
+ const params = {
+ requestPath: this.endpoint,
+ limit: DEFAULT_PER_PAGE,
+ search: this.name,
+ page: requestPage,
+ sort: sortOptions,
+ };
+
+ getHarborRepositoriesList(params)
.then((res) => {
- this.images = res?.repositories || [];
- this.totalCount = res?.totalCount || 0;
- this.pageInfo = res?.pageInfo || {};
+ this.images = (res?.data || []).map((item) => {
+ return convertObjectPropsToCamelCase(item);
+ });
+ const pagination = formatPagination(res.headers);
+
+ this.totalCount = pagination?.total || 0;
+ this.pageInfo = pagination;
+
this.isLoading = false;
})
- .catch(() => {});
+ .catch(() => {
+ createAlert({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
+ });
},
handleSearchUpdate({ sort, filters }) {
this.sorting = sort;
+ this.name = parseFilter(filters, 'name');
- const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
- this.name = escape(search?.value?.data);
-
- this.fetchHarborImages();
+ this.fetchHarborImages(1);
},
fetchPrevPage() {
- // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
- this.fetchHarborImages();
+ const prevPageNum = this.currentPage - 1;
+ this.fetchHarborImages(prevPageNum);
},
fetchNextPage() {
- // TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
- this.fetchHarborImages();
+ const nextPageNum = this.currentPage + 1;
+ this.fetchHarborImages(nextPageNum);
},
},
};
@@ -101,14 +162,14 @@ export default {
<div>
<gl-empty-state
v-if="showConnectionError"
- :title="$options.i18n.CONNECTION_ERROR_TITLE"
- :svg-path="config.containersErrorImage"
+ :title="$options.i18n.connectionErrorTitle"
+ :svg-path="containersErrorImage"
>
<template #description>
<p>
- <gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
+ <gl-sprintf :message="$options.i18n.connectionErrorMessage">
<template #docLink="{ content }">
- <gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
+ <gl-link :href="$options.helpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
@@ -117,17 +178,13 @@ export default {
</template>
</gl-empty-state>
<template v-else>
- <harbor-list-header
- :metadata-loading="isLoading"
- :images-count="totalCount"
- :help-page-path="config.helpPagePath"
- >
+ <harbor-list-header :metadata-loading="isLoading" :images-count="totalCount">
<template #commands>
<cli-commands
v-if="showCommands"
- :docker-build-command="dockerBuildCommand"
- :docker-push-command="dockerPushCommand"
- :docker-login-command="dockerLoginCommand"
+ :docker-build-command="dockerCommand.build"
+ :docker-push-command="dockerCommand.push"
+ :docker-login-command="dockerCommand.login"
/>
</template>
</harbor-list-header>
@@ -152,26 +209,24 @@ export default {
</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>
+ <harbor-list
+ v-if="images.length"
+ :images="images"
+ :metadata-loading="isLoading"
+ :page-info="pageInfo"
+ @prev-page="fetchPrevPage"
+ @next-page="fetchNextPage"
+ />
+ <gl-empty-state
+ v-else
+ :svg-path="noContainersImage"
+ data-testid="emptySearch"
+ :title="emptyStateTexts.title"
+ >
+ <template #description>
+ {{ emptyStateTexts.message }}
+ </template>
+ </gl-empty-state>
</template>
</template>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js
index 572dd382be3..5a792e30c62 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/router.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/router.js
@@ -3,6 +3,7 @@ import VueRouter from 'vue-router';
import { HARBOR_REGISTRY_TITLE } from './constants/index';
import List from './pages/list.vue';
import Details from './pages/details.vue';
+import HarborTags from './pages/harbor_tags.vue';
Vue.use(VueRouter);
@@ -22,10 +23,20 @@ export default function createRouter(base, breadCrumbState) {
},
{
name: 'details',
- path: '/:id',
+ path: '/:project/:image',
component: Details,
meta: {
nameGenerator: () => breadCrumbState.name,
+ hrefGenerator: () => breadCrumbState.href,
+ },
+ },
+ {
+ name: 'tags',
+ path: '/:project/:image/:digest',
+ component: HarborTags,
+ meta: {
+ nameGenerator: () => breadCrumbState.name,
+ hrefGenerator: () => breadCrumbState.href,
},
},
],
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
new file mode 100644
index 00000000000..13df303cffe
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
@@ -0,0 +1,84 @@
+import { isFinite } from 'lodash';
+import {
+ SORT_FIELD_MAPPING,
+ TOKEN_TYPE_TAG_NAME,
+} from '~/packages_and_registries/harbor_registry/constants';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
+import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
+
+export const extractSortingDetail = (parsedSorting = '') => {
+ const [orderBy, sortOrder] = parsedSorting.split('_');
+ if (orderBy && sortOrder) {
+ return {
+ orderBy: SORT_FIELD_MAPPING[orderBy],
+ sort: sortOrder.toLowerCase(),
+ };
+ }
+
+ return {
+ orderBy: '',
+ sort: '',
+ };
+};
+
+export const parseFilter = (filters = [], defaultPrefix = '') => {
+ /* eslint-disable @gitlab/require-i18n-strings */
+ const prefixMap = {
+ [FILTERED_SEARCH_TERM]: `${defaultPrefix}=`,
+ [TOKEN_TYPE_TAG_NAME]: 'tags=',
+ };
+ /* eslint-enable @gitlab/require-i18n-strings */
+ const filterList = [];
+ filters.forEach((i) => {
+ if (i.value?.data) {
+ const filterVal = i.value?.data;
+ const prefix = prefixMap[i.type];
+ const filterString = `${prefix}${filterVal}`;
+
+ filterList.push(filterString);
+ }
+ });
+
+ return filterList.join(',');
+};
+
+export const getNameFromParams = (fullName) => {
+ const names = fullName.split('/');
+ return {
+ projectName: names[0] || '',
+ imageName: names[1] || '',
+ };
+};
+
+export const formatPagination = (headers) => {
+ const pagination = parseIntPagination(normalizeHeaders(headers)) || {};
+
+ if (pagination.nextPage || pagination.previousPage) {
+ pagination.hasNextPage = isFinite(pagination.nextPage);
+ pagination.hasPreviousPage = isFinite(pagination.previousPage);
+ }
+
+ return pagination;
+};
+
+/* eslint-disable @gitlab/require-i18n-strings */
+export const dockerBuildCommand = ({ repositoryUrl, harborProjectName, projectName = '' }) => {
+ return `docker build -t ${repositoryUrl}/${harborProjectName}/${projectName} .`;
+};
+
+export const dockerPushCommand = ({ repositoryUrl, harborProjectName, projectName = '' }) => {
+ return `docker push ${repositoryUrl}/${harborProjectName}/${projectName}`;
+};
+
+export const dockerLoginCommand = (repositoryUrl) => {
+ return `docker login ${repositoryUrl}`;
+};
+
+export const artifactPullCommand = ({ repositoryUrl, harborProjectName, imageName, digest }) => {
+ return `docker pull ${repositoryUrl}/${harborProjectName}/${imageName}@${digest}`;
+};
+
+export const tagPullCommand = ({ repositoryUrl, harborProjectName, imageName, tag }) => {
+ return `docker pull ${repositoryUrl}/${harborProjectName}/${imageName}:${tag}`;
+};
+/* eslint-enable @gitlab/require-i18n-strings */
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index 425fb4596fd..fd099ee4e69 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -114,7 +114,7 @@ export default {
deleteModalContent: s__(
`PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
),
- deleteFileModalTitle: s__(`PackageRegistry|Delete Package File`),
+ deleteFileModalTitle: s__(`PackageRegistry|Delete package asset`),
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
index 28bfb82093c..e45b88bc6d5 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/package_files.vue
@@ -84,14 +84,14 @@ export default {
},
},
i18n: {
- deleteFile: __('Delete file'),
+ deleteFile: __('Delete asset'),
},
};
</script>
<template>
<div>
- <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
+ <h3 class="gl-font-lg gl-mt-5">{{ __('Assets') }}</h3>
<gl-table
:fields="filesTableHeaderFields"
:items="filesTableRows"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
index a465fea0b74..dab4a051d0c 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list.vue
@@ -98,7 +98,7 @@ export default {
</div>
<template v-else>
- <div data-qa-selector="packages-table">
+ <div data-testid="packages-table">
<packages-list-row
v-for="packageEntity in list"
:key="packageEntity.id"
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue
index 3c6b8344c34..cc52235eaf3 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue
@@ -78,7 +78,7 @@ export default {
</script>
<template>
- <list-item data-qa-selector="package_row" :disabled="disabledRow">
+ <list-item data-testid="package-row" :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue
index 122d444e859..f581469b12b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/installation_commands.vue
@@ -14,16 +14,17 @@ import NpmInstallation from './npm_installation.vue';
import NugetInstallation from './nuget_installation.vue';
import PypiInstallation from './pypi_installation.vue';
+const components = {
+ [PACKAGE_TYPE_CONAN]: ConanInstallation,
+ [PACKAGE_TYPE_MAVEN]: MavenInstallation,
+ [PACKAGE_TYPE_NPM]: NpmInstallation,
+ [PACKAGE_TYPE_NUGET]: NugetInstallation,
+ [PACKAGE_TYPE_PYPI]: PypiInstallation,
+ [PACKAGE_TYPE_COMPOSER]: ComposerInstallation,
+};
+
export default {
name: 'InstallationCommands',
- components: {
- [PACKAGE_TYPE_CONAN]: ConanInstallation,
- [PACKAGE_TYPE_MAVEN]: MavenInstallation,
- [PACKAGE_TYPE_NPM]: NpmInstallation,
- [PACKAGE_TYPE_NUGET]: NugetInstallation,
- [PACKAGE_TYPE_PYPI]: PypiInstallation,
- [PACKAGE_TYPE_COMPOSER]: ComposerInstallation,
- },
props: {
packageEntity: {
type: Object,
@@ -32,7 +33,7 @@ export default {
},
computed: {
installationComponent() {
- return this.$options.components[this.packageEntity.packageType];
+ return components[this.packageEntity.packageType];
},
},
};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
index b872294d2cf..8eb8654cddd 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -139,7 +139,7 @@ export default {
},
},
i18n: {
- deleteFile: __('Delete file'),
+ deleteFile: __('Delete asset'),
deleteSelected: s__('PackageRegistry|Delete selected'),
moreActionsText: __('More actions'),
},
@@ -149,7 +149,7 @@ export default {
<template>
<div class="gl-pt-6">
<div class="gl-display-flex gl-align-items-center gl-justify-content-space-between">
- <h3 class="gl-font-lg gl-mt-5">{{ __('Files') }}</h3>
+ <h3 class="gl-font-lg gl-mt-5">{{ __('Assets') }}</h3>
<gl-button
v-if="canDelete"
:disabled="isLoading || !areFilesSelected"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 04faff1a75b..7a000aca0f2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -90,7 +90,7 @@ export default {
</script>
<template>
- <list-item data-qa-selector="package_row">
+ <list-item data-testid="package-row">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<router-link
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
index a6ac2eb1b2b..e84f181e9b2 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/packages_list.vue
@@ -151,7 +151,7 @@ export default {
@primaryAction="showConfirmationModal"
>{{ $options.i18n.errorMessageBodyAlert }}</gl-alert
>
- <div data-qa-selector="packages-table">
+ <div data-testid="packages-table">
<packages-list-row
v-for="packageEntity in list"
:key="packageEntity.id"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index 5b2a347a4ee..06a04ee248a 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -79,10 +79,10 @@ export const TRACKING_LABEL_PACKAGE_HISTORY = 'package_history';
export const SHOW_DELETE_SUCCESS_ALERT = 'showSuccessDeleteAlert';
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package file.',
+ 'PackageRegistry|Something went wrong while deleting the package asset.',
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
- 'PackageRegistry|Package file deleted successfully',
+ 'PackageRegistry|Package asset deleted successfully',
);
export const DELETE_PACKAGE_FILES_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package assets.',
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index e83962bb608..c10fc914d56 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -256,7 +256,7 @@ export default {
deleteModalContent: s__(
`PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
),
- deleteFileModalTitle: s__(`PackageRegistry|Delete Package File`),
+ deleteFileModalTitle: s__(`PackageRegistry|Delete package asset`),
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
deleted file mode 100644
index 51a97aead49..00000000000
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/duplicates_settings.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<script>
-import { GlToggle, GlFormGroup, GlFormInput } from '@gitlab/ui';
-import { isEqual } from 'lodash';
-
-import {
- DUPLICATES_TOGGLE_LABEL,
- DUPLICATES_SETTING_EXCEPTION_TITLE,
- DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
-} from '~/packages_and_registries/settings/group/constants';
-
-export default {
- name: 'DuplicatesSettings',
- i18n: {
- DUPLICATES_TOGGLE_LABEL,
- DUPLICATES_SETTING_EXCEPTION_TITLE,
- DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
- },
- components: {
- GlToggle,
- GlFormGroup,
- GlFormInput,
- },
- props: {
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- duplicatesAllowed: {
- type: Boolean,
- default: false,
- required: false,
- },
- duplicateExceptionRegex: {
- type: String,
- default: '',
- required: false,
- },
- duplicateExceptionRegexError: {
- type: String,
- default: '',
- required: false,
- },
- modelNames: {
- type: Object,
- required: true,
- validator(value) {
- return isEqual(Object.keys(value), ['allowed', 'exception']);
- },
- },
- toggleQaSelector: {
- type: String,
- required: false,
- default: null,
- },
- labelQaSelector: {
- type: String,
- required: false,
- default: null,
- },
- },
- computed: {
- isExceptionRegexValid() {
- return !this.duplicateExceptionRegexError;
- },
- },
- methods: {
- update(type, value) {
- this.$emit('update', { [type]: value });
- },
- },
-};
-</script>
-
-<template>
- <form>
- <gl-toggle
- :data-qa-selector="toggleQaSelector"
- :label="$options.i18n.DUPLICATES_TOGGLE_LABEL"
- :value="!duplicatesAllowed"
- :disabled="loading"
- @change="update(modelNames.allowed, !$event)"
- />
- <gl-form-group
- v-if="!duplicatesAllowed"
- class="gl-mt-4"
- :label="$options.i18n.DUPLICATES_SETTING_EXCEPTION_TITLE"
- label-size="sm"
- :state="isExceptionRegexValid"
- :invalid-feedback="duplicateExceptionRegexError"
- :description="$options.i18n.DUPLICATES_SETTINGS_EXCEPTION_LEGEND"
- label-for="maven-duplicated-settings-regex-input"
- >
- <gl-form-input
- id="maven-duplicated-settings-regex-input"
- :disabled="loading"
- size="lg"
- :value="duplicateExceptionRegex"
- @change="update(modelNames.exception, $event)"
- />
- </gl-form-group>
- </form>
-</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
new file mode 100644
index 00000000000..9ac1673dbf3
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/exceptions_input.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+
+import {
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+} from '~/packages_and_registries/settings/group/constants';
+
+export default {
+ name: 'ExceptionsInput',
+ i18n: {
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_SETTINGS_EXCEPTION_LEGEND,
+ },
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ duplicatesAllowed: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ duplicateExceptionRegex: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ duplicateExceptionRegexError: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ id: {
+ type: String,
+ required: true,
+ },
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isExceptionRegexValid() {
+ return !this.duplicateExceptionRegexError;
+ },
+ },
+ methods: {
+ update(type, value) {
+ this.$emit('update', { [type]: value });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group
+ class="gl-mb-0"
+ :label="$options.i18n.DUPLICATES_SETTING_EXCEPTION_TITLE"
+ label-sr-only
+ :invalid-feedback="duplicateExceptionRegexError"
+ :label-for="id"
+ >
+ <gl-form-input
+ :id="id"
+ :disabled="duplicatesAllowed || loading"
+ size="lg"
+ :value="duplicateExceptionRegex"
+ :state="isExceptionRegexValid"
+ @change="update(name, $event)"
+ />
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue
deleted file mode 100644
index e5f63fe8d0d..00000000000
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/generic_settings.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-
-export default {
- name: 'GenericSettings',
- components: {
- SettingsTitles,
- },
- i18n: {
- title: s__('PackageRegistry|Generic'),
- subTitle: s__('PackageRegistry|Settings for Generic packages'),
- },
- modelNames: {
- allowed: 'genericDuplicatesAllowed',
- exception: 'genericDuplicateExceptionRegex',
- },
-};
-</script>
-
-<template>
- <div>
- <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" />
- <slot :model-names="$options.modelNames"></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
deleted file mode 100644
index a1cbd695f34..00000000000
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/maven_settings.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { s__ } from '~/locale';
-import SettingsTitles from '~/packages_and_registries/settings/group/components/settings_titles.vue';
-
-export default {
- name: 'MavenSettings',
- components: {
- SettingsTitles,
- },
- i18n: {
- title: s__('PackageRegistry|Maven'),
- subTitle: s__('PackageRegistry|Settings for Maven packages'),
- },
- modelNames: {
- allowed: 'mavenDuplicatesAllowed',
- exception: 'mavenDuplicateExceptionRegex',
- },
-};
-</script>
-
-<template>
- <div>
- <settings-titles :title="$options.i18n.title" :sub-title="$options.i18n.subTitle" />
- <slot :model-names="$options.modelNames"></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index abb9f02d290..de087a8fcc5 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -1,27 +1,50 @@
<script>
-import DuplicatesSettings from '~/packages_and_registries/settings/group/components/duplicates_settings.vue';
-import GenericSettings from '~/packages_and_registries/settings/group/components/generic_settings.vue';
-import MavenSettings from '~/packages_and_registries/settings/group/components/maven_settings.vue';
+import { GlTableLite, GlToggle } from '@gitlab/ui';
import {
+ GENERIC_PACKAGE_FORMAT,
+ MAVEN_PACKAGE_FORMAT,
+ PACKAGE_FORMATS_TABLE_HEADER,
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_TOGGLE_LABEL,
} from '~/packages_and_registries/settings/group/constants';
import updateNamespacePackageSettings from '~/packages_and_registries/settings/group/graphql/mutations/update_group_packages_settings.mutation.graphql';
import { updateGroupPackageSettings } from '~/packages_and_registries/settings/group/graphql/utils/cache_update';
import { updateGroupPackagesSettingsOptimisticResponse } from '~/packages_and_registries/settings/group/graphql/utils/optimistic_responses';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import ExceptionsInput from '~/packages_and_registries/settings/group/components/exceptions_input.vue';
export default {
name: 'PackageSettings',
i18n: {
PACKAGE_SETTINGS_HEADER,
PACKAGE_SETTINGS_DESCRIPTION,
+ DUPLICATES_SETTING_EXCEPTION_TITLE,
+ DUPLICATES_TOGGLE_LABEL,
},
+ tableHeaderFields: [
+ {
+ key: 'packageFormat',
+ label: PACKAGE_FORMATS_TABLE_HEADER,
+ thClass: 'gl-bg-gray-10!',
+ },
+ {
+ key: 'allowDuplicates',
+ label: DUPLICATES_TOGGLE_LABEL,
+ thClass: 'gl-bg-gray-10!',
+ },
+ {
+ key: 'exceptions',
+ label: DUPLICATES_SETTING_EXCEPTION_TITLE,
+ thClass: 'gl-bg-gray-10!',
+ },
+ ],
components: {
SettingsBlock,
- MavenSettings,
- GenericSettings,
- DuplicatesSettings,
+ GlTableLite,
+ GlToggle,
+ ExceptionsInput,
},
inject: ['groupPath'],
props: {
@@ -40,6 +63,37 @@ export default {
errors: {},
};
},
+ computed: {
+ tableRows() {
+ return [
+ {
+ id: 'maven-duplicated-settings-regex-input',
+ format: MAVEN_PACKAGE_FORMAT,
+ duplicatesAllowed: this.packageSettings.mavenDuplicatesAllowed,
+ duplicateExceptionRegex: this.packageSettings.mavenDuplicateExceptionRegex,
+ duplicateExceptionRegexError: this.errors.mavenDuplicateExceptionRegex,
+ modelNames: {
+ allowed: 'mavenDuplicatesAllowed',
+ exception: 'mavenDuplicateExceptionRegex',
+ },
+ testid: 'maven-settings',
+ dataQaSelector: 'allow_duplicates_toggle',
+ },
+ {
+ id: 'generic-duplicated-settings-regex-input',
+ format: GENERIC_PACKAGE_FORMAT,
+ duplicatesAllowed: this.packageSettings.genericDuplicatesAllowed,
+ duplicateExceptionRegex: this.packageSettings.genericDuplicateExceptionRegex,
+ duplicateExceptionRegexError: this.errors.genericDuplicateExceptionRegex,
+ modelNames: {
+ allowed: 'genericDuplicatesAllowed',
+ exception: 'genericDuplicateExceptionRegex',
+ },
+ testid: 'generic-settings',
+ },
+ ];
+ },
+ },
methods: {
async updateSettings(payload) {
this.errors = {};
@@ -79,6 +133,9 @@ export default {
this.$emit('error');
}
},
+ update(type, value) {
+ this.updateSettings({ [type]: value });
+ },
},
};
</script>
@@ -92,32 +149,40 @@ export default {
</span>
</template>
<template #default>
- <maven-settings data-testid="maven-settings">
- <template #default="{ modelNames }">
- <duplicates-settings
- :duplicates-allowed="packageSettings.mavenDuplicatesAllowed"
- :duplicate-exception-regex="packageSettings.mavenDuplicateExceptionRegex"
- :duplicate-exception-regex-error="errors.mavenDuplicateExceptionRegex"
- :model-names="modelNames"
- :loading="isLoading"
- toggle-qa-selector="reject_duplicates_toggle"
- label-qa-selector="reject_duplicates_label"
- @update="updateSettings"
- />
- </template>
- </maven-settings>
- <generic-settings class="gl-mt-6" data-testid="generic-settings">
- <template #default="{ modelNames }">
- <duplicates-settings
- :duplicates-allowed="packageSettings.genericDuplicatesAllowed"
- :duplicate-exception-regex="packageSettings.genericDuplicateExceptionRegex"
- :duplicate-exception-regex-error="errors.genericDuplicateExceptionRegex"
- :model-names="modelNames"
- :loading="isLoading"
- @update="updateSettings"
- />
- </template>
- </generic-settings>
+ <form>
+ <gl-table-lite
+ :fields="$options.tableHeaderFields"
+ :items="tableRows"
+ stacked="sm"
+ :tbody-tr-attr="(item) => ({ 'data-testid': item.testid })"
+ >
+ <template #cell(packageFormat)="{ item }">
+ <span class="gl-md-pt-3">{{ item.format }}</span>
+ </template>
+ <template #cell(allowDuplicates)="{ item }">
+ <gl-toggle
+ :data-qa-selector="item.dataQaSelector"
+ :label="$options.i18n.DUPLICATES_TOGGLE_LABEL"
+ :value="item.duplicatesAllowed"
+ :disabled="isLoading"
+ label-position="hidden"
+ class="gl-align-items-flex-end gl-sm-align-items-flex-start"
+ @change="update(item.modelNames.allowed, $event)"
+ />
+ </template>
+ <template #cell(exceptions)="{ item }">
+ <exceptions-input
+ :id="item.id"
+ :duplicates-allowed="item.duplicatesAllowed"
+ :duplicate-exception-regex="item.duplicateExceptionRegex"
+ :duplicate-exception-regex-error="item.duplicateExceptionRegexError"
+ :name="item.modelNames.exception"
+ :loading="isLoading"
+ @update="updateSettings"
+ />
+ </template>
+ </gl-table-lite>
+ </form>
</template>
</settings-block>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
deleted file mode 100644
index 1e93875c1e3..00000000000
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/settings_titles.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- name: 'SettingsTitle',
- props: {
- title: {
- type: String,
- required: true,
- },
- subTitle: {
- type: String,
- required: false,
- default: '',
- },
- },
-};
-</script>
-
-<template>
- <div>
- <h5 class="gl-border-b-solid gl-border-b-1 gl-border-gray-200 gl-pb-3">
- {{ title }}
- </h5>
- <p v-if="subTitle">{{ subTitle }}</p>
- <slot></slot>
- </div>
-</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index 34764663892..2dd6d3f76f6 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -5,10 +5,11 @@ export const PACKAGE_SETTINGS_HEADER = s__('PackageRegistry|Duplicate packages')
export const PACKAGE_SETTINGS_DESCRIPTION = s__(
'PackageRegistry|Allow packages with the same name and version to be uploaded to the registry. The newest version of a package is always used when installing.',
);
+export const PACKAGE_FORMATS_TABLE_HEADER = s__('PackageRegistry|Package formats');
+export const MAVEN_PACKAGE_FORMAT = s__('PackageRegistry|Maven');
+export const GENERIC_PACKAGE_FORMAT = s__('PackageRegistry|Generic');
-export const DUPLICATES_TOGGLE_LABEL = s__(
- 'PackageRegistry|Reject packages with the same name and version',
-);
+export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
'PackageRegistry|Publish packages if their name or version matches this regex.',
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
new file mode 100644
index 00000000000..72e68aca070
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/cleanup_image_tags.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { isEqual, get, isEmpty } from 'lodash';
+import {
+ CONTAINER_CLEANUP_POLICY_TITLE,
+ CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ UNAVAILABLE_FEATURE_TITLE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ UNAVAILABLE_USER_FEATURE_TEXT,
+ 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 ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
+
+export default {
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ ContainerExpirationPolicyForm,
+ },
+ inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
+ i18n: {
+ CONTAINER_CLEANUP_POLICY_TITLE,
+ CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ UNAVAILABLE_FEATURE_TITLE,
+ UNAVAILABLE_FEATURE_INTRO_TEXT,
+ FETCH_SETTINGS_ERROR_MESSAGE,
+ },
+ apollo: {
+ containerExpirationPolicy: {
+ query: expirationPolicyQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ };
+ },
+ update: (data) => data.project?.containerExpirationPolicy,
+ result({ data }) {
+ this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
+ },
+ error(e) {
+ this.fetchSettingsError = e;
+ },
+ },
+ },
+ data() {
+ return {
+ fetchSettingsError: false,
+ containerExpirationPolicy: null,
+ workingCopy: {},
+ };
+ },
+ computed: {
+ isEnabled() {
+ return this.containerExpirationPolicy || this.enableHistoricEntries;
+ },
+ showDisabledFormMessage() {
+ return !this.isEnabled && !this.fetchSettingsError;
+ },
+ unavailableFeatureMessage() {
+ return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
+ },
+ isEdited() {
+ if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
+ return false;
+ }
+ return !isEqual(this.containerExpirationPolicy, this.workingCopy);
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="container-expiration-policy-project-settings">
+ <h4 data-testid="title">{{ $options.i18n.CONTAINER_CLEANUP_POLICY_TITLE }}</h4>
+ <p data-testid="description">
+ <gl-sprintf :message="$options.i18n.CONTAINER_CLEANUP_POLICY_DESCRIPTION">
+ <template #link="{ content }">
+ <gl-link :href="helpPagePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <container-expiration-policy-form
+ v-if="isEnabled"
+ v-model="workingCopy"
+ :is-loading="$apollo.queries.containerExpirationPolicy.loading"
+ :is-edited="isEdited"
+ />
+ <template v-else>
+ <gl-alert
+ v-if="showDisabledFormMessage"
+ :dismissible="false"
+ :title="$options.i18n.UNAVAILABLE_FEATURE_TITLE"
+ variant="tip"
+ >
+ {{ $options.i18n.UNAVAILABLE_FEATURE_INTRO_TEXT }}
+
+ <gl-sprintf :message="unavailableFeatureMessage">
+ <template #link="{ content }">
+ <gl-link :href="adminSettingsPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.FETCH_SETTINGS_ERROR_MESSAGE" />
+ </gl-alert>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
index 1c44d2bc38b..b003b6aeb6b 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy.vue
@@ -1,9 +1,11 @@
<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { isEqual, get, isEmpty } from 'lodash';
+import { GlAlert, GlSprintf, GlLink, GlCard, GlButton } from '@gitlab/ui';
import {
CONTAINER_CLEANUP_POLICY_TITLE,
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ CONTAINER_CLEANUP_POLICY_EDIT_RULES,
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
+ CONTAINER_CLEANUP_POLICY_SET_RULES,
FETCH_SETTINGS_ERROR_MESSAGE,
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
@@ -13,20 +15,29 @@ import {
import expirationPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_expiration_policy.query.graphql';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
-import ContainerExpirationPolicyForm from './container_expiration_policy_form.vue';
-
export default {
components: {
SettingsBlock,
GlAlert,
GlSprintf,
GlLink,
- ContainerExpirationPolicyForm,
+ GlCard,
+ GlButton,
},
- inject: ['projectPath', 'isAdmin', 'adminSettingsPath', 'enableHistoricEntries', 'helpPagePath'],
+ inject: [
+ 'projectPath',
+ 'isAdmin',
+ 'adminSettingsPath',
+ 'enableHistoricEntries',
+ 'helpPagePath',
+ 'cleanupSettingsPath',
+ ],
i18n: {
CONTAINER_CLEANUP_POLICY_TITLE,
CONTAINER_CLEANUP_POLICY_DESCRIPTION,
+ CONTAINER_CLEANUP_POLICY_EDIT_RULES,
+ CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION,
+ CONTAINER_CLEANUP_POLICY_SET_RULES,
UNAVAILABLE_FEATURE_TITLE,
UNAVAILABLE_FEATURE_INTRO_TEXT,
FETCH_SETTINGS_ERROR_MESSAGE,
@@ -40,9 +51,6 @@ export default {
};
},
update: (data) => data.project?.containerExpirationPolicy,
- result({ data }) {
- this.workingCopy = { ...get(data, 'project.containerExpirationPolicy', {}) };
- },
error(e) {
this.fetchSettingsError = e;
},
@@ -52,29 +60,25 @@ export default {
return {
fetchSettingsError: false,
containerExpirationPolicy: null,
- workingCopy: {},
};
},
computed: {
- isDisabled() {
- return !(this.containerExpirationPolicy || this.enableHistoricEntries);
+ isCleanupEnabled() {
+ return this.containerExpirationPolicy?.enabled ?? false;
+ },
+ isEnabled() {
+ return this.containerExpirationPolicy || this.enableHistoricEntries;
},
showDisabledFormMessage() {
- return this.isDisabled && !this.fetchSettingsError;
+ return !this.isEnabled && !this.fetchSettingsError;
},
unavailableFeatureMessage() {
return this.isAdmin ? UNAVAILABLE_ADMIN_FEATURE_TEXT : UNAVAILABLE_USER_FEATURE_TEXT;
},
- isEdited() {
- if (isEmpty(this.containerExpirationPolicy) && isEmpty(this.workingCopy)) {
- return false;
- }
- return !isEqual(this.containerExpirationPolicy, this.workingCopy);
- },
- },
- methods: {
- restoreOriginal() {
- this.workingCopy = { ...this.containerExpirationPolicy };
+ cleanupRulesButtonText() {
+ return this.isCleanupEnabled
+ ? this.$options.i18n.CONTAINER_CLEANUP_POLICY_EDIT_RULES
+ : this.$options.i18n.CONTAINER_CLEANUP_POLICY_SET_RULES;
},
},
};
@@ -93,13 +97,19 @@ export default {
</span>
</template>
<template #default>
- <container-expiration-policy-form
- v-if="!isDisabled"
- v-model="workingCopy"
- :is-loading="$apollo.queries.containerExpirationPolicy.loading"
- :is-edited="isEdited"
- @reset="restoreOriginal"
- />
+ <gl-card v-if="isEnabled">
+ <p data-testid="description">
+ {{ $options.i18n.CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION }}
+ </p>
+ <gl-button
+ data-testid="rules-button"
+ :href="cleanupSettingsPath"
+ category="secondary"
+ variant="confirm"
+ >
+ {{ cleanupRulesButtonText }}
+ </gl-button>
+ </gl-card>
<template v-else>
<gl-alert
v-if="showDisabledFormMessage"
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
index ae2d5f4fbc5..11d8732426d 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/container_expiration_policy_form.vue
@@ -1,8 +1,9 @@
<script>
import { GlCard, GlButton, GlSprintf } from '@gitlab/ui';
+import { objectToQuery, visitUrl } from '~/lib/utils/url_utility';
import {
UPDATE_SETTINGS_ERROR_MESSAGE,
- UPDATE_SETTINGS_SUCCESS_MESSAGE,
+ SHOW_SETUP_SUCCESS_ALERT,
SET_CLEANUP_POLICY_BUTTON,
KEEP_HEADER_TEXT,
KEEP_INFO_TEXT,
@@ -37,7 +38,7 @@ export default {
ExpirationRunText,
},
mixins: [Tracking.mixin()],
- inject: ['projectPath'],
+ inject: ['projectPath', 'projectSettingsPath'],
props: {
value: {
type: Object,
@@ -95,10 +96,10 @@ export default {
return Object.values(this.localErrors).every((error) => error);
},
isSubmitButtonDisabled() {
- return !this.fieldsAreValid || this.showLoadingIcon;
+ return !this.isEdited || !this.fieldsAreValid || this.showLoadingIcon;
},
isCancelButtonDisabled() {
- return !this.isEdited || this.isLoading || this.mutationLoading;
+ return this.isLoading || this.mutationLoading;
},
isFieldDisabled() {
return this.showLoadingIcon || !this.value.enabled;
@@ -119,12 +120,6 @@ export default {
findDefaultOption(option) {
return this.value[option] || this.$options.formOptions[option].find((f) => f.default)?.key;
},
- reset() {
- this.track('reset_form');
- this.apiErrors = {};
- this.localErrors = {};
- this.$emit('reset');
- },
setApiErrors(response) {
this.apiErrors = response.graphQLErrors.reduce((acc, curr) => {
curr.extensions.problems.forEach((item) => {
@@ -168,7 +163,7 @@ export default {
const customError = this.encapsulateError('nameRegex', errorMessage);
throw customError;
} else {
- this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE);
+ this.navigateToSettingsWithSuccessAlert();
}
})
.catch((error) => {
@@ -183,12 +178,17 @@ export default {
this.$emit('input', { ...this.value, [model]: newValue });
this.apiErrors[model] = undefined;
},
+ navigateToSettingsWithSuccessAlert() {
+ const alertQuery = objectToQuery({ [SHOW_SETUP_SUCCESS_ALERT]: true });
+
+ visitUrl(`${this.projectSettingsPath}?${alertQuery}`);
+ },
},
};
</script>
<template>
- <form ref="form-element" @submit.prevent="submit" @reset.prevent="reset">
+ <form @submit.prevent="submit">
<expiration-toggle
:value="prefilledForm.enabled"
:disabled="showLoadingIcon"
@@ -199,7 +199,7 @@ export default {
<div class="gl-display-flex gl-mt-7">
<expiration-dropdown
- v-model="prefilledForm.cadence"
+ :value="prefilledForm.cadence"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.cadence"
:label="$options.i18n.CADENCE_LABEL"
@@ -231,7 +231,7 @@ export default {
</gl-sprintf>
</p>
<expiration-dropdown
- v-model="prefilledForm.keepN"
+ :value="prefilledForm.keepN"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.keepN"
:label="$options.i18n.KEEP_N_LABEL"
@@ -270,7 +270,7 @@ export default {
</gl-sprintf>
</p>
<expiration-dropdown
- v-model="prefilledForm.olderThan"
+ :value="prefilledForm.olderThan"
:disabled="isFieldDisabled"
:form-options="$options.formOptions.olderThan"
:label="$options.i18n.EXPIRATION_SCHEDULE_LABEL"
@@ -306,7 +306,7 @@ export default {
</gl-button>
<gl-button
data-testid="cancel-button"
- type="reset"
+ :href="projectSettingsPath"
:disabled="isCancelButtonDisabled"
class="gl-mr-4"
>
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 710cfe7b1eb..2c1368262f2 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
@@ -1,18 +1,57 @@
<script>
+import { GlAlert } from '@gitlab/ui';
+import { historyReplaceState } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import {
+ SHOW_SETUP_SUCCESS_ALERT,
+ UPDATE_SETTINGS_SUCCESS_MESSAGE,
+} from '~/packages_and_registries/settings/project/constants';
import ContainerExpirationPolicy from './container_expiration_policy.vue';
import PackagesCleanupPolicy from './packages_cleanup_policy.vue';
export default {
components: {
ContainerExpirationPolicy,
+ GlAlert,
PackagesCleanupPolicy,
},
inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'],
+ i18n: {
+ UPDATE_SETTINGS_SUCCESS_MESSAGE,
+ },
+ data() {
+ return {
+ showAlert: false,
+ };
+ },
+ mounted() {
+ this.checkAlert();
+ },
+ methods: {
+ checkAlert() {
+ const showAlert = getParameterByName(SHOW_SETUP_SUCCESS_ALERT);
+
+ if (showAlert) {
+ this.showAlert = true;
+ const cleanUrl = window.location.href.split('?')[0];
+ historyReplaceState(cleanUrl);
+ }
+ },
+ },
};
</script>
<template>
<div>
+ <gl-alert
+ v-if="showAlert"
+ variant="success"
+ class="gl-mt-5"
+ dismissible
+ @dismiss="showAlert = false"
+ >
+ {{ $options.i18n.UPDATE_SETTINGS_SUCCESS_MESSAGE }}
+ </gl-alert>
<packages-cleanup-policy v-if="showPackageRegistrySettings" />
<container-expiration-policy v-if="showContainerRegistrySettings" />
</div>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/constants.js b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
index fcb4a8ee297..a9b47cbd343 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/constants.js
@@ -4,6 +4,13 @@ export const CONTAINER_CLEANUP_POLICY_TITLE = s__(`ContainerRegistry|Clean up im
export const CONTAINER_CLEANUP_POLICY_DESCRIPTION = s__(
`ContainerRegistry|Save storage space by automatically deleting tags from the container registry and keeping the ones you want. %{linkStart}How does cleanup work?%{linkEnd}`,
);
+export const CONTAINER_CLEANUP_POLICY_RULES_DESCRIPTION = s__(
+ 'ContainerRegistry|Set rules to automatically remove unused packages to save storage space.',
+);
+export const CONTAINER_CLEANUP_POLICY_EDIT_RULES = s__('ContainerRegistry|Edit cleanup rules');
+export const CONTAINER_CLEANUP_POLICY_SET_RULES = s__('ContainerRegistry|Set cleanup rules');
+export const SHOW_SETUP_SUCCESS_ALERT = 'showSetupSuccessAlert';
+
export const SET_CLEANUP_POLICY_BUTTON = __('Save changes');
export const UNAVAILABLE_FEATURE_TITLE = s__(
`ContainerRegistry|Cleanup policy for tags is disabled`,
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 daf1da6eac8..57c8d07e620 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
@@ -18,6 +18,7 @@ export default () => {
enableHistoricEntries,
projectPath,
adminSettingsPath,
+ cleanupSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showContainerRegistrySettings,
@@ -34,6 +35,7 @@ export default () => {
enableHistoricEntries: parseBoolean(enableHistoricEntries),
projectPath,
adminSettingsPath,
+ cleanupSettingsPath,
tagsRegexHelpPagePath,
helpPagePath,
showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js
new file mode 100644
index 00000000000..b1401c448a1
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_cleanup_tags_bundle.js
@@ -0,0 +1,41 @@
+import { GlToast } from '@gitlab/ui';
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
+import CleanupImageTags from './components/cleanup_image_tags.vue';
+import { apolloProvider } from './graphql/index';
+
+Vue.use(GlToast);
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-registry-settings-cleanup-image-tags');
+ if (!el) {
+ return null;
+ }
+ const {
+ isAdmin,
+ enableHistoricEntries,
+ projectPath,
+ adminSettingsPath,
+ projectSettingsPath,
+ tagsRegexHelpPagePath,
+ helpPagePath,
+ } = el.dataset;
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ isAdmin: parseBoolean(isAdmin),
+ enableHistoricEntries: parseBoolean(enableHistoricEntries),
+ projectPath,
+ adminSettingsPath,
+ projectSettingsPath,
+ tagsRegexHelpPagePath,
+ helpPagePath,
+ },
+ render(createElement) {
+ return createElement(CleanupImageTags, {});
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
index b2b1d2c8212..363304c20ce 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
@@ -18,6 +18,11 @@ export default {
type: String,
required: true,
},
+ tokens: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -68,7 +73,7 @@ export default {
v-if="mountRegistrySearch"
:filters="filters"
:sorting="sorting"
- :tokens="$options.tokens"
+ :tokens="tokens"
:sortable-fields="sortableFields"
@sorting:changed="updateSortingAndEmitUpdate"
@filter:changed="updateFilters"
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue b/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue
index 0458b914b58..7740924b058 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/settings_block.vue
@@ -1,7 +1,7 @@
<template>
<section class="settings gl-py-7">
- <div class="gl-lg-display-flex gl-gap-6">
- <div class="gl-lg-w-40p gl-pr-10 gl-flex-shrink-0">
+ <div class="row">
+ <div class="col-lg-4">
<h4>
<slot name="title"></slot>
</h4>
@@ -9,7 +9,7 @@
<slot name="description"></slot>
</p>
</div>
- <div class="gl-pt-3 gl-flex-grow-1">
+ <div class="col-lg-8 gl-pt-3">
<slot></slot>
</div>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
index 6744e821565..7fd440d0b27 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
@@ -33,10 +33,10 @@ export const DELETE_PACKAGE_ERROR_MESSAGE = s__(
'PackageRegistry|Something went wrong while deleting the package.',
);
export const DELETE_PACKAGE_FILE_ERROR_MESSAGE = s__(
- 'PackageRegistry|Something went wrong while deleting the package file.',
+ 'PackageRegistry|Something went wrong while deleting the package asset.',
);
export const DELETE_PACKAGE_FILE_SUCCESS_MESSAGE = s__(
- 'PackageRegistry|Package file deleted successfully',
+ 'PackageRegistry|Package asset deleted successfully',
);
export const PACKAGE_ERROR_STATUS = 'error';