diff options
Diffstat (limited to 'app/assets/javascripts/packages_and_registries/harbor_registry/pages')
3 files changed, 371 insertions, 57 deletions
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> |