Commit 9253812f authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '331992-cleanup-timeout-warning-icon-display-bug' into 'master'

Add cleanup status by row component

See merge request gitlab-org/gitlab!65320
parents 8ea9b0e2 94709de7
<script>
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
UNFINISHED_STATUS,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
} from '../../constants/index';
export default {
name: 'CleanupStatus',
components: {
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
},
props: {
status: {
type: String,
required: true,
validator(value) {
return [UNFINISHED_STATUS, UNSCHEDULED_STATUS, SCHEDULED_STATUS, ONGOING_STATUS].includes(
value,
);
},
},
},
i18n: {
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
},
computed: {
showStatus() {
return this.status !== UNSCHEDULED_STATUS;
},
failedDelete() {
return this.status === UNFINISHED_STATUS;
},
statusText() {
return this.$options.i18n[`CLEANUP_STATUS_${this.status}`];
},
expireIconClass() {
return this.failedDelete ? 'gl-text-orange-500' : '';
},
},
};
</script>
<template>
<div v-if="showStatus" class="gl-display-inline-flex gl-align-items-center">
<gl-icon name="expire" data-testid="main-icon" :class="expireIconClass" />
<span class="gl-mx-2">
{{ statusText }}
</span>
<gl-icon
v-if="failedDelete"
v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
:size="14"
class="gl-text-black-normal"
data-testid="extra-info"
name="information"
/>
</div>
</template>
...@@ -16,6 +16,7 @@ import { ...@@ -16,6 +16,7 @@ import {
ROOT_IMAGE_TEXT, ROOT_IMAGE_TEXT,
} from '../../constants/index'; } from '../../constants/index';
import DeleteButton from '../delete_button.vue'; import DeleteButton from '../delete_button.vue';
import CleanupStatus from './cleanup_status.vue';
export default { export default {
name: 'ImageListRow', name: 'ImageListRow',
...@@ -26,6 +27,7 @@ export default { ...@@ -26,6 +27,7 @@ export default {
GlIcon, GlIcon,
ListItem, ListItem,
GlSkeletonLoader, GlSkeletonLoader,
CleanupStatus,
}, },
directives: { directives: {
GlTooltip: GlTooltipDirective, GlTooltip: GlTooltipDirective,
...@@ -112,27 +114,24 @@ export default { ...@@ -112,27 +114,24 @@ export default {
:title="item.location" :title="item.location"
category="tertiary" category="tertiary"
/> />
<gl-icon
v-if="warningIconText"
v-gl-tooltip="{ title: warningIconText }"
data-testid="warning-icon"
name="warning"
class="gl-text-orange-500"
/>
</template> </template>
<template #left-secondary> <template #left-secondary>
<span <template v-if="!metadataLoading">
v-if="!metadataLoading" <span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
class="gl-display-flex gl-align-items-center" <gl-icon name="tag" class="gl-mr-2" />
data-testid="tags-count" <gl-sprintf :message="tagsCountText">
> <template #count>
<gl-icon name="tag" class="gl-mr-2" /> {{ item.tagsCount }}
<gl-sprintf :message="tagsCountText"> </template>
<template #count> </gl-sprintf>
{{ item.tagsCount }} </span>
</template>
</gl-sprintf> <cleanup-status
</span> v-if="item.expirationPolicyCleanupStatus"
class="ml-2"
:status="item.expirationPolicyCleanupStatus"
/>
</template>
<div v-else class="gl-w-full"> <div v-else class="gl-w-full">
<gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet"> <gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet">
......
...@@ -89,6 +89,10 @@ export const CLEANUP_DISABLED_TOOLTIP = s__( ...@@ -89,6 +89,10 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project', 'ContainerRegistry|Cleanup is disabled for this project',
); );
export const CLEANUP_STATUS_SCHEDULED = s__('ContainerRegistry|Cleanup will run soon');
export const CLEANUP_STATUS_ONGOING = s__('ContainerRegistry|Cleanup is ongoing');
export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Cleanup timed out');
export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__( export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.', 'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
); );
......
...@@ -29,6 +29,7 @@ query getProjectContainerRepositories( ...@@ -29,6 +29,7 @@ query getProjectContainerRepositories(
canDelete canDelete
createdAt createdAt
expirationPolicyStartedAt expirationPolicyStartedAt
expirationPolicyCleanupStatus
__typename __typename
} }
pageInfo { pageInfo {
...@@ -61,6 +62,7 @@ query getProjectContainerRepositories( ...@@ -61,6 +62,7 @@ query getProjectContainerRepositories(
canDelete canDelete
createdAt createdAt
expirationPolicyStartedAt expirationPolicyStartedAt
expirationPolicyCleanupStatus
__typename __typename
} }
pageInfo { pageInfo {
......
...@@ -8490,6 +8490,9 @@ msgstr "" ...@@ -8490,6 +8490,9 @@ msgstr ""
msgid "ContainerRegistry|Cleanup is disabled for this project" msgid "ContainerRegistry|Cleanup is disabled for this project"
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup is ongoing"
msgstr ""
msgid "ContainerRegistry|Cleanup pending" msgid "ContainerRegistry|Cleanup pending"
msgstr "" msgstr ""
...@@ -8505,6 +8508,9 @@ msgstr "" ...@@ -8505,6 +8508,9 @@ msgstr ""
msgid "ContainerRegistry|Cleanup ran but some tags were not removed" msgid "ContainerRegistry|Cleanup ran but some tags were not removed"
msgstr "" msgstr ""
msgid "ContainerRegistry|Cleanup timed out"
msgstr ""
msgid "ContainerRegistry|Cleanup timed out before it could delete all tags" msgid "ContainerRegistry|Cleanup timed out before it could delete all tags"
msgstr "" msgstr ""
......
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
import {
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_STATUS_SCHEDULED,
CLEANUP_STATUS_ONGOING,
CLEANUP_STATUS_UNFINISHED,
UNFINISHED_STATUS,
UNSCHEDULED_STATUS,
SCHEDULED_STATUS,
ONGOING_STATUS,
} from '~/registry/explorer/constants';
describe('cleanup_status', () => {
let wrapper;
const findMainIcon = () => wrapper.findByTestId('main-icon');
const findExtraInfoIcon = () => wrapper.findByTestId('extra-info');
const mountComponent = (propsData = { status: SCHEDULED_STATUS }) => {
wrapper = shallowMountExtended(CleanupStatus, {
propsData,
directives: {
GlTooltip: createMockDirective(),
},
});
};
afterEach(() => {
wrapper.destroy();
});
it.each`
status | visible | text
${UNFINISHED_STATUS} | ${true} | ${CLEANUP_STATUS_UNFINISHED}
${SCHEDULED_STATUS} | ${true} | ${CLEANUP_STATUS_SCHEDULED}
${ONGOING_STATUS} | ${true} | ${CLEANUP_STATUS_ONGOING}
${UNSCHEDULED_STATUS} | ${false} | ${''}
`(
'when the status is $status is $visible that the component is mounted and has the correct text',
({ status, visible, text }) => {
mountComponent({ status });
expect(findMainIcon().exists()).toBe(visible);
expect(wrapper.text()).toBe(text);
},
);
describe('main icon', () => {
it('exists', () => {
mountComponent();
expect(findMainIcon().exists()).toBe(true);
});
it(`has the orange class when the status is ${UNFINISHED_STATUS}`, () => {
mountComponent({ status: UNFINISHED_STATUS });
expect(findMainIcon().classes('gl-text-orange-500')).toBe(true);
});
});
describe('extra info icon', () => {
it.each`
status | visible
${UNFINISHED_STATUS} | ${true}
${SCHEDULED_STATUS} | ${false}
${ONGOING_STATUS} | ${false}
`(
'when the status is $status is $visible that the extra icon is visible',
({ status, visible }) => {
mountComponent({ status });
expect(findExtraInfoIcon().exists()).toBe(visible);
},
);
it(`has a tooltip`, () => {
mountComponent({ status: UNFINISHED_STATUS });
const tooltip = getBinding(findExtraInfoIcon().element, 'gl-tooltip');
expect(tooltip.value.title).toBe(ASYNC_DELETE_IMAGE_ERROR_MESSAGE);
});
});
});
...@@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -3,15 +3,14 @@ import { shallowMount } from '@vue/test-utils';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive'; import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils'; import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import DeleteButton from '~/registry/explorer/components/delete_button.vue'; import DeleteButton from '~/registry/explorer/components/delete_button.vue';
import CleanupStatus from '~/registry/explorer/components/list_page/cleanup_status.vue';
import Component from '~/registry/explorer/components/list_page/image_list_row.vue'; import Component from '~/registry/explorer/components/list_page/image_list_row.vue';
import { import {
ROW_SCHEDULED_FOR_DELETION, ROW_SCHEDULED_FOR_DELETION,
LIST_DELETE_BUTTON_DISABLED, LIST_DELETE_BUTTON_DISABLED,
REMOVE_REPOSITORY_LABEL, REMOVE_REPOSITORY_LABEL,
ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
CLEANUP_TIMED_OUT_ERROR_MESSAGE,
IMAGE_DELETE_SCHEDULED_STATUS, IMAGE_DELETE_SCHEDULED_STATUS,
IMAGE_FAILED_DELETED_STATUS, SCHEDULED_STATUS,
ROOT_IMAGE_TEXT, ROOT_IMAGE_TEXT,
} from '~/registry/explorer/constants'; } from '~/registry/explorer/constants';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
...@@ -27,7 +26,7 @@ describe('Image List Row', () => { ...@@ -27,7 +26,7 @@ describe('Image List Row', () => {
const findTagsCount = () => wrapper.find('[data-testid="tags-count"]'); const findTagsCount = () => wrapper.find('[data-testid="tags-count"]');
const findDeleteBtn = () => wrapper.findComponent(DeleteButton); const findDeleteBtn = () => wrapper.findComponent(DeleteButton);
const findClipboardButton = () => wrapper.findComponent(ClipboardButton); const findClipboardButton = () => wrapper.findComponent(ClipboardButton);
const findWarningIcon = () => wrapper.find('[data-testid="warning-icon"]'); const findCleanupStatus = () => wrapper.findComponent(CleanupStatus);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader); const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findListItemComponent = () => wrapper.findComponent(ListItem); const findListItemComponent = () => wrapper.findComponent(ListItem);
...@@ -106,23 +105,22 @@ describe('Image List Row', () => { ...@@ -106,23 +105,22 @@ describe('Image List Row', () => {
expect(button.props('title')).toBe(item.location); expect(button.props('title')).toBe(item.location);
}); });
describe('warning icon', () => { describe('cleanup status component', () => {
it.each` it.each`
status | expirationPolicyStartedAt | shown | title expirationPolicyCleanupStatus | shown
${IMAGE_FAILED_DELETED_STATUS} | ${true} | ${true} | ${ASYNC_DELETE_IMAGE_ERROR_MESSAGE} ${null} | ${false}
${''} | ${true} | ${true} | ${CLEANUP_TIMED_OUT_ERROR_MESSAGE} ${SCHEDULED_STATUS} | ${true}
${''} | ${false} | ${false} | ${''}
`( `(
'when status is $status and expirationPolicyStartedAt is $expirationPolicyStartedAt', 'when expirationPolicyCleanupStatus is $expirationPolicyCleanupStatus it is $shown that the component exists',
({ expirationPolicyStartedAt, status, shown, title }) => { ({ expirationPolicyCleanupStatus, shown }) => {
mountComponent({ item: { ...item, status, expirationPolicyStartedAt } }); mountComponent({ item: { ...item, expirationPolicyCleanupStatus } });
const icon = findWarningIcon(); expect(findCleanupStatus().exists()).toBe(shown);
expect(icon.exists()).toBe(shown);
if (shown) { if (shown) {
const tooltip = getBinding(icon.element, 'gl-tooltip'); expect(findCleanupStatus().props()).toMatchObject({
expect(tooltip.value.title).toBe(title); status: expirationPolicyCleanupStatus,
});
} }
}, },
); );
......
...@@ -9,6 +9,7 @@ export const imagesListResponse = [ ...@@ -9,6 +9,7 @@ export const imagesListResponse = [
canDelete: true, canDelete: true,
createdAt: '2020-11-03T13:29:21Z', createdAt: '2020-11-03T13:29:21Z',
expirationPolicyStartedAt: null, expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
}, },
{ {
__typename: 'ContainerRepository', __typename: 'ContainerRepository',
...@@ -20,6 +21,7 @@ export const imagesListResponse = [ ...@@ -20,6 +21,7 @@ export const imagesListResponse = [
canDelete: true, canDelete: true,
createdAt: '2020-09-21T06:57:43Z', createdAt: '2020-09-21T06:57:43Z',
expirationPolicyStartedAt: null, expirationPolicyStartedAt: null,
expirationPolicyCleanupStatus: 'UNSCHEDULED',
}, },
]; ];
......
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