Commit 2a6451c1 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '296962-container-registry-details-split-details-from-tags-call' into 'master'

Container Registry Details: split details from tags call

See merge request gitlab-org/gitlab!59969
parents f0bab9fb 9079b29a
<script> <script>
import { GlButton } from '@gitlab/ui'; import { GlButton, GlKeysetPagination } from '@gitlab/ui';
import { REMOVE_TAGS_BUTTON_TITLE, TAGS_LIST_TITLE } from '../../constants/index'; import createFlash from '~/flash';
import { joinPaths } from '~/lib/utils/url_utility';
import {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
} from '../../constants/index';
import getContainerRepositoryTagsQuery from '../../graphql/queries/get_container_repository_tags.query.graphql';
import EmptyState from './empty_state.vue';
import TagsListRow from './tags_list_row.vue'; import TagsListRow from './tags_list_row.vue';
import TagsLoader from './tags_loader.vue';
export default { export default {
name: 'TagsList', name: 'TagsList',
components: { components: {
GlButton, GlButton,
GlKeysetPagination,
TagsListRow, TagsListRow,
EmptyState,
TagsLoader,
}, },
inject: ['config'],
props: { props: {
tags: { id: {
type: Array, type: [Number, String],
required: false, required: true,
default: () => [],
}, },
isMobile: { isMobile: {
type: Boolean, type: Boolean,
...@@ -25,17 +38,46 @@ export default { ...@@ -25,17 +38,46 @@ export default {
default: false, default: false,
required: false, required: false,
}, },
isImageLoading: {
type: Boolean,
default: false,
required: false,
},
}, },
i18n: { i18n: {
REMOVE_TAGS_BUTTON_TITLE, REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE, TAGS_LIST_TITLE,
}, },
apollo: {
containerRepository: {
query: getContainerRepositoryTagsQuery,
variables() {
return this.queryVariables;
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
},
},
},
data() { data() {
return { return {
selectedItems: {}, selectedItems: {},
containerRepository: {},
}; };
}, },
computed: { computed: {
tags() {
return this.containerRepository?.tags?.nodes || [];
},
tagsPageInfo() {
return this.containerRepository?.tags?.pageInfo;
},
queryVariables() {
return {
id: joinPaths(this.config.gidPrefix, `${this.id}`),
first: GRAPHQL_PAGE_SIZE,
};
},
hasSelectedItems() { hasSelectedItems() {
return this.tags.some((tag) => this.selectedItems[tag.name]); return this.tags.some((tag) => this.selectedItems[tag.name]);
}, },
...@@ -45,17 +87,56 @@ export default { ...@@ -45,17 +87,56 @@ export default {
multiDeleteButtonIsDisabled() { multiDeleteButtonIsDisabled() {
return !this.hasSelectedItems || this.disabled; return !this.hasSelectedItems || this.disabled;
}, },
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
hasNoTags() {
return this.tags.length === 0;
},
isLoading() {
return this.isImageLoading || this.$apollo.queries.containerRepository.loading;
},
}, },
methods: { methods: {
updateSelectedItems(name) { updateSelectedItems(name) {
this.$set(this.selectedItems, name, !this.selectedItems[name]); this.$set(this.selectedItems, name, !this.selectedItems[name]);
}, },
mapTagsToBeDleeted(items) {
return this.tags.filter((tag) => items[tag.name]);
},
fetchNextPage() {
this.$apollo.queries.containerRepository.fetchMore({
variables: {
after: this.tagsPageInfo?.endCursor,
first: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
},
fetchPreviousPage() {
this.$apollo.queries.containerRepository.fetchMore({
variables: {
first: null,
before: this.tagsPageInfo?.startCursor,
last: GRAPHQL_PAGE_SIZE,
},
updateQuery(previousResult, { fetchMoreResult }) {
return fetchMoreResult;
},
});
},
}, },
}; };
</script> </script>
<template> <template>
<div> <div>
<tags-loader v-if="isLoading" />
<template v-else>
<empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
<template v-else>
<div class="gl-display-flex gl-justify-content-space-between gl-mb-3"> <div class="gl-display-flex gl-justify-content-space-between gl-mb-3">
<h5 data-testid="list-title"> <h5 data-testid="list-title">
{{ $options.i18n.TAGS_LIST_TITLE }} {{ $options.i18n.TAGS_LIST_TITLE }}
...@@ -66,7 +147,7 @@ export default { ...@@ -66,7 +147,7 @@ export default {
:disabled="multiDeleteButtonIsDisabled" :disabled="multiDeleteButtonIsDisabled"
category="secondary" category="secondary"
variant="danger" variant="danger"
@click="$emit('delete', selectedItems)" @click="$emit('delete', mapTagsToBeDleeted(selectedItems))"
> >
{{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }} {{ $options.i18n.REMOVE_TAGS_BUTTON_TITLE }}
</gl-button> </gl-button>
...@@ -80,7 +161,19 @@ export default { ...@@ -80,7 +161,19 @@ export default {
:is-mobile="isMobile" :is-mobile="isMobile"
:disabled="disabled" :disabled="disabled"
@select="updateSelectedItems(tag.name)" @select="updateSelectedItems(tag.name)"
@delete="$emit('delete', { [tag.name]: true })" @delete="$emit('delete', mapTagsToBeDleeted({ [tag.name]: true }))"
/>
<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> </div>
</template>
</template>
</div>
</template> </template>
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql" query getContainerRepositoryDetails($id: ID!) {
query getContainerRepositoryDetails(
$id: ID!
$first: Int
$last: Int
$after: String
$before: String
) {
containerRepository(id: $id) { containerRepository(id: $id) {
id id
name name
...@@ -19,22 +11,6 @@ query getContainerRepositoryDetails( ...@@ -19,22 +11,6 @@ query getContainerRepositoryDetails(
tagsCount tagsCount
expirationPolicyStartedAt expirationPolicyStartedAt
expirationPolicyCleanupStatus expirationPolicyCleanupStatus
tags(after: $after, before: $before, first: $first, last: $last) {
nodes {
digest
location
path
name
revision
shortRevision
createdAt
totalSize
canDelete
}
pageInfo {
...PageInfo
}
}
project { project {
visibility visibility
containerExpirationPolicy { containerExpirationPolicy {
......
#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
query getContainerRepositoryDetails(
$id: ID!
$first: Int
$last: Int
$after: String
$before: String
) {
containerRepository(id: $id) {
id
tags(after: $after, before: $before, first: $first, last: $last) {
nodes {
digest
location
path
name
revision
shortRevision
createdAt
totalSize
canDelete
}
pageInfo {
...PageInfo
}
}
}
}
<script> <script>
import { GlKeysetPagination, GlResizeObserverDirective } from '@gitlab/ui'; import { GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash'; import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
...@@ -21,7 +21,6 @@ import { ...@@ -21,7 +21,6 @@ import {
ALERT_SUCCESS_TAGS, ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS, ALERT_DANGER_TAGS,
ALERT_DANGER_IMAGE, ALERT_DANGER_IMAGE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE, FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS, UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB, MISSING_OR_DELETED_IMAGE_BREADCRUMB,
...@@ -36,7 +35,6 @@ export default { ...@@ -36,7 +35,6 @@ export default {
DeleteAlert, DeleteAlert,
PartialCleanupAlert, PartialCleanupAlert,
DetailsHeader, DetailsHeader,
GlKeysetPagination,
DeleteModal, DeleteModal,
TagsList, TagsList,
TagsLoader, TagsLoader,
...@@ -58,8 +56,7 @@ export default { ...@@ -58,8 +56,7 @@ export default {
update(data) { update(data) {
return data.containerRepository; return data.containerRepository;
}, },
result({ data }) { result() {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
this.updateBreadcrumb(); this.updateBreadcrumb();
}, },
error() { error() {
...@@ -70,7 +67,6 @@ export default { ...@@ -70,7 +67,6 @@ export default {
data() { data() {
return { return {
image: {}, image: {},
tagsPageInfo: {},
itemsToBeDeleted: [], itemsToBeDeleted: [],
isMobile: false, isMobile: false,
mutationLoading: false, mutationLoading: false,
...@@ -83,15 +79,11 @@ export default { ...@@ -83,15 +79,11 @@ export default {
queryVariables() { queryVariables() {
return { return {
id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`), id: joinPaths(this.config.gidPrefix, `${this.$route.params.id}`),
first: GRAPHQL_PAGE_SIZE,
}; };
}, },
isLoading() { isLoading() {
return this.$apollo.queries.image.loading || this.mutationLoading; return this.$apollo.queries.image.loading || this.mutationLoading;
}, },
tags() {
return this.image?.tags?.nodes || [];
},
showPartialCleanupWarning() { showPartialCleanupWarning() {
return ( return (
this.config.showUnfinishedTagCleanupCallout && this.config.showUnfinishedTagCleanupCallout &&
...@@ -105,12 +97,6 @@ export default { ...@@ -105,12 +97,6 @@ export default {
this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete', this.itemsToBeDeleted?.length > 1 ? 'bulk_registry_tag_delete' : 'registry_tag_delete',
}; };
}, },
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
hasNoTags() {
return this.tags.length === 0;
},
pageActionsAreDisabled() { pageActionsAreDisabled() {
return Boolean(this.image?.status); return Boolean(this.image?.status);
}, },
...@@ -124,7 +110,7 @@ export default { ...@@ -124,7 +110,7 @@ export default {
}, },
deleteTags(toBeDeleted) { deleteTags(toBeDeleted) {
this.deleteImageAlert = false; this.deleteImageAlert = false;
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]); this.itemsToBeDeleted = toBeDeleted;
this.track('click_button'); this.track('click_button');
this.$refs.deleteModal.show(); this.$refs.deleteModal.show();
}, },
...@@ -170,33 +156,6 @@ export default { ...@@ -170,33 +156,6 @@ export default {
handleResize() { handleResize() {
this.isMobile = GlBreakpointInstance.getBreakpointSize() === 'xs'; 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;
},
});
}
},
dismissPartialCleanupWarning() { dismissPartialCleanupWarning() {
this.hidePartialCleanupWarning = true; this.hidePartialCleanupWarning = true;
axios.post(this.config.userCalloutsPath, { axios.post(this.config.userCalloutsPath, {
...@@ -246,27 +205,14 @@ export default { ...@@ -246,27 +205,14 @@ export default {
/> />
<tags-loader v-if="isLoading" /> <tags-loader v-if="isLoading" />
<template v-else>
<empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
<template v-else>
<tags-list <tags-list
:tags="tags" v-else
:id="$route.params.id"
:is-image-loading="isLoading"
:is-mobile="isMobile" :is-mobile="isMobile"
:disabled="pageActionsAreDisabled" :disabled="pageActionsAreDisabled"
@delete="deleteTags" @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>
<delete-image <delete-image
:id="image.id" :id="image.id"
......
---
title: 'Container Registry Details: split details from tags call'
merge_request: 59969
author:
type: changed
...@@ -161,6 +161,20 @@ export const tagsMock = [ ...@@ -161,6 +161,20 @@ export const tagsMock = [
}, },
]; ];
export const imageTagsMock = (nodes = tagsMock) => ({
data: {
containerRepository: {
id: containerRepositoryMock.id,
tags: {
nodes,
pageInfo: { ...tagsPageInfo },
__typename: 'ContainerRepositoryTagConnection',
},
__typename: 'ContainerRepositoryDetails',
},
},
});
export const graphQLImageDetailsMock = (override) => ({ export const graphQLImageDetailsMock = (override) => ({
data: { data: {
containerRepository: { containerRepository: {
......
...@@ -28,12 +28,10 @@ import Tracking from '~/tracking'; ...@@ -28,12 +28,10 @@ import Tracking from '~/tracking';
import { import {
graphQLImageDetailsMock, graphQLImageDetailsMock,
graphQLImageDetailsEmptyTagsMock,
graphQLDeleteImageRepositoryTagsMock, graphQLDeleteImageRepositoryTagsMock,
containerRepositoryMock, containerRepositoryMock,
graphQLEmptyImageDetailsMock, graphQLEmptyImageDetailsMock,
tagsMock, tagsMock,
tagsPageInfo,
} from '../mock_data'; } from '../mock_data';
import { DeleteModal } from '../stubs'; import { DeleteModal } from '../stubs';
...@@ -72,12 +70,6 @@ describe('Details Page', () => { ...@@ -72,12 +70,6 @@ describe('Details Page', () => {
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
}; };
const tagsArrayToSelectedTags = (tags) =>
tags.reduce((acc, c) => {
acc[c.name] = true;
return acc;
}, {});
const mountComponent = ({ const mountComponent = ({
resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()), resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock()),
mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock), mutationResolver = jest.fn().mockResolvedValue(graphQLDeleteImageRepositoryTagsMock),
...@@ -138,12 +130,6 @@ describe('Details Page', () => { ...@@ -138,12 +130,6 @@ describe('Details Page', () => {
expect(findTagsList().exists()).toBe(false); expect(findTagsList().exists()).toBe(false);
}); });
it('does not show pagination', () => {
mountComponent();
expect(findPagination().exists()).toBe(false);
});
}); });
describe('when the image does not exist', () => { describe('when the image does not exist', () => {
...@@ -167,34 +153,6 @@ describe('Details Page', () => { ...@@ -167,34 +153,6 @@ describe('Details Page', () => {
}); });
}); });
describe('when the list of tags is empty', () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
it('has the empty state', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findEmptyState().exists()).toBe(true);
});
it('does not show the loader', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findTagsLoader().exists()).toBe(false);
});
it('does not show the list', async () => {
mountComponent({ resolver });
await waitForApolloRequestRender();
expect(findTagsList().exists()).toBe(false);
});
});
describe('list', () => { describe('list', () => {
it('exists', async () => { it('exists', async () => {
mountComponent(); mountComponent();
...@@ -211,7 +169,6 @@ describe('Details Page', () => { ...@@ -211,7 +169,6 @@ describe('Details Page', () => {
expect(findTagsList().props()).toMatchObject({ expect(findTagsList().props()).toMatchObject({
isMobile: false, isMobile: false,
tags: cleanTags,
}); });
}); });
...@@ -224,7 +181,7 @@ describe('Details Page', () => { ...@@ -224,7 +181,7 @@ describe('Details Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
[tagToBeDeleted] = cleanTags; [tagToBeDeleted] = cleanTags;
findTagsList().vm.$emit('delete', { [tagToBeDeleted.name]: true }); findTagsList().vm.$emit('delete', [tagToBeDeleted]);
}); });
it('open the modal', async () => { it('open the modal', async () => {
...@@ -244,7 +201,7 @@ describe('Details Page', () => { ...@@ -244,7 +201,7 @@ describe('Details Page', () => {
await waitForApolloRequestRender(); await waitForApolloRequestRender();
findTagsList().vm.$emit('delete', tagsArrayToSelectedTags(cleanTags)); findTagsList().vm.$emit('delete', cleanTags);
}); });
it('open the modal', () => { it('open the modal', () => {
...@@ -260,61 +217,6 @@ describe('Details Page', () => { ...@@ -260,61 +217,6 @@ describe('Details Page', () => {
}); });
}); });
describe('pagination', () => {
it('exists', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findPagination().exists()).toBe(true);
});
it('is hidden when there are no more pages', async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock) });
await waitForApolloRequestRender();
expect(findPagination().exists()).toBe(false);
});
it('is wired to the correct pagination props', async () => {
mountComponent();
await waitForApolloRequestRender();
expect(findPagination().props()).toMatchObject({
hasNextPage: tagsPageInfo.hasNextPage,
hasPreviousPage: tagsPageInfo.hasPreviousPage,
});
});
it('fetch next page when user clicks next', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
mountComponent({ resolver });
await waitForApolloRequestRender();
findPagination().vm.$emit('next');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ after: tagsPageInfo.endCursor }),
);
});
it('fetch previous page when user clicks prev', async () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsMock());
mountComponent({ resolver });
await waitForApolloRequestRender();
findPagination().vm.$emit('prev');
expect(resolver).toHaveBeenCalledWith(
expect.objectContaining({ first: null, before: tagsPageInfo.startCursor }),
);
});
});
describe('modal', () => { describe('modal', () => {
it('exists', async () => { it('exists', async () => {
mountComponent(); mountComponent();
...@@ -349,7 +251,7 @@ describe('Details Page', () => { ...@@ -349,7 +251,7 @@ describe('Details Page', () => {
}); });
describe('when one item is selected to be deleted', () => { describe('when one item is selected to be deleted', () => {
it('calls apollo mutation with the right parameters', async () => { it('calls apollo mutation with the right parameters', async () => {
findTagsList().vm.$emit('delete', { [cleanTags[0].name]: true }); findTagsList().vm.$emit('delete', [cleanTags[0]]);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
...@@ -363,7 +265,7 @@ describe('Details Page', () => { ...@@ -363,7 +265,7 @@ describe('Details Page', () => {
describe('when more than one item is selected to be deleted', () => { describe('when more than one item is selected to be deleted', () => {
it('calls apollo mutation with the right parameters', async () => { it('calls apollo mutation with the right parameters', async () => {
findTagsList().vm.$emit('delete', { ...tagsArrayToSelectedTags(tagsMock) }); findTagsList().vm.$emit('delete', tagsMock);
await wrapper.vm.$nextTick(); await wrapper.vm.$nextTick();
......
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