Commit 23b43456 authored by Kushal Pandya's avatar Kushal Pandya

Merge branch...

Merge branch '216757-include-tag-count-in-the-image-repository-list-view-of-the-container-registry' into 'master'

Include tag count in the image repository list

See merge request gitlab-org/gitlab!33027
parents 8f87bd8c fbe31d4d
<script>
import { GlPagination, GlTooltipDirective, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
} from '../constants';
import { GlPagination } from '@gitlab/ui';
import ImageListRow from './image_list_row.vue';
export default {
name: 'ImageList',
components: {
GlPagination,
ClipboardButton,
GlDeprecatedButton,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
ImageListRow,
},
props: {
images: {
......@@ -30,12 +18,6 @@ export default {
required: true,
},
},
i18n: {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
currentPage: {
get() {
......@@ -46,79 +28,25 @@ export default {
},
},
},
methods: {
encodeListItem(item) {
const params = JSON.stringify({ name: item.path, tags_path: item.tags_path, id: item.id });
return window.btoa(params);
},
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column">
<div
<image-list-row
v-for="(listItem, index) in images"
:key="index"
v-gl-tooltip="{
placement: 'left',
disabled: !listItem.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
data-testid="rowItem"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 border-bottom"
:class="{ 'border-top': index === 0, 'disabled-content': listItem.deleting }"
>
<div class="gl-display-flex gl-align-items-center">
<router-link
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodeListItem(listItem) } }"
>
{{ listItem.path }}
</router-link>
<clipboard-button
v-if="listItem.location"
:disabled="listItem.deleting"
:text="listItem.location"
:title="listItem.location"
css-class="btn-default btn-transparent btn-clipboard"
/>
<gl-icon
v-if="listItem.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning align-middle"
/>
</div>
<div
v-gl-tooltip="{ disabled: listItem.destroy_path }"
class="d-none d-sm-block"
:title="$options.i18n.LIST_DELETE_BUTTON_DISABLED"
>
<gl-deprecated-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="!listItem.destroy_path || listItem.deleting"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
class="btn-inverted"
variant="danger"
@click="$emit('delete', listItem)"
>
<gl-icon name="remove" />
</gl-deprecated-button>
</div>
</div>
</div>
:item="listItem"
:show-top-border="index === 0"
@delete="$emit('delete', $event)"
/>
<gl-pagination
v-model="currentPage"
:per-page="pagination.perPage"
:total-items="pagination.total"
align="center"
class="w-100 gl-mt-2"
class="w-100 gl-mt-3"
/>
</div>
</template>
<script>
import { GlTooltipDirective, GlButton, GlIcon, GlSprintf } from '@gitlab/ui';
import { n__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
} from '../constants';
export default {
name: 'ImageListrow',
components: {
ClipboardButton,
GlButton,
GlSprintf,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
item: {
type: Object,
required: true,
},
showTopBorder: {
type: Boolean,
default: false,
required: false,
},
},
i18n: {
LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL,
ROW_SCHEDULED_FOR_DELETION,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
encodedItem() {
const params = JSON.stringify({
name: this.item.path,
tags_path: this.item.tags_path,
id: this.item.id,
});
return window.btoa(params);
},
disabledDelete() {
return !this.item.destroy_path || this.item.deleting;
},
tagsCountText() {
return n__(
'ContainerRegistry|%{count} Tag',
'ContainerRegistry|%{count} Tags',
this.item.tags_count,
);
},
},
};
</script>
<template>
<div
v-gl-tooltip="{
placement: 'left',
disabled: !item.deleting,
title: $options.i18n.ROW_SCHEDULED_FOR_DELETION,
}"
>
<div
class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-py-2 gl-px-1 gl-border-gray-200 gl-border-b-solid gl-border-b-1 gl-py-4 "
:class="{
'gl-border-t-solid gl-border-t-1': showTopBorder,
'disabled-content': item.deleting,
}"
>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center">
<router-link
class="gl-text-black-normal gl-font-weight-bold"
data-testid="detailsLink"
:to="{ name: 'details', params: { id: encodedItem } }"
>
{{ item.path }}
</router-link>
<clipboard-button
v-if="item.location"
:disabled="item.deleting"
:text="item.location"
:title="item.location"
css-class="btn-default btn-transparent btn-clipboard gl-text-gray-500"
/>
<gl-icon
v-if="item.failedDelete"
v-gl-tooltip
:title="$options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE"
name="warning"
class="text-warning"
/>
</div>
<div class="gl-font-sm gl-text-gray-500">
<span class="gl-display-flex gl-align-items-center" data-testid="tagsCount">
<gl-icon name="tag" class="gl-mr-2" />
<gl-sprintf :message="tagsCountText">
<template #count>
{{ item.tags_count }}
</template>
</gl-sprintf>
</span>
</div>
</div>
<div
v-gl-tooltip="{
disabled: item.destroy_path,
title: $options.i18n.LIST_DELETE_BUTTON_DISABLED,
}"
class="d-none d-sm-block"
data-testid="deleteButtonWrapper"
>
<gl-button
v-gl-tooltip
data-testid="deleteImageButton"
:disabled="disabledDelete"
:title="$options.i18n.REMOVE_REPOSITORY_LABEL"
:aria-label="$options.i18n.REMOVE_REPOSITORY_LABEL"
class="btn-inverted"
variant="danger"
icon="remove"
@click="$emit('delete', item)"
/>
</div>
</div>
</div>
</template>
---
title: Include tag count in the image repository list
merge_request: 33027
author:
type: changed
......@@ -5849,6 +5849,11 @@ msgid_plural "ContainerRegistry|%{count} Image repositories"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{count} Tag"
msgid_plural "ContainerRegistry|%{count} Tags"
msgstr[0] ""
msgstr[1] ""
msgid "ContainerRegistry|%{imageName} tags"
msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlSprintf } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import Component from '~/registry/explorer/components/image_list_row.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import {
ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED,
} from '~/registry/explorer/constants';
import { RouterLink } from '../stubs';
import { imagesListResponse } from '../mock_data';
describe('Image List Row', () => {
let wrapper;
const item = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findTagsCount = () => wrapper.find('[data-testid="tagsCount"]');
const findDeleteButtonWrapper = () => wrapper.find('[data-testid="deleteButtonWrapper"]');
const findClipboardButton = () => wrapper.find(ClipboardButton);
const mountComponent = props => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
GlSprintf,
},
propsData: {
item,
...props,
},
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('main tooltip', () => {
it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
mountComponent();
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
});
it('is disabled when item is being deleted', () => {
mountComponent({ item: { ...item, deleting: true } });
const tooltip = getBinding(wrapper.element, 'gl-tooltip');
expect(tooltip.value.disabled).toBe(false);
});
});
describe('image title and path', () => {
it('contains a link to the details page', () => {
mountComponent();
const link = findDetailsLink();
expect(link.html()).toContain(item.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
mountComponent();
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(item.location);
expect(button.props('title')).toBe(item.location);
});
});
describe('delete button wrapper', () => {
it('has a tooltip', () => {
mountComponent();
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(LIST_DELETE_BUTTON_DISABLED);
});
it('tooltip is enabled when destroy_path is falsy', () => {
mountComponent({ item: { ...item, destroy_path: null } });
const tooltip = getBinding(findDeleteButtonWrapper().element, 'gl-tooltip');
expect(tooltip.value.disabled).toBeFalsy();
});
});
describe('delete button', () => {
it('exists', () => {
mountComponent();
expect(findDeleteBtn().exists()).toBe(true);
});
it('emits a delete event', () => {
mountComponent();
findDeleteBtn().vm.$emit('click');
expect(wrapper.emitted('delete')).toEqual([[item]]);
});
it.each`
destroy_path | deleting | state
${null} | ${null} | ${'true'}
${null} | ${true} | ${'true'}
${'foo'} | ${true} | ${'true'}
${'foo'} | ${false} | ${undefined}
`(
'disabled is $state when destroy_path is $destroy_path and deleting is $deleting',
({ destroy_path, deleting, state }) => {
mountComponent({ item: { ...item, destroy_path, deleting } });
expect(findDeleteBtn().attributes('disabled')).toBe(state);
},
);
});
describe('tags count', () => {
it('exists', () => {
mountComponent();
expect(findTagsCount().exists()).toBe(true);
});
it('contains a tag icon', () => {
mountComponent();
const icon = findTagsCount().find(GlIcon);
expect(icon.exists()).toBe(true);
expect(icon.props('name')).toBe('tag');
});
describe('tags count text', () => {
it('with one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 1 } });
expect(findTagsCount().text()).toMatchInterpolatedText('1 Tag');
});
it('with more than one tag in the image', () => {
mountComponent({ item: { ...item, tags_count: 3 } });
expect(findTagsCount().text()).toMatchInterpolatedText('3 Tags');
});
});
});
});
import { shallowMount } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import Component from '~/registry/explorer/components/image_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { RouterLink } from '../stubs';
import ImageListRow from '~/registry/explorer/components/image_list_row.vue';
import { imagesListResponse, imagePagination } from '../mock_data';
describe('Image List', () => {
let wrapper;
const firstElement = imagesListResponse.data[0];
const findDeleteBtn = () => wrapper.find('[data-testid="deleteImageButton"]');
const findRowItems = () => wrapper.findAll('[data-testid="rowItem"]');
const findDetailsLink = () => wrapper.find('[data-testid="detailsLink"]');
const findClipboardButton = () => wrapper.find(ClipboardButton);
const findRow = () => wrapper.findAll(ImageListRow);
const findPagination = () => wrapper.find(GlPagination);
const mountComponent = () => {
wrapper = shallowMount(Component, {
stubs: {
RouterLink,
},
propsData: {
images: imagesListResponse.data,
pagination: imagePagination,
......@@ -32,26 +24,17 @@ describe('Image List', () => {
mountComponent();
});
it('contains one list element for each image', () => {
expect(findRowItems().length).toBe(imagesListResponse.data.length);
});
it('contains a link to the details page', () => {
const link = findDetailsLink();
expect(link.html()).toContain(firstElement.path);
expect(link.props('to').name).toBe('details');
});
it('contains a clipboard button', () => {
const button = findClipboardButton();
expect(button.exists()).toBe(true);
expect(button.props('text')).toBe(firstElement.location);
expect(button.props('title')).toBe(firstElement.location);
});
describe('list', () => {
it('contains one list element for each image', () => {
expect(findRow().length).toBe(imagesListResponse.data.length);
});
it('should be possible to delete a repo', () => {
const deleteBtn = findDeleteBtn();
expect(deleteBtn.exists()).toBe(true);
it('when delete event is emitted on the row it emits up a delete event', () => {
findRow()
.at(0)
.vm.$emit('delete', 'foo');
expect(wrapper.emitted('delete')).toEqual([['foo']]);
});
});
describe('pagination', () => {
......
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