Commit 1142bacb authored by Mike Greiling's avatar Mike Greiling

Merge branch '320901-container-page-says-image-repository-not-found' into 'master'

Use Root Image for images with missing name

See merge request gitlab-org/gitlab!54693
parents 0ca80dd7 6f3b0814
<script>
import { GlSprintf, GlButton } from '@gitlab/ui';
import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { sprintf, n__ } from '~/locale';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import {
DETAILS_PAGE_TITLE,
UPDATED_AT,
CLEANUP_UNSCHEDULED_TEXT,
CLEANUP_SCHEDULED_TEXT,
......@@ -20,11 +19,16 @@ import {
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
} from '../../constants/index';
export default {
name: 'DetailsHeader',
components: { GlSprintf, GlButton, TitleArea, MetadataItem },
components: { GlButton, GlIcon, TitleArea, MetadataItem },
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [timeagoMixin],
props: {
image: {
......@@ -73,9 +77,12 @@ export default {
deleteButtonDisabled() {
return this.disabled || !this.image.canDelete;
},
},
i18n: {
DETAILS_PAGE_TITLE,
rootImageTooltip() {
return !this.image.name ? ROOT_IMAGE_TOOLTIP : '';
},
imageName() {
return this.image.name || ROOT_IMAGE_TEXT;
},
},
};
</script>
......@@ -84,12 +91,15 @@ export default {
<title-area :metadata-loading="metadataLoading">
<template #title>
<span data-testid="title">
<gl-sprintf :message="$options.i18n.DETAILS_PAGE_TITLE">
<template #imageName>
{{ image.name }}
</template>
</gl-sprintf>
{{ imageName }}
</span>
<gl-icon
v-if="rootImageTooltip"
v-gl-tooltip="rootImageTooltip"
class="gl-text-blue-600"
name="information-o"
:aria-label="rootImageTooltip"
/>
</template>
<template #metadata-tags-count>
<metadata-item icon="tag" :text="tagCountText" data-testid="tags-count" />
......
......@@ -13,6 +13,7 @@ import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
ROOT_IMAGE_TEXT,
} from '../../constants/index';
import DeleteButton from '../delete_button.vue';
......@@ -74,6 +75,9 @@ export default {
}
return null;
},
imageName() {
return this.item.name ? this.item.path : `${this.item.path}/ ${ROOT_IMAGE_TEXT}`;
},
},
};
</script>
......@@ -95,7 +99,7 @@ export default {
data-qa-selector="registry_image_content"
:to="{ name: 'details', params: { id } }"
>
{{ item.path }}
{{ imageName }}
</router-link>
<clipboard-button
v-if="item.location"
......
import { s__ } from '~/locale';
export const ROOT_IMAGE_TEXT = s__('ContainerRegistry|Root image');
......@@ -2,7 +2,6 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import { s__, __ } from '~/locale';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
export const DELETE_TAG_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while marking the tag for deletion.',
);
......@@ -53,7 +52,8 @@ export const MISSING_OR_DELETED_IMAGE_TITLE = s__(
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__(
export const MISSING_OR_DELETED_IMAGE_BREADCRUMB = s__(
'ContainerRegistry|Image repository not found',
);
......@@ -112,6 +112,10 @@ export const FAILED_DELETION_STATUS_MESSAGE = s__(
'ContainerRegistry|This image repository has failed to be deleted',
);
export const ROOT_IMAGE_TOOLTIP = s__(
'ContainerRegistry|Image repository with no name located at the project URL.',
);
// Parameters
export const DEFAULT_PAGE = 1;
......
export * from './common';
export * from './expiration_policies';
export * from './quick_start';
export * from './list';
......
......@@ -24,7 +24,8 @@ import {
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETE_IMAGE_BREADCRUMB,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
} from '../constants/index';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
......@@ -116,7 +117,9 @@ export default {
},
methods: {
updateBreadcrumb() {
const name = this.image?.name || MISSING_OR_DELETE_IMAGE_BREADCRUMB;
const name = this.image?.id
? this.image?.name || ROOT_IMAGE_TEXT
: MISSING_OR_DELETED_IMAGE_BREADCRUMB;
this.breadCrumbState.updateName(name);
},
deleteTags(toBeDeleted) {
......
---
title: Use Root Image for images with missing name
merge_request: 54693
author:
type: changed
......@@ -7955,9 +7955,6 @@ msgid_plural "ContainerRegistry|%{count} Tags"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
msgid "ContainerRegistry|%{strongStart}Disabled%{strongEnd} - Tags will not be automatically deleted."
msgstr ""
......@@ -8060,6 +8057,9 @@ msgstr ""
msgid "ContainerRegistry|Image repository will be deleted"
msgstr ""
msgid "ContainerRegistry|Image repository with no name located at the project URL."
msgstr ""
msgid "ContainerRegistry|Image tags"
msgstr ""
......@@ -8125,6 +8125,9 @@ msgstr ""
msgid "ContainerRegistry|Remove these tags"
msgstr ""
msgid "ContainerRegistry|Root image"
msgstr ""
msgid "ContainerRegistry|Run cleanup:"
msgstr ""
......
......@@ -67,7 +67,13 @@ RSpec.describe 'Container Registry', :js do
end
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
expect(page).to have_content 'my/image'
end
it 'shows the image tags' do
expect(page).to have_content 'Image tags'
first_tag = first('[data-testid="name"]')
expect(first_tag).to have_content 'latest'
end
it 'user removes a specific tag from container repository' do
......
......@@ -82,7 +82,13 @@ RSpec.describe 'Container Registry', :js do
end
it 'shows the image title' do
expect(page).to have_content 'my/image tags'
expect(page).to have_content 'my/image'
end
it 'shows the image tags' do
expect(page).to have_content 'Image tags'
first_tag = first('[data-testid="name"]')
expect(first_tag).to have_content '1'
end
it 'user removes a specific tag from container repository' do
......
import { GlSprintf, GlButton } from '@gitlab/ui';
import { GlButton, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/registry/explorer/components/details_page/details_header.vue';
import {
DETAILS_PAGE_TITLE,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
......@@ -13,6 +13,8 @@ import {
CLEANUP_SCHEDULED_TOOLTIP,
CLEANUP_ONGOING_TOOLTIP,
CLEANUP_UNFINISHED_TOOLTIP,
ROOT_IMAGE_TEXT,
ROOT_IMAGE_TOOLTIP,
} from '~/registry/explorer/constants';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
......@@ -41,6 +43,7 @@ describe('Details Header', () => {
const findTagsCount = () => findByTestId('tags-count');
const findCleanup = () => findByTestId('cleanup');
const findDeleteButton = () => wrapper.find(GlButton);
const findInfoIcon = () => wrapper.find(GlIcon);
const waitForMetadataItems = async () => {
// Metadata items are printed by a loop in the title-area and it takes two ticks for them to be available
......@@ -51,8 +54,10 @@ describe('Details Header', () => {
const mountComponent = (propsData = { image: defaultImage }) => {
wrapper = shallowMount(component, {
propsData,
directives: {
GlTooltip: createMockDirective(),
},
stubs: {
GlSprintf,
TitleArea,
},
});
......@@ -62,15 +67,41 @@ describe('Details Header', () => {
wrapper.destroy();
wrapper = null;
});
describe('image name', () => {
describe('missing image name', () => {
it('root image ', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
it('has the correct title ', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
expect(findTitle().text()).toMatchInterpolatedText(DETAILS_PAGE_TITLE);
});
expect(findTitle().text()).toBe(ROOT_IMAGE_TEXT);
});
it('shows imageName in the title', () => {
mountComponent();
expect(findTitle().text()).toContain('foo');
it('has an icon', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
expect(findInfoIcon().exists()).toBe(true);
expect(findInfoIcon().props('name')).toBe('information-o');
});
it('has a tooltip', () => {
mountComponent({ image: { ...defaultImage, name: '' } });
const tooltip = getBinding(findInfoIcon().element, 'gl-tooltip');
expect(tooltip.value).toBe(ROOT_IMAGE_TOOLTIP);
});
});
describe('with image name present', () => {
it('shows image.name ', () => {
mountComponent();
expect(findTitle().text()).toContain('foo');
});
it('has no icon', () => {
mountComponent();
expect(findInfoIcon().exists()).toBe(false);
});
});
});
describe('delete button', () => {
......
......@@ -12,6 +12,7 @@ import {
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS,
ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
......@@ -73,8 +74,8 @@ describe('Image List Row', () => {
mountComponent();
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to')).toMatchObject({
expect(link.text()).toBe(item.path);
expect(findDetailsLink().props('to')).toMatchObject({
name: 'details',
params: {
id: getIdFromGraphQLId(item.id),
......@@ -82,6 +83,12 @@ describe('Image List Row', () => {
});
});
it(`when the image has no name appends ${ROOT_IMAGE_TEXT} to the path`, () => {
mountComponent({ item: { ...item, name: '' } });
expect(findDetailsLink().text()).toBe(`${item.path}/ ${ROOT_IMAGE_TEXT}`);
});
it('contains a clipboard button', () => {
mountComponent();
const button = findClipboardButton();
......
......@@ -17,6 +17,8 @@ import {
UNFINISHED_STATUS,
DELETE_SCHEDULED,
ALERT_DANGER_IMAGE,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
......@@ -515,6 +517,26 @@ describe('Details Page', () => {
expect(breadCrumbState.updateName).toHaveBeenCalledWith(containerRepositoryMock.name);
});
it(`when the image is missing set the breadcrumb to ${MISSING_OR_DELETED_IMAGE_BREADCRUMB}`, async () => {
mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
await waitForApolloRequestRender();
expect(breadCrumbState.updateName).toHaveBeenCalledWith(MISSING_OR_DELETED_IMAGE_BREADCRUMB);
});
it(`when the image has no name set the breadcrumb to ${ROOT_IMAGE_TEXT}`, async () => {
mountComponent({
resolver: jest
.fn()
.mockResolvedValue(graphQLImageDetailsMock({ ...containerRepositoryMock, name: null })),
});
await waitForApolloRequestRender();
expect(breadCrumbState.updateName).toHaveBeenCalledWith(ROOT_IMAGE_TEXT);
});
});
describe('when the image has a status different from null', () => {
......
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