diff options
Diffstat (limited to 'app/assets/javascripts/registry/explorer/pages')
3 files changed, 221 insertions, 93 deletions
diff --git a/app/assets/javascripts/registry/explorer/pages/details.vue b/app/assets/javascripts/registry/explorer/pages/details.vue index a60ef5c4982..540f02d58d4 100644 --- a/app/assets/javascripts/registry/explorer/pages/details.vue +++ b/app/assets/javascripts/registry/explorer/pages/details.vue @@ -1,8 +1,9 @@ <script> -import { mapState, mapActions } from 'vuex'; -import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui'; +import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; +import createFlash from '~/flash'; import Tracking from '~/tracking'; +import { joinPaths } from '~/lib/utils/url_utility'; import DeleteAlert from '../components/details_page/delete_alert.vue'; import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue'; import DeleteModal from '../components/details_page/delete_modal.vue'; @@ -11,11 +12,16 @@ import TagsList from '../components/details_page/tags_list.vue'; import TagsLoader from '../components/details_page/tags_loader.vue'; import EmptyTagsState from '../components/details_page/empty_tags_state.vue'; +import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql'; +import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql'; + import { ALERT_SUCCESS_TAG, ALERT_DANGER_TAG, ALERT_SUCCESS_TAGS, ALERT_DANGER_TAGS, + GRAPHQL_PAGE_SIZE, + FETCH_IMAGES_LIST_ERROR_MESSAGE, } from '../constants/index'; export default { @@ -23,28 +29,61 @@ export default { DeleteAlert, PartialCleanupAlert, DetailsHeader, - GlPagination, + GlKeysetPagination, DeleteModal, TagsList, TagsLoader, EmptyTagsState, }, + inject: ['breadCrumbState', 'config'], directives: { GlResizeObserver: GlResizeObserverDirective, }, mixins: [Tracking.mixin()], + apollo: { + image: { + query: getContainerRepositoryDetailsQuery, + variables() { + return this.queryVariables; + }, + update(data) { + return data.containerRepository; + }, + result({ data }) { + this.tagsPageInfo = data.containerRepository?.tags?.pageInfo; + this.breadCrumbState.updateName(data.containerRepository?.name); + }, + error() { + createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); + }, + }, + }, data() { return { + image: {}, + tagsPageInfo: {}, itemsToBeDeleted: [], isMobile: false, + mutationLoading: false, deleteAlertType: null, dismissPartialCleanupWarning: false, }; }, computed: { - ...mapState(['tagsPagination', 'isLoading', 'config', 'tags', 'imageDetails']), + queryVariables() { + return { + id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`), + first: GRAPHQL_PAGE_SIZE, + }; + }, + isLoading() { + return this.$apollo.queries.image.loading || this.mutationLoading; + }, + tags() { + return this.image?.tags?.nodes || []; + }, showPartialCleanupWarning() { - return this.imageDetails?.cleanup_policy_started_at && !this.dismissPartialCleanupWarning; + return this.image?.expirationPolicyStartedAt && !this.dismissPartialCleanupWarning; }, tracking() { return { @@ -52,66 +91,78 @@ export default { this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', }; }, - currentPage: { - get() { - return this.tagsPagination.page; - }, - set(page) { - this.requestTagsList({ page }); - }, + showPagination() { + return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage; }, }, - mounted() { - this.requestImageDetailsAndTagsList(this.$route.params.id); - }, methods: { - ...mapActions([ - 'requestTagsList', - 'requestDeleteTag', - 'requestDeleteTags', - 'requestImageDetailsAndTagsList', - ]), deleteTags(toBeDeleted) { this.itemsToBeDeleted = this.tags.filter(tag => toBeDeleted[tag.name]); this.track('click_button'); this.$refs.deleteModal.show(); }, - handleSingleDelete() { - const [itemToDelete] = this.itemsToBeDeleted; - this.itemsToBeDeleted = []; - return this.requestDeleteTag({ tag: itemToDelete }) - .then(() => { - this.deleteAlertType = ALERT_SUCCESS_TAG; - }) - .catch(() => { - this.deleteAlertType = ALERT_DANGER_TAG; - }); - }, - handleMultipleDelete() { + async handleDelete() { + this.track('confirm_delete'); const { itemsToBeDeleted } = this; this.itemsToBeDeleted = []; - - return this.requestDeleteTags({ - ids: itemsToBeDeleted.map(x => x.name), - }) - .then(() => { - this.deleteAlertType = ALERT_SUCCESS_TAGS; - }) - .catch(() => { - this.deleteAlertType = ALERT_DANGER_TAGS; + this.mutationLoading = true; + try { + const { data } = await this.$apollo.mutate({ + mutation: deleteContainerRepositoryTagsMutation, + variables: { + id: this.queryVariables.id, + tagNames: itemsToBeDeleted.map(i => i.name), + }, + awaitRefetchQueries: true, + refetchQueries: [ + { + query: getContainerRepositoryDetailsQuery, + variables: this.queryVariables, + }, + ], }); - }, - onDeletionConfirmed() { - this.track('confirm_delete'); - if (this.itemsToBeDeleted.length > 1) { - this.handleMultipleDelete(); - } else { - this.handleSingleDelete(); + + if (data?.destroyContainerRepositoryTags?.errors[0]) { + throw new Error(); + } + this.deleteAlertType = + itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS; + } catch (e) { + this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS; } + + this.mutationLoading = false; }, handleResize() { this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs'; }, + fetchNextPage() { + if (this.tagsPageInfo?.hasNextPage) { + this.$apollo.queries.image.fetchMore({ + variables: { + after: this.tagsPageInfo?.endCursor, + first: GRAPHQL_PAGE_SIZE, + }, + updateQuery(previousResult, { fetchMoreResult }) { + return fetchMoreResult; + }, + }); + } + }, + fetchPreviousPage() { + if (this.tagsPageInfo?.hasPreviousPage) { + this.$apollo.queries.image.fetchMore({ + variables: { + first: null, + before: this.tagsPageInfo?.startCursor, + last: GRAPHQL_PAGE_SIZE, + }, + updateQuery(previousResult, { fetchMoreResult }) { + return fetchMoreResult; + }, + }); + } + }, }, }; </script> @@ -132,28 +183,30 @@ export default { @dismiss="dismissPartialCleanupWarning = true" /> - <details-header :image-name="imageDetails.name" /> + <details-header :image="image" /> <tags-loader v-if="isLoading" /> <template v-else> <empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" /> - <tags-list v-else :tags="tags" :is-mobile="isMobile" @delete="deleteTags" /> + <template v-else> + <tags-list :tags="tags" :is-mobile="isMobile" @delete="deleteTags" /> + <div class="gl-display-flex gl-justify-content-center"> + <gl-keyset-pagination + v-if="showPagination" + :has-next-page="tagsPageInfo.hasNextPage" + :has-previous-page="tagsPageInfo.hasPreviousPage" + class="gl-mt-3" + @prev="fetchPreviousPage" + @next="fetchNextPage" + /> + </div> + </template> </template> - <gl-pagination - v-if="!isLoading" - ref="pagination" - v-model="currentPage" - :per-page="tagsPagination.perPage" - :total-items="tagsPagination.total" - align="center" - class="gl-w-full gl-mt-3" - /> - <delete-modal ref="deleteModal" :items-to-be-deleted="itemsToBeDeleted" - @confirmDelete="onDeletionConfirmed" + @confirmDelete="handleDelete" @cancel="track('cancel_delete')" /> </div> diff --git a/app/assets/javascripts/registry/explorer/pages/index.vue b/app/assets/javascripts/registry/explorer/pages/index.vue index 4ac0bca84c1..dca63e1a569 100644 --- a/app/assets/javascripts/registry/explorer/pages/index.vue +++ b/app/assets/javascripts/registry/explorer/pages/index.vue @@ -1,7 +1,3 @@ -<script> -export default {}; -</script> - <template> <div> <router-view ref="router-view" /> diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue index 81e47073fe9..3192ba82db8 100644 --- a/app/assets/javascripts/registry/explorer/pages/list.vue +++ b/app/assets/javascripts/registry/explorer/pages/list.vue @@ -1,5 +1,4 @@ <script> -import { mapState, mapActions } from 'vuex'; import { GlEmptyState, GlTooltipDirective, @@ -11,6 +10,7 @@ import { GlSearchBoxByClick, } from '@gitlab/ui'; import Tracking from '~/tracking'; +import createFlash from '~/flash'; import ProjectEmptyState from '../components/list_page/project_empty_state.vue'; import GroupEmptyState from '../components/list_page/group_empty_state.vue'; @@ -18,6 +18,10 @@ import RegistryHeader from '../components/list_page/registry_header.vue'; import ImageList from '../components/list_page/image_list.vue'; import CliCommands from '../components/list_page/cli_commands.vue'; +import getProjectContainerRepositoriesQuery from '../graphql/queries/get_project_container_repositories.query.graphql'; +import getGroupContainerRepositoriesQuery from '../graphql/queries/get_group_container_repositories.query.graphql'; +import deleteContainerRepositoryMutation from '../graphql/mutations/delete_container_repository.mutation.graphql'; + import { DELETE_IMAGE_SUCCESS_MESSAGE, DELETE_IMAGE_ERROR_MESSAGE, @@ -29,6 +33,8 @@ import { IMAGE_REPOSITORY_LIST_LABEL, EMPTY_RESULT_TITLE, EMPTY_RESULT_MESSAGE, + GRAPHQL_PAGE_SIZE, + FETCH_IMAGES_LIST_ERROR_MESSAGE, } from '../constants/index'; export default { @@ -47,6 +53,7 @@ export default { RegistryHeader, CliCommands, }, + inject: ['config'], directives: { GlTooltip: GlTooltipDirective, }, @@ -66,21 +73,62 @@ export default { EMPTY_RESULT_TITLE, EMPTY_RESULT_MESSAGE, }, + apollo: { + images: { + query() { + return this.graphQlQuery; + }, + variables() { + return this.queryVariables; + }, + update(data) { + return data[this.graphqlResource]?.containerRepositories.nodes; + }, + result({ data }) { + this.pageInfo = data[this.graphqlResource]?.containerRepositories?.pageInfo; + this.containerRepositoriesCount = data[this.graphqlResource]?.containerRepositoriesCount; + }, + error() { + createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE }); + }, + }, + }, data() { return { + images: [], + pageInfo: {}, + containerRepositoriesCount: 0, itemToDelete: {}, deleteAlertType: null, - search: null, - isEmpty: false, + searchValue: null, + name: null, + mutationLoading: false, }; }, computed: { - ...mapState(['config', 'isLoading', 'images', 'pagination']), + graphqlResource() { + return this.config.isGroupPage ? 'group' : 'project'; + }, + graphQlQuery() { + return this.config.isGroupPage + ? getGroupContainerRepositoriesQuery + : getProjectContainerRepositoriesQuery; + }, + queryVariables() { + return { + name: this.name, + fullPath: this.config.isGroupPage ? this.config.groupPath : this.config.projectPath, + first: GRAPHQL_PAGE_SIZE, + }; + }, tracking() { return { label: 'registry_repository_delete', }; }, + isLoading() { + return this.$apollo.queries.images.loading || this.mutationLoading; + }, showCommands() { return Boolean(!this.isLoading && !this.config?.isGroupPage && this.images?.length); }, @@ -93,19 +141,7 @@ export default { : DELETE_IMAGE_ERROR_MESSAGE; }, }, - mounted() { - this.loadImageList(this.$route.name); - }, methods: { - ...mapActions(['requestImagesList', 'requestDeleteImage']), - loadImageList(fromName) { - if (!fromName || !this.images?.length) { - return this.requestImagesList().then(() => { - this.isEmpty = this.images.length === 0; - }); - } - return Promise.resolve(); - }, deleteImage(item) { this.track('click_button'); this.itemToDelete = item; @@ -113,18 +149,59 @@ export default { }, handleDeleteImage() { this.track('confirm_delete'); - return this.requestDeleteImage(this.itemToDelete) - .then(() => { - this.deleteAlertType = 'success'; + this.mutationLoading = true; + return this.$apollo + .mutate({ + mutation: deleteContainerRepositoryMutation, + variables: { + id: this.itemToDelete.id, + }, + }) + .then(({ data }) => { + if (data?.destroyContainerRepository?.errors[0]) { + this.deleteAlertType = 'danger'; + } else { + this.deleteAlertType = 'success'; + } }) .catch(() => { this.deleteAlertType = 'danger'; + }) + .finally(() => { + this.mutationLoading = false; }); }, dismissDeleteAlert() { this.deleteAlertType = null; this.itemToDelete = {}; }, + fetchNextPage() { + if (this.pageInfo?.hasNextPage) { + this.$apollo.queries.images.fetchMore({ + variables: { + after: this.pageInfo?.endCursor, + first: GRAPHQL_PAGE_SIZE, + }, + updateQuery(previousResult, { fetchMoreResult }) { + return fetchMoreResult; + }, + }); + } + }, + fetchPreviousPage() { + if (this.pageInfo?.hasPreviousPage) { + this.$apollo.queries.images.fetchMore({ + variables: { + first: null, + before: this.pageInfo?.startCursor, + last: GRAPHQL_PAGE_SIZE, + }, + updateQuery(previousResult, { fetchMoreResult }) { + return fetchMoreResult; + }, + }); + } + }, }, }; </script> @@ -134,7 +211,7 @@ export default { <gl-alert v-if="showDeleteAlert" :variant="deleteAlertType" - class="mt-2" + class="gl-mt-5" dismissible @dismiss="dismissDeleteAlert" > @@ -165,7 +242,7 @@ export default { <template v-else> <registry-header - :images-count="pagination.total" + :images-count="containerRepositoriesCount" :expiration-policy="config.expirationPolicy" :help-page-path="config.helpPagePath" :expiration-policy-help-page-path="config.expirationPolicyHelpPagePath" @@ -176,7 +253,7 @@ export default { </template> </registry-header> - <div v-if="isLoading" class="mt-2"> + <div v-if="isLoading" class="gl-mt-5"> <gl-skeleton-loader v-for="index in $options.loader.repeat" :key="index" @@ -190,16 +267,17 @@ export default { </gl-skeleton-loader> </div> <template v-else> - <template v-if="!isEmpty"> + <template v-if="images.length > 0 || name"> <div class="gl-display-flex gl-p-1 gl-mt-3" data-testid="listHeader"> <div class="gl-flex-fill-1"> <h5>{{ $options.i18n.IMAGE_REPOSITORY_LIST_LABEL }}</h5> </div> <div> <gl-search-box-by-click - v-model="search" + v-model="searchValue" :placeholder="$options.i18n.SEARCH_PLACEHOLDER_TEXT" - @submit="requestImagesList({ name: $event })" + @clear="name = null" + @submit="name = $event" /> </div> </div> @@ -207,9 +285,10 @@ export default { <image-list v-if="images.length" :images="images" - :pagination="pagination" - @pageChange="requestImagesList({ pagination: { page: $event }, name: search })" + :page-info="pageInfo" @delete="deleteImage" + @prev-page="fetchPreviousPage" + @next-page="fetchNextPage" /> <gl-empty-state |