diff options
Diffstat (limited to 'app/assets/javascripts/packages_and_registries/harbor_registry/components')
9 files changed, 570 insertions, 27 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> |