Commit 6ac20f41 authored by Martin Wortschack's avatar Martin Wortschack

Merge branch '227558-missing-digest-revision-and-short-revision-in-tags' into 'master'

Add broken tag state to tags list items

See merge request gitlab-org/gitlab!36442
parents f5a8e1ee 9b06a994
<script>
import { GlFormCheckbox, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
import { GlFormCheckbox, GlTooltipDirective, GlSprintf, GlIcon } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { numberToHumanSize } from '~/lib/utils/number_utils';
......@@ -16,12 +16,16 @@ import {
PUBLISHED_DETAILS_ROW_TEXT,
MANIFEST_DETAILS_ROW_TEST,
CONFIGURATION_DETAILS_ROW_TEST,
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '../../constants/index';
export default {
components: {
GlSprintf,
GlFormCheckbox,
GlIcon,
DeleteButton,
ListItem,
ClipboardButton,
......@@ -55,10 +59,11 @@ export default {
PUBLISHED_DETAILS_ROW_TEXT,
MANIFEST_DETAILS_ROW_TEST,
CONFIGURATION_DETAILS_ROW_TEST,
MISSING_MANIFEST_WARNING_TOOLTIP,
},
computed: {
formattedSize() {
return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : '';
return this.tag.total_size ? numberToHumanSize(this.tag.total_size) : NOT_AVAILABLE_SIZE;
},
layers() {
return this.tag.layers ? n__('%d layer', '%d layers', this.tag.layers) : '';
......@@ -68,7 +73,7 @@ export default {
},
shortDigest() {
// remove sha256: from the string, and show only the first 7 char
return this.tag.digest?.substring(7, 14);
return this.tag.digest?.substring(7, 14) ?? NOT_AVAILABLE_TEXT;
},
publishedDate() {
return formatDate(this.tag.created_at, 'isoDate');
......@@ -85,6 +90,9 @@ export default {
tagLocation() {
return this.tag.path?.replace(`:${this.tag.name}`, '');
},
invalidTag() {
return !this.tag.digest;
},
},
};
</script>
......@@ -94,6 +102,7 @@ export default {
<template #left-action>
<gl-form-checkbox
v-if="Boolean(tag.destroy_path)"
:disabled="invalidTag"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"
......@@ -116,6 +125,13 @@ export default {
:text="tag.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
<gl-icon
v-if="invalidTag"
v-gl-tooltip="{ title: $options.i18n.MISSING_MANIFEST_WARNING_TOOLTIP }"
name="warning"
class="gl-text-orange-500 gl-mb-2 gl-ml-2"
/>
</div>
</template>
......@@ -146,7 +162,7 @@ export default {
</template>
<template #right-action>
<delete-button
:disabled="!tag.destroy_path"
:disabled="!tag.destroy_path || invalidTag"
:title="$options.i18n.REMOVE_TAG_BUTTON_TITLE"
:tooltip-title="$options.i18n.REMOVE_TAG_BUTTON_DISABLE_TOOLTIP"
:tooltip-disabled="Boolean(tag.destroy_path)"
......@@ -154,7 +170,8 @@ export default {
@delete="$emit('delete')"
/>
</template>
<template #details_published>
<template v-if="!invalidTag" #details_published>
<details-row icon="clock" data-testid="published-date-detail">
<gl-sprintf :message="$options.i18n.PUBLISHED_DETAILS_ROW_TEXT">
<template #repositoryPath>
......@@ -169,7 +186,7 @@ export default {
</gl-sprintf>
</details-row>
</template>
<template #details_manifest_digest>
<template v-if="!invalidTag" #details_manifest_digest>
<details-row icon="log" data-testid="manifest-detail">
<gl-sprintf :message="$options.i18n.MANIFEST_DETAILS_ROW_TEST">
<template #digest>
......@@ -184,7 +201,7 @@ export default {
/>
</details-row>
</template>
<template #details_configuration_digest>
<template v-if="!invalidTag" #details_configuration_digest>
<details-row icon="cloud-gear" data-testid="configuration-detail">
<gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST">
<template #digest>
......
......@@ -88,7 +88,7 @@ export default {
v-if="item.failedDelete"
v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
name="warning"
class="text-warning"
class="gl-text-orange-500"
/>
</template>
<template #left-secondary>
......
import { s__ } from '~/locale';
import { s__, __ } from '~/locale';
// Translations strings
export const DETAILS_PAGE_TITLE = s__('ContainerRegistry|%{imageName} tags');
......@@ -48,6 +48,12 @@ export const REMOVE_TAG_BUTTON_DISABLE_TOOLTIP = s__(
'ContainerRegistry|Deletion disabled due to missing or insufficient permissions.',
);
export const MISSING_MANIFEST_WARNING_TOOLTIP = s__(
'ContainerRegistry|Invalid tag: missing manifest digest',
);
export const NOT_AVAILABLE_TEXT = __('N/A');
export const NOT_AVAILABLE_SIZE = __('0 bytes');
// Parameters
export const DEFAULT_PAGE = 1;
......
---
title: Add broken tag state to tags list items
merge_request: 36442
author:
type: changed
......@@ -824,6 +824,9 @@ msgstr ""
msgid "- show less"
msgstr ""
msgid "0 bytes"
msgstr ""
msgid "0 for unlimited"
msgstr ""
......@@ -6306,6 +6309,9 @@ msgstr ""
msgid "ContainerRegistry|Image tags"
msgstr ""
msgid "ContainerRegistry|Invalid tag: missing manifest digest"
msgstr ""
msgid "ContainerRegistry|Login"
msgstr ""
......@@ -15110,6 +15116,9 @@ msgstr ""
msgid "My-Reaction"
msgstr ""
msgid "N/A"
msgstr ""
msgid "Name"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
......@@ -9,6 +9,9 @@ import DetailsRow from '~/registry/explorer/components/details_page/details_row.
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
MISSING_MANIFEST_WARNING_TOOLTIP,
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '~/registry/explorer/constants/index';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
......@@ -33,6 +36,7 @@ describe('tags list row', () => {
const findPublishedDateDetail = () => wrapper.find('[data-testid="published-date-detail"]');
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
const findWarningIcon = () => wrapper.find(GlIcon);
const mountComponent = (propsData = defaultProps) => {
wrapper = shallowMount(component, {
......@@ -68,6 +72,11 @@ describe('tags list row', () => {
expect(findCheckbox().exists()).toBe(false);
});
it('is disabled when the digest is missing', () => {
mountComponent({ tag: { ...tag, digest: null } });
expect(findCheckbox().attributes('disabled')).toBe('true');
});
it('is wired to the selected prop', () => {
mountComponent({ ...defaultProps, selected: true });
......@@ -134,6 +143,27 @@ describe('tags list row', () => {
});
});
describe('warning icon', () => {
it('is normally hidden', () => {
mountComponent();
expect(findWarningIcon().exists()).toBe(false);
});
it('is shown when the tag is broken', () => {
mountComponent({ tag: { ...tag, digest: null } });
expect(findWarningIcon().exists()).toBe(true);
});
it('has an appropriate tooltip', () => {
mountComponent({ tag: { ...tag, digest: null } });
const tooltip = getBinding(findWarningIcon().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(MISSING_MANIFEST_WARNING_TOOLTIP);
});
});
describe('size', () => {
it('exists', () => {
mountComponent();
......@@ -150,7 +180,7 @@ describe('tags list row', () => {
it('when total_size is missing', () => {
mountComponent();
expect(findSize().text()).toMatchInterpolatedText('10 layers');
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 10 layers`);
});
it('when layers are missing', () => {
......@@ -162,7 +192,7 @@ describe('tags list row', () => {
it('when there is 1 layer', () => {
mountComponent({ ...defaultProps, tag: { ...tag, layers: 1 } });
expect(findSize().text()).toMatchInterpolatedText('1 layer');
expect(findSize().text()).toMatchInterpolatedText(`${NOT_AVAILABLE_SIZE} · 1 layer`);
});
});
......@@ -204,6 +234,12 @@ describe('tags list row', () => {
expect(findShortRevision().text()).toMatchInterpolatedText('Digest: 1ab51d5');
});
it(`displays ${NOT_AVAILABLE_TEXT} when digest is missing`, () => {
mountComponent({ tag: { ...tag, digest: null } });
expect(findShortRevision().text()).toMatchInterpolatedText(`Digest: ${NOT_AVAILABLE_TEXT}`);
});
});
describe('delete button', () => {
......@@ -223,11 +259,19 @@ describe('tags list row', () => {
});
});
it('is disabled when tag has no destroy path', () => {
mountComponent({ ...defaultProps, tag: { ...tag, destroy_path: null } });
expect(findDeleteButton().attributes('disabled')).toBe('true');
});
it.each`
destroy_path | digest
${'foo'} | ${null}
${null} | ${'foo'}
${null} | ${null}
`(
'is disabled when destroy_path is $destroy_path and digest is $digest',
({ destroy_path, digest }) => {
mountComponent({ ...defaultProps, tag: { ...tag, destroy_path, digest } });
expect(findDeleteButton().attributes('disabled')).toBe('true');
},
);
it('delete event emits delete', () => {
mountComponent();
......@@ -239,36 +283,47 @@ describe('tags list row', () => {
});
describe('details rows', () => {
beforeEach(() => {
mountComponent();
describe('when the tag has a digest', () => {
beforeEach(() => {
mountComponent();
return wrapper.vm.$nextTick();
});
it('has 3 details rows', () => {
expect(findDetailsRows().length).toBe(3);
});
return wrapper.vm.$nextTick();
});
describe.each`
name | finderFunction | text | icon | clipboard
${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false}
${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true}
${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, () => {
expect(finderFunction().text()).toMatchInterpolatedText(text);
it('has 3 details rows', () => {
expect(findDetailsRows().length).toBe(3);
});
it(`has the ${icon} icon`, () => {
expect(finderFunction().props('icon')).toBe(icon);
describe.each`
name | finderFunction | text | icon | clipboard
${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the bar image repository at 10:23 GMT+0000 on 2020-06-29'} | ${'clock'} | ${false}
${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:1ab51d519f574b636ae7788051c60239334ae8622a9fd82a0cf7bae7786dfd5c'} | ${'log'} | ${true}
${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:b118ab5b0e90b7cb5127db31d5321ac14961d097516a8e0e72084b6cdc783b43'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, () => {
expect(finderFunction().text()).toMatchInterpolatedText(text);
});
it(`has the ${icon} icon`, () => {
expect(finderFunction().props('icon')).toBe(icon);
});
it(`is ${clipboard} that clipboard button exist`, () => {
expect(
finderFunction()
.find(ClipboardButton)
.exists(),
).toBe(clipboard);
});
});
});
describe('when the tag does not have a digest', () => {
it('hides the details rows', async () => {
mountComponent({ tag: { ...tag, digest: null } });
it(`is ${clipboard} that clipboard button exist`, () => {
expect(
finderFunction()
.find(ClipboardButton)
.exists(),
).toBe(clipboard);
await wrapper.vm.$nextTick();
expect(findDetailsRows().length).toBe(0);
});
});
});
......
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