Commit 4a30420b authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Illya Klymov

Add new partial_cleanup alert component

- component
- tests
parent 8232e0bf
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '../../constants/index';
export default {
components: {
GlSprintf,
GlAlert,
GlLink,
},
props: {
runCleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
cleanupPoliciesHelpPagePath: { type: String, required: false, default: '' },
},
i18n: {
DELETE_ALERT_TITLE,
DELETE_ALERT_LINK_TEXT,
},
};
</script>
<template>
<gl-alert variant="warning" :title="$options.i18n.DELETE_ALERT_TITLE" @dismiss="$emit('dismiss')">
<gl-sprintf :message="$options.i18n.DELETE_ALERT_LINK_TEXT">
<template #adminLink="{content}">
<gl-link data-testid="run-link" :href="runCleanupPoliciesHelpPagePath" target="_blank">{{
content
}}</gl-link>
</template>
<template #docLink="{content}">
<gl-link data-testid="help-link" :href="cleanupPoliciesHelpPagePath" target="_blank">{{
content
}}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
...@@ -42,6 +42,7 @@ export default { ...@@ -42,6 +42,7 @@ export default {
name: this.item.path, name: this.item.path,
tags_path: this.item.tags_path, tags_path: this.item.tags_path,
id: this.item.id, id: this.item.id,
cleanup_policy_started_at: this.item.cleanup_policy_started_at,
}); });
return window.btoa(params); return window.btoa(params);
}, },
......
...@@ -9,3 +9,7 @@ export const EXPIRATION_POLICY_DISABLED_TEXT = s__( ...@@ -9,3 +9,7 @@ export const EXPIRATION_POLICY_DISABLED_TEXT = s__(
export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__( export const EXPIRATION_POLICY_DISABLED_MESSAGE = s__(
'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}', 'ContainerRegistry|Expiration policies help manage the storage space used by the Container Registry, but the expiration policies for this registry are disabled. Contact your administrator to enable. %{docLinkStart}More information%{docLinkEnd}',
); );
export const DELETE_ALERT_TITLE = s__('ContainerRegistry|Some tags were not deleted');
export const DELETE_ALERT_LINK_TEXT = s__(
'ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}',
);
...@@ -4,6 +4,7 @@ import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui'; ...@@ -4,6 +4,7 @@ import { GlPagination, GlResizeObserverDirective } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils'; import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import DeleteAlert from '../components/details_page/delete_alert.vue'; import DeleteAlert from '../components/details_page/delete_alert.vue';
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue'; import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue'; import DetailsHeader from '../components/details_page/details_header.vue';
import TagsList from '../components/details_page/tags_list.vue'; import TagsList from '../components/details_page/tags_list.vue';
...@@ -21,6 +22,7 @@ import { ...@@ -21,6 +22,7 @@ import {
export default { export default {
components: { components: {
DeleteAlert, DeleteAlert,
PartialCleanupAlert,
DetailsHeader, DetailsHeader,
GlPagination, GlPagination,
DeleteModal, DeleteModal,
...@@ -37,13 +39,16 @@ export default { ...@@ -37,13 +39,16 @@ export default {
itemsToBeDeleted: [], itemsToBeDeleted: [],
isDesktop: true, isDesktop: true,
deleteAlertType: null, deleteAlertType: null,
dismissPartialCleanupWarning: false,
}; };
}, },
computed: { computed: {
...mapState(['tagsPagination', 'isLoading', 'config', 'tags']), ...mapState(['tagsPagination', 'isLoading', 'config', 'tags']),
imageName() { queryParameters() {
const { name } = decodeAndParse(this.$route.params.id); return decodeAndParse(this.$route.params.id);
return name; },
showPartialCleanupWarning() {
return this.queryParameters.cleanup_policy_started_at && !this.dismissPartialCleanupWarning;
}, },
tracking() { tracking() {
return { return {
...@@ -120,7 +125,14 @@ export default { ...@@ -120,7 +125,14 @@ export default {
class="gl-my-2" class="gl-my-2"
/> />
<details-header :image-name="imageName" /> <partial-cleanup-alert
v-if="showPartialCleanupWarning"
:run-cleanup-policies-help-page-path="config.runCleanupPoliciesHelpPagePath"
:cleanup-policies-help-page-path="config.cleanupPoliciesHelpPagePath"
@dismiss="dismissPartialCleanupWarning = true"
/>
<details-header :image-name="queryParameters.name" />
<tags-loader v-if="isLoading" /> <tags-loader v-if="isLoading" />
<template v-else> <template v-else>
......
...@@ -4,6 +4,7 @@ class ContainerRepositoryEntity < Grape::Entity ...@@ -4,6 +4,7 @@ class ContainerRepositoryEntity < Grape::Entity
include RequestAwareEntity include RequestAwareEntity
expose :id, :name, :path, :location, :created_at, :status, :tags_count expose :id, :name, :path, :location, :created_at, :status, :tags_count
expose :expiration_policy_started_at, as: :cleanup_policy_started_at
expose :tags_path do |repository| expose :tags_path do |repository|
project_registry_repository_tags_path(project, repository, format: :json) project_registry_repository_tags_path(project, repository, format: :json)
......
...@@ -12,6 +12,8 @@ ...@@ -12,6 +12,8 @@
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
is_group_page: "true", is_group_page: "true",
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
...@@ -15,5 +15,8 @@ ...@@ -15,5 +15,8 @@
"registry_host_url_with_port" => escape_once(registry_config.host_port), "registry_host_url_with_port" => escape_once(registry_config.host_port),
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'), "expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'expiration-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'), "garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
"cleanup_policies_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'how-the-cleanup-policy-works'),
"is_admin": current_user&.admin.to_s, "is_admin": current_user&.admin.to_s,
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
---
title: Display alert for partially executed cleanup policies
merge_request: 43831
author:
type: changed
...@@ -6950,6 +6950,9 @@ msgstr[1] "" ...@@ -6950,6 +6950,9 @@ msgstr[1] ""
msgid "ContainerRegistry|Set cleanup policy" msgid "ContainerRegistry|Set cleanup policy"
msgstr "" msgstr ""
msgid "ContainerRegistry|Some tags were not deleted"
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy." msgid "ContainerRegistry|Something went wrong while fetching the cleanup policy."
msgstr "" msgstr ""
...@@ -6989,6 +6992,9 @@ msgstr "" ...@@ -6989,6 +6992,9 @@ msgstr ""
msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}" msgid "ContainerRegistry|Tags with names matching this regex pattern will %{italicStart}expire:%{italicEnd}"
msgstr "" msgstr ""
msgid "ContainerRegistry|The cleanup policy timed out before it could delete all tags. An administrator can %{adminLinkStart}manually run cleanup now%{adminLinkEnd} or you can wait for the cleanup policy to automatically run again. %{docLinkStart}More information%{docLinkEnd}"
msgstr ""
msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator." msgid "ContainerRegistry|The last tag related to this image was recently removed. This empty image and any associated data will be automatically removed as part of the regular Garbage Collection process. If you have any questions, contact your administrator."
msgstr "" msgstr ""
......
import { shallowMount } from '@vue/test-utils';
import { GlAlert, GlSprintf } from '@gitlab/ui';
import component from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import { DELETE_ALERT_TITLE, DELETE_ALERT_LINK_TEXT } from '~/registry/explorer/constants';
describe('Partial Cleanup alert', () => {
let wrapper;
const findAlert = () => wrapper.find(GlAlert);
const findRunLink = () => wrapper.find('[data-testid="run-link"');
const findHelpLink = () => wrapper.find('[data-testid="help-link"');
const mountComponent = () => {
wrapper = shallowMount(component, {
stubs: { GlSprintf },
propsData: {
runCleanupPoliciesHelpPagePath: 'foo',
cleanupPoliciesHelpPagePath: 'bar',
},
});
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it(`gl-alert has the correct properties`, () => {
mountComponent();
expect(findAlert().props()).toMatchObject({
title: DELETE_ALERT_TITLE,
variant: 'warning',
});
});
it('has the right text', () => {
mountComponent();
expect(wrapper.text()).toMatchInterpolatedText(DELETE_ALERT_LINK_TEXT);
});
it('contains run link', () => {
mountComponent();
const link = findRunLink();
expect(link.exists()).toBe(true);
expect(link.attributes()).toMatchObject({
href: 'foo',
target: '_blank',
});
});
it('contains help link', () => {
mountComponent();
const link = findHelpLink();
expect(link.exists()).toBe(true);
expect(link.attributes()).toMatchObject({
href: 'bar',
target: '_blank',
});
});
it('GlAlert dismiss event triggers a dismiss event', () => {
mountComponent();
findAlert().vm.$emit('dismiss');
expect(wrapper.emitted('dismiss')).toEqual([[]]);
});
});
...@@ -3,6 +3,7 @@ import { GlPagination } from '@gitlab/ui'; ...@@ -3,6 +3,7 @@ import { GlPagination } from '@gitlab/ui';
import Tracking from '~/tracking'; import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/details.vue'; import component from '~/registry/explorer/pages/details.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue'; import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue'; import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue'; import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue'; import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
...@@ -30,8 +31,10 @@ describe('Details Page', () => { ...@@ -30,8 +31,10 @@ describe('Details Page', () => {
const findDeleteAlert = () => wrapper.find(DeleteAlert); const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader); const findDetailsHeader = () => wrapper.find(DetailsHeader);
const findEmptyTagsState = () => wrapper.find(EmptyTagsState); const findEmptyTagsState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
const routeId = window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar' })); const routeIdGenerator = override =>
window.btoa(JSON.stringify({ name: 'foo', tags_path: 'bar', ...override }));
const tagsArrayToSelectedTags = tags => const tagsArrayToSelectedTags = tags =>
tags.reduce((acc, c) => { tags.reduce((acc, c) => {
...@@ -39,7 +42,7 @@ describe('Details Page', () => { ...@@ -39,7 +42,7 @@ describe('Details Page', () => {
return acc; return acc;
}, {}); }, {});
const mountComponent = options => { const mountComponent = ({ options, routeParams } = {}) => {
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
store, store,
stubs: { stubs: {
...@@ -48,7 +51,7 @@ describe('Details Page', () => { ...@@ -48,7 +51,7 @@ describe('Details Page', () => {
mocks: { mocks: {
$route: { $route: {
params: { params: {
id: routeId, id: routeIdGenerator(routeParams),
}, },
}, },
}, },
...@@ -224,7 +227,7 @@ describe('Details Page', () => { ...@@ -224,7 +227,7 @@ describe('Details Page', () => {
findDeleteModal().vm.$emit('confirmDelete'); findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', { expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTag', {
tag: store.state.tags[0], tag: store.state.tags[0],
params: routeId, params: routeIdGenerator(),
}); });
}); });
}); });
...@@ -239,7 +242,7 @@ describe('Details Page', () => { ...@@ -239,7 +242,7 @@ describe('Details Page', () => {
findDeleteModal().vm.$emit('confirmDelete'); findDeleteModal().vm.$emit('confirmDelete');
expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', { expect(dispatchSpy).toHaveBeenCalledWith('requestDeleteTags', {
ids: store.state.tags.map(t => t.name), ids: store.state.tags.map(t => t.name),
params: routeId, params: routeIdGenerator(),
}); });
}); });
}); });
...@@ -273,11 +276,57 @@ describe('Details Page', () => { ...@@ -273,11 +276,57 @@ describe('Details Page', () => {
it('has the correct props', () => { it('has the correct props', () => {
store.commit(SET_INITIAL_STATE, { ...config }); store.commit(SET_INITIAL_STATE, { ...config });
mountComponent({ mountComponent({
data: () => ({ options: {
deleteAlertType, data: () => ({
}), deleteAlertType,
}),
},
}); });
expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType }); expect(findDeleteAlert().props()).toEqual({ ...config, deleteAlertType });
}); });
}); });
describe('Partial Cleanup Alert', () => {
const config = {
runCleanupPoliciesHelpPagePath: 'foo',
cleanupPoliciesHelpPagePath: 'bar',
};
describe('when expiration_policy_started is not null', () => {
const routeParams = { cleanup_policy_started_at: Date.now().toString() };
it('exists', () => {
mountComponent({ routeParams });
expect(findPartialCleanupAlert().exists()).toBe(true);
});
it('has the correct props', () => {
store.commit(SET_INITIAL_STATE, { ...config });
mountComponent({ routeParams });
expect(findPartialCleanupAlert().props()).toEqual({ ...config });
});
it('dismiss hides the component', async () => {
mountComponent({ routeParams });
expect(findPartialCleanupAlert().exists()).toBe(true);
findPartialCleanupAlert().vm.$emit('dismiss');
await wrapper.vm.$nextTick();
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
describe('when expiration_policy_started is null', () => {
it('the component is hidden', () => {
mountComponent();
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
});
}); });
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