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