Commit 94a33f80 authored by orozot's avatar orozot Committed by Nicolò Maria Mezzopera

Add harbor registry list page

Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/352235
Changelog: added
parent f34e394b
<script>
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
export default {
name: 'HarborList',
components: {
RegistryList,
HarborListRow,
},
props: {
images: {
type: Array,
required: true,
},
metadataLoading: {
type: Boolean,
default: false,
required: false,
},
pageInfo: {
type: Object,
required: true,
},
},
};
</script>
<template>
<registry-list
:items="images"
:hidden-delete="true"
:pagination="pageInfo"
id-property="name"
@prev-page="$emit('prev-page')"
@next-page="$emit('next-page')"
>
<template #default="{ item }">
<harbor-list-row :item="item" :metadata-loading="metadataLoading" />
</template>
</registry-list>
</template>
<script>
import { sprintf } from '~/locale';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
HARBOR_REGISTRY_TITLE,
LIST_INTRO_TEXT,
imagesCountInfoText,
} from '~/packages_and_registries/harbor_registry/constants';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
export default {
name: 'HarborListHeader',
components: {
TitleArea,
MetadataItem,
},
props: {
imagesCount: {
type: Number,
default: 0,
required: false,
},
helpPagePath: {
type: String,
default: '',
required: false,
},
metadataLoading: {
type: Boolean,
required: false,
default: false,
},
},
i18n: {
HARBOR_REGISTRY_TITLE,
},
computed: {
imagesCountText() {
const pluralisedString = imagesCountInfoText(this.imagesCount);
return sprintf(pluralisedString, { count: this.imagesCount });
},
infoMessages() {
return [{ text: LIST_INTRO_TEXT, link: this.helpPagePath }];
},
},
};
</script>
<template>
<title-area
:title="$options.i18n.HARBOR_REGISTRY_TITLE"
:info-messages="infoMessages"
:metadata-loading="metadataLoading"
>
<template #right-actions>
<slot name="commands"></slot>
</template>
<template #metadata-count>
<metadata-item
v-if="imagesCount"
data-testid="images-count"
icon="container-image"
:text="imagesCountText"
/>
</template>
</title-area>
</template>
<script>
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
export default {
name: 'HarborListRow',
components: {
ClipboardButton,
GlSprintf,
GlIcon,
ListItem,
GlSkeletonLoader,
},
props: {
item: {
type: Object,
required: true,
},
metadataLoading: {
type: Boolean,
default: false,
required: false,
},
},
computed: {
id() {
return this.item.id;
},
artifactCountText() {
return n__(
'HarborRegistry|%{count} Tag',
'HarborRegistry|%{count} Tags',
this.item.artifactCount,
);
},
imageName() {
return this.item.name;
},
},
};
</script>
<template>
<list-item v-bind="$attrs">
<template #left-primary>
<router-link
class="gl-text-body gl-font-weight-bold"
data-testid="details-link"
data-qa-selector="registry_image_content"
:to="{ name: 'details', params: { id } }"
>
{{ imageName }}
</router-link>
<clipboard-button
v-if="item.location"
:text="item.location"
:title="item.location"
category="tertiary"
/>
</template>
<template #left-secondary>
<template v-if="!metadataLoading">
<span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="artifactCountText">
<template #count>
{{ item.artifactCount }}
</template>
</gl-sprintf>
</span>
</template>
<div v-else class="gl-w-full">
<gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet">
<circle cx="6" cy="8" r="6" />
<rect x="16" y="4" width="100" height="8" rx="4" />
</gl-skeleton-loader>
</div>
</template>
</list-item>
</template>
import { s__, __ } from '~/locale';
export const ROOT_IMAGE_TEXT = s__('HarborRegistry|Root image');
export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') };
export const ASCENDING_ORDER = 'asc';
export const DESCENDING_ORDER = 'desc';
export const NAME_SORT_FIELD_KEY = 'name';
export const UPDATED_SORT_FIELD_KEY = 'update_time';
export const CREATED_SORT_FIELD_KEY = 'creation_time';
export const SORT_FIELD_MAPPING = {
NAME: NAME_SORT_FIELD_KEY,
UPDATED: UPDATED_SORT_FIELD_KEY,
CREATED: CREATED_SORT_FIELD_KEY,
};
/* eslint-disable @gitlab/require-i18n-strings */
export const dockerBuildCommand = (repositoryUrl) => {
return `docker build -t ${repositoryUrl} .`;
};
export const dockerPushCommand = (repositoryUrl) => {
return `docker push ${repositoryUrl}`;
};
export const dockerLoginCommand = (registryHostUrlWithPort) => {
return `docker login ${registryHostUrlWithPort}`;
};
/* eslint-enable @gitlab/require-i18n-strings */
import { s__, __ } from '~/locale';
export const UPDATED_AT = s__('HarborRegistry|Last updated %{time}');
export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
'HarborRegistry|The image repository could not be found.',
);
export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
'HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
);
export const NO_TAGS_TITLE = s__('HarborRegistry|This image has no active tags');
export const NO_TAGS_MESSAGE = s__(
`HarborRegistry|The last tag related to this image was recently removed.
This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process.
If you have any questions, contact your administrator.`,
);
export const NO_TAGS_MATCHING_FILTERS_TITLE = s__('HarborRegistry|The filter returned no results');
export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__(
'HarborRegistry|Please try different search criteria',
);
export const DIGEST_LABEL = s__('HarborRegistry|Digest: %{imageId}');
export const CREATED_AT_LABEL = s__('HarborRegistry|Published %{timeInfo}');
export const PUBLISHED_DETAILS_ROW_TEXT = s__(
'HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}',
);
export const MANIFEST_DETAILS_ROW_TEST = s__('HarborRegistry|Manifest digest: %{digest}');
export const CONFIGURATION_DETAILS_ROW_TEST = s__('HarborRegistry|Configuration digest: %{digest}');
export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'HarborRegistry|Invalid tag: missing manifest digest',
);
export const NOT_AVAILABLE_TEXT = __('N/A');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
export * from './common';
export * from './list';
export * from './details';
import { s__, __, n__ } from '~/locale';
import { NAME_SORT_FIELD } from './common';
// Translations strings
export const HARBOR_REGISTRY_TITLE = s__('HarborRegistry|Harbor Registry');
export const CONNECTION_ERROR_TITLE = s__('HarborRegistry|Harbor connection error');
export const CONNECTION_ERROR_MESSAGE = s__(
`HarborRegistry|We are having trouble connecting to the Harbor Registry. Please try refreshing the page. If this error persists, please review %{docLinkStart}the troubleshooting documentation%{docLinkEnd}.`,
);
export const LIST_INTRO_TEXT = s__(
`HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}`,
);
export const imagesCountInfoText = (count) => {
return n__(
'HarborRegistry|%{count} Image repository',
'HarborRegistry|%{count} Image repositories',
count,
);
};
export const EMPTY_RESULT_TITLE = s__('HarborRegistry|Sorry, your filter produced no results.');
export const EMPTY_RESULT_MESSAGE = s__(
'HarborRegistry|To widen your search, change or remove the filters above.',
);
export const SORT_FIELDS = [
{ orderBy: 'UPDATED', label: __('Updated') },
{ orderBy: 'CREATED', label: __('Created') },
NAME_SORT_FIELD,
];
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import {
dockerBuildCommand,
dockerPushCommand,
dockerLoginCommand,
} from '~/packages_and_registries/harbor_registry/constants';
import createRouter from './router';
import HarborRegistryExplorer from './pages/index.vue';
Vue.use(Translate);
Vue.use(GlToast);
Vue.use(PerformancePlugin, {
components: [
'RegistryListPage',
'ListHeader',
'ImageListRow',
'RegistryDetailsPage',
'DetailsHeader',
'TagsList',
],
});
export default (id) => {
const el = document.getElementById(id);
if (!el) {
return null;
}
const { endpoint, connectionError, invalidPathError, isGroupPage, ...config } = el.dataset;
const breadCrumbState = Vue.observable({
name: '',
updateName(value) {
this.name = value;
},
});
const router = createRouter(endpoint, breadCrumbState);
const attachMainComponent = () => {
return new Vue({
el,
router,
provide() {
return {
breadCrumbState,
config: {
...config,
connectionError: parseBoolean(connectionError),
invalidPathError: parseBoolean(invalidPathError),
isGroupPage: parseBoolean(isGroupPage),
helpPagePath: helpPagePath('user/packages/container_registry/index'),
},
dockerBuildCommand: dockerBuildCommand(config.repositoryUrl),
dockerPushCommand: dockerPushCommand(config.repositoryUrl),
dockerLoginCommand: dockerLoginCommand(config.registryHostUrlWithPort),
};
},
render(createElement) {
return createElement(HarborRegistryExplorer);
},
});
};
return {
attachBreadcrumb: renderBreadcrumb(router, null, RegistryBreadcrumb),
attachMainComponent,
};
};
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);
};
<template>
<div>
<router-view ref="router-view" />
</div>
</template>
<script>
import { GlEmptyState, GlSprintf, GlLink, GlSkeletonLoader } from '@gitlab/ui';
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import {
SORT_FIELDS,
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
} from '~/packages_and_registries/harbor_registry/constants';
import Tracking from '~/tracking';
import { harborListResponse } from '../mock_api';
export default {
name: 'HarborListPage',
components: {
HarborListHeader,
HarborList,
GlSkeletonLoader,
GlEmptyState,
GlSprintf,
GlLink,
PersistedSearch,
CliCommands: () =>
import(
/* webpackChunkName: 'harbor_registry_components' */ '~/packages_and_registries/shared/components/cli_commands.vue'
),
},
mixins: [Tracking.mixin()],
inject: ['config', 'dockerBuildCommand', 'dockerPushCommand', 'dockerLoginCommand'],
loader: {
repeat: 10,
width: 1000,
height: 40,
},
i18n: {
CONNECTION_ERROR_TITLE,
CONNECTION_ERROR_MESSAGE,
EMPTY_RESULT_TITLE,
EMPTY_RESULT_MESSAGE,
},
searchConfig: SORT_FIELDS,
data() {
return {
images: [],
totalCount: 0,
pageInfo: {},
filter: [],
isLoading: true,
sorting: null,
name: null,
};
},
computed: {
showCommands() {
return !this.isLoading && !this.config?.isGroupPage && this.images?.length;
},
showConnectionError() {
return this.config.connectionError || this.config.invalidPathError;
},
},
methods: {
fetchHarborImages() {
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
this.isLoading = true;
harborListResponse()
.then((res) => {
this.images = res?.repositories || [];
this.totalCount = res?.totalCount || 0;
this.pageInfo = res?.pageInfo || {};
this.isLoading = false;
})
.catch(() => {});
},
handleSearchUpdate({ sort, filters }) {
this.sorting = sort;
const search = filters.find((i) => i.type === FILTERED_SEARCH_TERM);
this.name = search?.value?.data;
this.fetchHarborImages();
},
fetchPrevPage() {
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
this.fetchHarborImages();
},
fetchNextPage() {
// TODO: Waiting for harbor api integration to finish: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/82777
this.fetchHarborImages();
},
},
};
</script>
<template>
<div>
<gl-empty-state
v-if="showConnectionError"
:title="$options.i18n.CONNECTION_ERROR_TITLE"
:svg-path="config.containersErrorImage"
>
<template #description>
<p>
<gl-sprintf :message="$options.i18n.CONNECTION_ERROR_MESSAGE">
<template #docLink="{ content }">
<gl-link :href="`${config.helpPagePath}#docker-connection-error`" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</p>
</template>
</gl-empty-state>
<template v-else>
<harbor-list-header
:metadata-loading="isLoading"
:images-count="totalCount"
:help-page-path="config.helpPagePath"
>
<template #commands>
<cli-commands
v-if="showCommands"
:docker-build-command="dockerBuildCommand"
:docker-push-command="dockerPushCommand"
:docker-login-command="dockerLoginCommand"
/>
</template>
</harbor-list-header>
<persisted-search
:sortable-fields="$options.searchConfig"
:default-order="$options.searchConfig[0].orderBy"
default-sort="desc"
@update="handleSearchUpdate"
/>
<div v-if="isLoading" class="gl-mt-5">
<gl-skeleton-loader
v-for="index in $options.loader.repeat"
:key="index"
:width="$options.loader.width"
:height="$options.loader.height"
preserve-aspect-ratio="xMinYMax meet"
>
<rect width="500" x="10" y="10" height="20" rx="4" />
<circle cx="525" cy="20" r="10" />
<rect x="960" y="0" width="40" height="40" rx="4" />
</gl-skeleton-loader>
</div>
<template v-else>
<template v-if="images.length > 0 || name">
<harbor-list
v-if="images.length"
:images="images"
:meta-data-loading="isLoading"
:page-info="pageInfo"
@prev-page="fetchPrevPage"
@next-page="fetchNextPage"
/>
<gl-empty-state
v-else
:svg-path="config.noContainersImage"
data-testid="emptySearch"
:title="$options.i18n.EMPTY_RESULT_TITLE"
>
<template #description>
{{ $options.i18n.EMPTY_RESULT_MESSAGE }}
</template>
</gl-empty-state>
</template>
</template>
</template>
</div>
</template>
import Vue from 'vue';
import VueRouter from 'vue-router';
import { HARBOR_REGISTRY_TITLE } from './constants/index';
import List from './pages/list.vue';
import Details from './pages/details.vue';
Vue.use(VueRouter);
export default function createRouter(base, breadCrumbState) {
const router = new VueRouter({
base,
mode: 'history',
routes: [
{
name: 'list',
path: '/',
component: List,
meta: {
nameGenerator: () => HARBOR_REGISTRY_TITLE,
root: true,
},
},
{
name: 'details',
path: '/:id',
component: Details,
meta: {
nameGenerator: () => breadCrumbState.name,
},
},
],
});
return router;
}
...@@ -13,7 +13,8 @@ export default { ...@@ -13,7 +13,8 @@ export default {
props: { props: {
title: { title: {
type: String, type: String,
required: true, default: '',
required: false,
}, },
isLoading: { isLoading: {
type: Boolean, type: Boolean,
......
import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index';
const explorer = HarborRegistryExplorer('js-harbor-registry-list-group');
if (explorer) {
explorer.attachBreadcrumb();
explorer.attachMainComponent();
}
import HarborRegistryExplorer from '~/packages_and_registries/harbor_registry/index';
const explorer = HarborRegistryExplorer('js-harbor-registry-list-project');
if (explorer) {
explorer.attachBreadcrumb();
explorer.attachMainComponent();
}
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
#js-harbor-registry-list-group{ data: { endpoint: group_harbor_registries_path(@group), #js-harbor-registry-list-group{ data: { endpoint: group_harbor_registries_path(@group),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"help_page_path" => help_page_path('user/packages/container_registry/index'), "repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
"registry_host_url_with_port" => 'demo.harbor.com',
connection_error: (!!@connection_error).to_s, connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s, } } invalid_path_error: (!!@invalid_path_error).to_s,
is_group_page: true.to_s } }
...@@ -4,6 +4,8 @@ ...@@ -4,6 +4,8 @@
#js-harbor-registry-list-project{ data: { endpoint: project_harbor_registry_index_path(@project), #js-harbor-registry-list-project{ data: { endpoint: project_harbor_registry_index_path(@project),
"no_containers_image" => image_path('illustrations/docker-empty-state.svg'), "no_containers_image" => image_path('illustrations/docker-empty-state.svg'),
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"help_page_path" => help_page_path('user/packages/container_registry/index'), "repository_url" => 'demo.harbor.com/gitlab-cn/build/cng-images/gitlab-kas',
"registry_host_url_with_port" => 'demo.harbor.com',
connection_error: (!!@connection_error).to_s, connection_error: (!!@connection_error).to_s,
invalid_path_error: (!!@invalid_path_error).to_s, } } invalid_path_error: (!!@invalid_path_error).to_s,
is_group_page: false.to_s, } }
...@@ -47,7 +47,7 @@ module Sidebars ...@@ -47,7 +47,7 @@ module Sidebars
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Container Registry'), title: _('Container Registry'),
link: project_container_registry_index_path(context.project), link: project_container_registry_index_path(context.project),
active_routes: { controller: :repositories }, active_routes: { controller: 'projects/registry/repositories' },
item_id: :container_registry item_id: :container_registry
) )
end end
...@@ -71,7 +71,7 @@ module Sidebars ...@@ -71,7 +71,7 @@ module Sidebars
::Sidebars::MenuItem.new( ::Sidebars::MenuItem.new(
title: _('Harbor Registry'), title: _('Harbor Registry'),
link: project_harbor_registry_index_path(context.project), link: project_harbor_registry_index_path(context.project),
active_routes: { controller: :harbor_registry }, active_routes: { controller: 'projects/harbor/repositories' },
item_id: :harbor_registry item_id: :harbor_registry
) )
end end
......
...@@ -18256,6 +18256,76 @@ msgstr "" ...@@ -18256,6 +18256,76 @@ msgstr ""
msgid "HarborIntegration|Use Harbor as this project's container registry." msgid "HarborIntegration|Use Harbor as this project's container registry."
msgstr "" msgstr ""
msgid "HarborRegistry|%{count} Image repository"
msgid_plural "HarborRegistry|%{count} Image repositories"
msgstr[0] ""
msgstr[1] ""
msgid "HarborRegistry|%{count} Tag"
msgid_plural "HarborRegistry|%{count} Tags"
msgstr[0] ""
msgstr[1] ""
msgid "HarborRegistry|Configuration digest: %{digest}"
msgstr ""
msgid "HarborRegistry|Digest: %{imageId}"
msgstr ""
msgid "HarborRegistry|Harbor Registry"
msgstr ""
msgid "HarborRegistry|Harbor connection error"
msgstr ""
msgid "HarborRegistry|Invalid tag: missing manifest digest"
msgstr ""
msgid "HarborRegistry|Last updated %{time}"
msgstr ""
msgid "HarborRegistry|Manifest digest: %{digest}"
msgstr ""
msgid "HarborRegistry|Please try different search criteria"
msgstr ""
msgid "HarborRegistry|Published %{timeInfo}"
msgstr ""
msgid "HarborRegistry|Published to the %{repositoryPath} image repository at %{time} on %{date}"
msgstr ""
msgid "HarborRegistry|Root image"
msgstr ""
msgid "HarborRegistry|Sorry, your filter produced no results."
msgstr ""
msgid "HarborRegistry|The filter returned no results"
msgstr ""
msgid "HarborRegistry|The image repository could not be found."
msgstr ""
msgid "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."
msgstr ""
msgid "HarborRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page."
msgstr ""
msgid "HarborRegistry|This image has no active tags"
msgstr ""
msgid "HarborRegistry|To widen your search, change or remove the filters above."
msgstr ""
msgid "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}."
msgstr ""
msgid "HarborRegistry|With the Harbor Registry, every project can have its own space to store images. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "Hashed Storage must be enabled to use Geo" msgid "Hashed Storage must be enabled to use Geo"
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import {
HARBOR_REGISTRY_TITLE,
LIST_INTRO_TEXT,
} from '~/packages_and_registries/harbor_registry/constants/index';
describe('harbor_list_header', () => {
let wrapper;
const findTitleArea = () => wrapper.find(TitleArea);
const findCommandsSlot = () => wrapper.find('[data-testid="commands-slot"]');
const findImagesMetaDataItem = () => wrapper.find(MetadataItem);
const mountComponent = async (propsData, slots) => {
wrapper = shallowMount(HarborListHeader, {
stubs: {
GlSprintf,
TitleArea,
},
propsData,
slots,
});
await nextTick();
};
afterEach(() => {
wrapper.destroy();
});
describe('header', () => {
it('has a title', () => {
mountComponent({ metadataLoading: true });
expect(findTitleArea().props()).toMatchObject({
title: HARBOR_REGISTRY_TITLE,
metadataLoading: true,
});
});
it('has a commands slot', () => {
mountComponent(null, { commands: '<div data-testid="commands-slot">baz</div>' });
expect(findCommandsSlot().text()).toBe('baz');
});
describe('sub header parts', () => {
describe('images count', () => {
it('exists', async () => {
await mountComponent({ imagesCount: 1 });
expect(findImagesMetaDataItem().exists()).toBe(true);
});
it('when there is one image', async () => {
await mountComponent({ imagesCount: 1 });
expect(findImagesMetaDataItem().props()).toMatchObject({
text: '1 Image repository',
icon: 'container-image',
});
});
it('when there is more than one image', async () => {
await mountComponent({ imagesCount: 3 });
expect(findImagesMetaDataItem().props('text')).toBe('3 Image repositories');
});
});
});
});
describe('info messages', () => {
describe('default message', () => {
it('is correctly bound to title_area props', () => {
mountComponent({ helpPagePath: 'foo' });
expect(findTitleArea().props('infoMessages')).toEqual([
{ text: LIST_INTRO_TEXT, link: 'foo' },
]);
});
});
});
});
import { shallowMount, RouterLinkStub as RouterLink } from '@vue/test-utils';
import { GlIcon, GlSprintf, GlSkeletonLoader } from '@gitlab/ui';
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { harborListResponse } from '../../mock_data';
describe('Harbor List Row', () => {
let wrapper;
const [item] = harborListResponse.repositories;
const findDetailsLink = () => wrapper.find(RouterLink);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const mountComponent = (props) => {
wrapper = shallowMount(HarborListRow, {
stubs: {
RouterLink,
GlSprintf,
ListItem,
},
propsData: {
item,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
expect(link.text()).toBe(item.name);
expect(findDetailsLink().props('to')).toMatchObject({
name: 'details',
params: {
id: item.id,
},
});
});
it('contains a clipboard button', () => {
mountComponent();
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(item.location);
expect(button.props('title')).toBe(item.location);
});
});
describe('tags count', () => {
it('exists', () => {
mountComponent();
expect(findTagsCount().exists()).toBe(true);
});
it('contains a tag icon', () => {
mountComponent();
const icon = findTagsCount().find(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('tag');
});
describe('loading state', () => {
it('shows a loader when metadataLoading is true', () => {
mountComponent({ metadataLoading: true });
expect(findSkeletonLoader().exists()).toBe(true);
});
it('hides the tags count while loading', () => {
mountComponent({ metadataLoading: true });
expect(findTagsCount().exists()).toBe(false);
});
});
describe('tags count text', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, artifactCount: 1 } });
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
});
it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, artifactCount: 3 } });
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
import HarborListRow from '~/packages_and_registries/harbor_registry/components/list/harbor_list_row.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import { harborListResponse } from '../../mock_data';
describe('Harbor List', () => {
let wrapper;
const findHarborListRow = () => wrapper.findAll(HarborListRow);
const mountComponent = (props) => {
wrapper = shallowMount(HarborList, {
stubs: { RegistryList },
propsData: {
images: harborListResponse.repositories,
pageInfo: harborListResponse.pageInfo,
...props,
},
});
};
afterEach(() => {
wrapper.destroy();
});
describe('list', () => {
it('contains one list element for each image', () => {
mountComponent();
expect(findHarborListRow().length).toBe(harborListResponse.repositories.length);
});
it('passes down the metadataLoading prop', () => {
mountComponent({ metadataLoading: true });
expect(findHarborListRow().at(0).props('metadataLoading')).toBe(true);
});
});
});
export const harborListResponse = {
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,
},
};
export const harborTagsResponse = {
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: 100,
pageInfo: {
hasNextPage: false,
hasPreviousPage: false,
},
};
export const dockerCommands = {
dockerBuildCommand: 'foofoo',
dockerPushCommand: 'barbar',
dockerLoginCommand: 'bazbaz',
};
import { shallowMount } from '@vue/test-utils';
import component from '~/packages_and_registries/harbor_registry/pages/index.vue';
describe('List Page', () => {
let wrapper;
const findRouterView = () => wrapper.find({ ref: 'router-view' });
const mountComponent = () => {
wrapper = shallowMount(component, {
stubs: {
RouterView: true,
},
});
};
beforeEach(() => {
mountComponent();
});
it('has a router view', () => {
expect(findRouterView().exists()).toBe(true);
});
});
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { GlSkeletonLoader } from '@gitlab/ui';
import HarborListHeader from '~/packages_and_registries/harbor_registry/components/list/harbor_list_header.vue';
import HarborRegistryList from '~/packages_and_registries/harbor_registry/pages/list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import waitForPromises from 'helpers/wait_for_promises';
// import { harborListResponse } from '~/packages_and_registries/harbor_registry/mock_api.js';
import HarborList from '~/packages_and_registries/harbor_registry/components/list/harbor_list.vue';
import CliCommands from '~/packages_and_registries/shared/components/cli_commands.vue';
import { SORT_FIELDS } from '~/packages_and_registries/harbor_registry/constants/index';
import { harborListResponse, dockerCommands } from '../mock_data';
let mockHarborListResponse;
jest.mock('~/packages_and_registries/harbor_registry/mock_api.js', () => ({
harborListResponse: () => mockHarborListResponse,
}));
describe('Harbor List Page', () => {
let wrapper;
const waitForHarborPageRequest = async () => {
await waitForPromises();
await nextTick();
};
beforeEach(() => {
mockHarborListResponse = Promise.resolve(harborListResponse);
});
const findHarborListHeader = () => wrapper.findComponent(HarborListHeader);
const findPersistedSearch = () => wrapper.findComponent(PersistedSearch);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findHarborList = () => wrapper.findComponent(HarborList);
const findCliCommands = () => wrapper.findComponent(CliCommands);
const fireFirstSortUpdate = () => {
findPersistedSearch().vm.$emit('update', { sort: 'UPDATED_DESC', filters: [] });
};
const mountComponent = ({ config = { isGroupPage: false } } = {}) => {
wrapper = shallowMount(HarborRegistryList, {
stubs: {
HarborListHeader,
},
provide() {
return {
config,
...dockerCommands,
};
},
});
};
afterEach(() => {
wrapper.destroy();
});
it('contains harbor registry header', async () => {
mountComponent();
fireFirstSortUpdate();
await waitForHarborPageRequest();
await nextTick();
expect(findHarborListHeader().exists()).toBe(true);
expect(findHarborListHeader().props()).toMatchObject({
imagesCount: 3,
metadataLoading: false,
});
});
describe('isLoading is true', () => {
it('shows the skeleton loader', async () => {
mountComponent();
fireFirstSortUpdate();
expect(findSkeletonLoader().exists()).toBe(true);
});
it('harborList is not visible', () => {
mountComponent();
expect(findHarborList().exists()).toBe(false);
});
it('cli commands is not visible', () => {
mountComponent();
expect(findCliCommands().exists()).toBe(false);
});
it('title has the metadataLoading props set to true', async () => {
mountComponent();
fireFirstSortUpdate();
expect(findHarborListHeader().props('metadataLoading')).toBe(true);
});
});
describe('list is not empty', () => {
describe('unfiltered state', () => {
it('quick start is visible', async () => {
mountComponent();
fireFirstSortUpdate();
await waitForHarborPageRequest();
await nextTick();
expect(findCliCommands().exists()).toBe(true);
});
it('list component is visible', async () => {
mountComponent();
fireFirstSortUpdate();
await waitForHarborPageRequest();
await nextTick();
expect(findHarborList().exists()).toBe(true);
});
});
describe('search and sorting', () => {
it('has a persisted search box element', async () => {
mountComponent();
fireFirstSortUpdate();
await waitForHarborPageRequest();
await nextTick();
const harborRegistrySearch = findPersistedSearch();
expect(harborRegistrySearch.exists()).toBe(true);
expect(harborRegistrySearch.props()).toMatchObject({
defaultOrder: 'UPDATED',
defaultSort: 'desc',
sortableFields: SORT_FIELDS,
});
});
});
});
});
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment