Skip to content
Snippets Groups Projects
Commit cf506304 authored by Nicolò Mezzopera's avatar Nicolò Mezzopera Committed by Enrique Alcántara
Browse files

Generalise empty state component

- rename
- add image related messages
- tests
parent ed29856f
No related branches found
No related tags found
No related merge requests found
<script>
import { GlEmptyState } from '@gitlab/ui';
import {
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
NO_TAGS_TITLE,
NO_TAGS_MESSAGE,
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '../../constants/index';
 
export default {
Loading
Loading
@@ -15,19 +17,28 @@ export default {
required: false,
default: '',
},
isEmptyImage: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
computed: {
title() {
return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_TITLE : NO_TAGS_TITLE;
},
description() {
return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_MESSAGE : NO_TAGS_MESSAGE;
},
},
};
</script>
 
<template>
<gl-empty-state
:title="$options.i18n.EMPTY_IMAGE_REPOSITORY_TITLE"
:title="title"
:svg-path="noContainersImage"
:description="$options.i18n.EMPTY_IMAGE_REPOSITORY_MESSAGE"
:description="description"
class="gl-mx-auto gl-my-0"
/>
</template>
Loading
Loading
@@ -40,12 +40,23 @@ export const REMOVE_TAG_CONFIRMATION_TEXT = s__(
export const REMOVE_TAGS_CONFIRMATION_TEXT = s__(
`ContainerRegistry|You are about to remove %{item} tags. Are you sure?`,
);
export const EMPTY_IMAGE_REPOSITORY_TITLE = s__('ContainerRegistry|This image has no active tags');
export const EMPTY_IMAGE_REPOSITORY_MESSAGE = s__(
export const NO_TAGS_TITLE = s__('ContainerRegistry|This image has no active tags');
export const NO_TAGS_MESSAGE = s__(
`ContainerRegistry|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 MISSING_OR_DELETED_IMAGE_TITLE = s__(
'ContainerRegistry|The image repository could not be found.',
);
export const MISSING_OR_DELETED_IMAGE_MESSAGE = s__(
'ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page.',
);
export const MISSING_OR_DELETE_IMAGE_BREADCRUMB = s__(
'ContainerRegistry|Image repository not found',
);
export const ADMIN_GARBAGE_COLLECTION_TIP = s__(
'ContainerRegistry|Remember to run %{docLinkStart}garbage collection%{docLinkEnd} to remove the stale data from storage.',
);
Loading
Loading
Loading
Loading
@@ -11,7 +11,7 @@ import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
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 EmptyState from '../components/details_page/empty_state.vue';
 
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
Loading
Loading
@@ -24,6 +24,7 @@ import {
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETE_IMAGE_BREADCRUMB,
} from '../constants/index';
 
export default {
Loading
Loading
@@ -36,7 +37,7 @@ export default {
DeleteModal,
TagsList,
TagsLoader,
EmptyTagsState,
EmptyState,
},
directives: {
GlResizeObserver: GlResizeObserverDirective,
Loading
Loading
@@ -54,7 +55,7 @@ export default {
},
result({ data }) {
this.tagsPageInfo = data.containerRepository?.tags?.pageInfo;
this.breadCrumbState.updateName(data.containerRepository?.name);
this.updateBreadcrumb();
},
error() {
createFlash({ message: FETCH_IMAGES_LIST_ERROR_MESSAGE });
Loading
Loading
@@ -101,8 +102,15 @@ export default {
showPagination() {
return this.tagsPageInfo.hasPreviousPage || this.tagsPageInfo.hasNextPage;
},
hasNoTags() {
return this.tags.length === 0;
},
},
methods: {
updateBreadcrumb() {
const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
this.breadCrumbState.updateName(name);
},
deleteTags(toBeDeleted) {
this.itemsToBeDeleted = this.tags.filter((tag) => toBeDeleted[tag.name]);
this.track('click_button');
Loading
Loading
@@ -182,45 +190,48 @@ export default {
 
<template>
<div v-gl-resize-observer="handleResize" class="gl-my-3">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
:is-admin="config.isAdmin"
class="gl-my-2"
/>
<template v-if="image">
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
:is-admin="config.isAdmin"
class="gl-my-2"
/>
 
<partial-cleanup-alert
v-if="showPartialCleanupWarning"
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
:cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
@dismiss="dismissPartialCleanupWarning"
/>
<partial-cleanup-alert
v-if="showPartialCleanupWarning"
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
:cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
@dismiss="dismissPartialCleanupWarning"
/>
 
<details-header :image="image" :metadata-loading="isLoading" />
<details-header :image="image" :metadata-loading="isLoading" />
 
<tags-loader v-if="isLoading" />
<template v-else>
<empty-tags-state v-if="tags.length === 0" :no-containers-image="config.noContainersImage" />
<tags-loader v-if="isLoading" />
<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>
<empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
<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>
</template>
 
<delete-modal
ref="deleteModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirmDelete="handleDelete"
@cancel="track('cancel_delete')"
/>
<delete-modal
ref="deleteModal"
:items-to-be-deleted="itemsToBeDeleted"
@confirmDelete="handleDelete"
@cancel="track('cancel_delete')"
/>
</template>
<empty-state v-else is-empty-image :no-containers-image="config.noContainersImage" />
</div>
</template>
---
title: Add 404 state to container registry details page
merge_request: 52466
author:
type: changed
Loading
Loading
@@ -7660,6 +7660,9 @@ msgstr ""
msgid "ContainerRegistry|Image repository deletion failed"
msgstr ""
 
msgid "ContainerRegistry|Image repository not found"
msgstr ""
msgid "ContainerRegistry|Image repository will be deleted"
msgstr ""
 
Loading
Loading
@@ -7785,9 +7788,15 @@ msgstr ""
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
 
msgid "ContainerRegistry|The image repository could not be found."
msgstr ""
msgid "ContainerRegistry|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 "ContainerRegistry|The requested image repository does not exist or has been deleted. If you think this is an error, try refreshing the page."
msgstr ""
msgid "ContainerRegistry|The value of this input should be less than 256 characters"
msgstr ""
 
Loading
Loading
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/empty_tags_state.vue';
import component from '~/registry/explorer/components/details_page/empty_state.vue';
import {
EMPTY_IMAGE_REPOSITORY_TITLE,
EMPTY_IMAGE_REPOSITORY_MESSAGE,
NO_TAGS_TITLE,
NO_TAGS_MESSAGE,
MISSING_OR_DELETED_IMAGE_TITLE,
MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '~/registry/explorer/constants';
 
describe('EmptyTagsState component', () => {
Loading
Loading
@@ -11,14 +13,12 @@ describe('EmptyTagsState component', () => {
 
const findEmptyState = () => wrapper.find(GlEmptyState);
 
const mountComponent = () => {
const mountComponent = (propsData) => {
wrapper = shallowMount(component, {
stubs: {
GlEmptyState,
},
propsData: {
noContainersImage: 'foo',
},
propsData,
});
};
 
Loading
Loading
@@ -32,12 +32,23 @@ describe('EmptyTagsState component', () => {
expect(findEmptyState().exists()).toBe(true);
});
 
it('has the correct props', () => {
mountComponent();
expect(findEmptyState().props()).toMatchObject({
title: EMPTY_IMAGE_REPOSITORY_TITLE,
description: EMPTY_IMAGE_REPOSITORY_MESSAGE,
svgPath: 'foo',
});
});
it.each`
isEmptyImage | title | description
${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE}
${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE}
`(
'when isEmptyImage is $isEmptyImage has the correct props',
({ isEmptyImage, title, description }) => {
mountComponent({
noContainersImage: 'foo',
isEmptyImage,
});
expect(findEmptyState().props()).toMatchObject({
title,
description,
svgPath: 'foo',
});
},
);
});
Loading
Loading
@@ -235,3 +235,9 @@ export const graphQLProjectImageRepositoriesDetailsMock = {
},
},
};
export const graphQLEmptyImageDetailsMock = {
data: {
containerRepository: null,
},
};
Loading
Loading
@@ -11,7 +11,7 @@ import PartialCleanupAlert from '~/registry/explorer/components/details_page/par
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
 
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
Loading
Loading
@@ -23,6 +23,7 @@ import {
graphQLImageDetailsEmptyTagsMock,
graphQLDeleteImageRepositoryTagsMock,
containerRepositoryMock,
graphQLEmptyImageDetailsMock,
tagsMock,
tagsPageInfo,
} from '../mock_data';
Loading
Loading
@@ -40,7 +41,7 @@ describe('Details Page', () => {
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
const findEmptyTagsState = () => wrapper.find(EmptyTagsState);
const findEmptyState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
 
const routeId = 1;
Loading
Loading
@@ -134,6 +135,27 @@ describe('Details Page', () => {
});
});
 
describe('when the image does not exist', () => {
it('does not show the default ui', async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(findTagsLoader().exists()).toBe(false);
expect(findDetailsHeader().exists()).toBe(false);
expect(findTagsList().exists()).toBe(false);
expect(findPagination().exists()).toBe(false);
});
it('shows an empty state message', async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(findEmptyState().exists()).toBe(true);
});
});
describe('when the list of tags is empty', () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
 
Loading
Loading
@@ -142,7 +164,7 @@ describe('Details Page', () => {
 
await waitForApolloRequestRender();
 
expect(findEmptyTagsState().exists()).toBe(true);
expect(findEmptyState().exists()).toBe(true);
});
 
it('does not show the loader', async () => {
Loading
Loading
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment