Commit 60eb3ff9 authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch...

Merge branch '208269-container-registry-expiration-policy-notify-the-user-that-expiration-policy-is-enabled-and' into 'master'

Show that the container expiration policy is enabled

See merge request gitlab-org/gitlab!26152
parents b71d3493 19ac21c1
<script>
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { mapState } from 'vuex';
import { approximateDuration, calculateRemainingMilliseconds } from '~/lib/utils/datetime_utility';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
} from '../constants';
export default {
components: {
GlAlert,
GlSprintf,
GlLink,
},
computed: {
...mapState(['config', 'images', 'isLoading']),
isEmpty() {
return !this.images || this.images.length === 0;
},
showAlert() {
return this.config.expirationPolicy?.enabled;
},
timeTillRun() {
const difference = calculateRemainingMilliseconds(this.config.expirationPolicy?.next_run_at);
return approximateDuration(difference / 1000);
},
alertConfiguration() {
if (this.isEmpty || this.isLoading) {
return {
title: null,
primaryButton: null,
message: EXPIRATION_POLICY_ALERT_SHORT_MESSAGE,
};
}
return {
title: EXPIRATION_POLICY_ALERT_TITLE,
primaryButton: EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
message: EXPIRATION_POLICY_ALERT_FULL_MESSAGE,
};
},
},
};
</script>
<template>
<gl-alert
v-if="showAlert"
:dismissible="false"
:primary-button-text="alertConfiguration.primaryButton"
:primary-button-link="config.settingsPath"
:title="alertConfiguration.title"
class="my-2"
>
<gl-sprintf :message="alertConfiguration.message">
<template #days>
<strong>{{ timeTillRun }}</strong>
</template>
<template #link="{content}">
<gl-link :href="config.expirationPolicyHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>
import { __ } from '~/locale'; import { s__ } from '~/locale';
export const FETCH_IMAGES_LIST_ERROR_MESSAGE = __( export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
'Something went wrong while fetching the packages list.', 'ContainerRegistry|Something went wrong while fetching the packages list.',
); );
export const FETCH_TAGS_LIST_ERROR_MESSAGE = __( export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
'Something went wrong while fetching the tags list.', 'ContainerRegistry|Something went wrong while fetching the tags list.',
); );
export const DELETE_IMAGE_ERROR_MESSAGE = __('Something went wrong while deleting the image.'); export const DELETE_IMAGE_ERROR_MESSAGE = s__(
export const DELETE_IMAGE_SUCCESS_MESSAGE = __('Image deleted successfully'); 'ContainerRegistry|Something went wrong while deleting the image.',
export const DELETE_TAG_ERROR_MESSAGE = __('Something went wrong while deleting the tag.'); );
export const DELETE_TAG_SUCCESS_MESSAGE = __('Tag deleted successfully'); export const DELETE_IMAGE_SUCCESS_MESSAGE = s__('ContainerRegistry|Image deleted successfully');
export const DELETE_TAGS_ERROR_MESSAGE = __('Something went wrong while deleting the tags.'); export const DELETE_TAG_ERROR_MESSAGE = s__(
export const DELETE_TAGS_SUCCESS_MESSAGE = __('Tags deleted successfully'); 'ContainerRegistry|Something went wrong while deleting the tag.',
);
export const DELETE_TAG_SUCCESS_MESSAGE = s__('ContainerRegistry|Tag deleted successfully');
export const DELETE_TAGS_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while deleting the tags.',
);
export const DELETE_TAGS_SUCCESS_MESSAGE = s__('ContainerRegistry|Tags deleted successfully');
export const DEFAULT_PAGE = 1; export const DEFAULT_PAGE = 1;
export const DEFAULT_PAGE_SIZE = 10; export const DEFAULT_PAGE_SIZE = 10;
...@@ -26,7 +32,18 @@ export const LIST_KEY_LAST_UPDATED = 'created_at'; ...@@ -26,7 +32,18 @@ export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions'; export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox'; export const LIST_KEY_CHECKBOX = 'checkbox';
export const LIST_LABEL_TAG = __('Tag'); export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
export const LIST_LABEL_IMAGE_ID = __('Image ID'); export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
export const LIST_LABEL_SIZE = __('Size'); export const LIST_LABEL_SIZE = s__('ContainerRegistry|Size');
export const LIST_LABEL_LAST_UPDATED = __('Last Updated'); export const LIST_LABEL_LAST_UPDATED = s__('ContainerRegistry|Last Updated');
export const EXPIRATION_POLICY_ALERT_TITLE = s__(
'ContainerRegistry|Retention policy has been Enabled',
);
export const EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON = s__('ContainerRegistry|Edit Settings');
export const EXPIRATION_POLICY_ALERT_FULL_MESSAGE = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}',
);
export const EXPIRATION_POLICY_ALERT_SHORT_MESSAGE = s__(
'ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}',
);
...@@ -15,6 +15,7 @@ import Tracking from '~/tracking'; ...@@ -15,6 +15,7 @@ import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue'; import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue'; import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue'; import GroupEmptyState from '../components/group_empty_state.vue';
import ProjectPolicyAlert from '../components/project_policy_alert.vue';
export default { export default {
name: 'RegistryListApp', name: 'RegistryListApp',
...@@ -23,6 +24,7 @@ export default { ...@@ -23,6 +24,7 @@ export default {
GlPagination, GlPagination,
ProjectEmptyState, ProjectEmptyState,
GroupEmptyState, GroupEmptyState,
ProjectPolicyAlert,
ClipboardButton, ClipboardButton,
GlButton, GlButton,
GlIcon, GlIcon,
...@@ -84,6 +86,8 @@ export default { ...@@ -84,6 +86,8 @@ export default {
<template> <template>
<div class="w-100 slide-enter-from-element"> <div class="w-100 slide-enter-from-element">
<project-policy-alert v-if="!config.isGroupPage" />
<gl-empty-state <gl-empty-state
v-if="config.characterError" v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')" :title="s__('ContainerRegistry|Docker connection error')"
......
...@@ -5,6 +5,7 @@ export default { ...@@ -5,6 +5,7 @@ export default {
[types.SET_INITIAL_STATE](state, config) { [types.SET_INITIAL_STATE](state, config) {
state.config = { state.config = {
...config, ...config,
expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined,
isGroupPage: config.isGroupPage !== undefined, isGroupPage: config.isGroupPage !== undefined,
}; };
}, },
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
.col-12 .col-12
- if Feature.enabled?(:vue_container_registry_explorer, @project.group) - if Feature.enabled?(:vue_container_registry_explorer, @project.group)
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project), #js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
settings_path: project_settings_ci_cd_path(@project),
expiration_policy: @project.container_expiration_policy.to_json,
"help_page_path" => help_page_path('user/packages/container_registry/index'), "help_page_path" => help_page_path('user/packages/container_registry/index'),
"two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'), "two_factor_auth_help_link" => help_page_path('user/profile/account/two_factor_authentication'),
"personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'), "personal_access_tokens_help_link" => help_page_path('user/profile/personal_access_tokens'),
...@@ -13,6 +15,7 @@ ...@@ -13,6 +15,7 @@
"containers_error_image" => image_path('illustrations/docker-error-state.svg'), "containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url), "repository_url" => escape_once(@project.container_registry_url),
"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'),
character_error: @character_error.to_s } } character_error: @character_error.to_s } }
- else - else
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json), #js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
......
...@@ -5250,6 +5250,9 @@ msgstr "" ...@@ -5250,6 +5250,9 @@ msgstr ""
msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:" msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Edit Settings"
msgstr ""
msgid "ContainerRegistry|Expiration interval:" msgid "ContainerRegistry|Expiration interval:"
msgstr "" msgstr ""
...@@ -5268,6 +5271,9 @@ msgstr "" ...@@ -5268,6 +5271,9 @@ msgstr ""
msgid "ContainerRegistry|Image ID" msgid "ContainerRegistry|Image ID"
msgstr "" msgstr ""
msgid "ContainerRegistry|Image deleted successfully"
msgstr ""
msgid "ContainerRegistry|Keep and protect the images that matter most." msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr "" msgstr ""
...@@ -5294,27 +5300,57 @@ msgid_plural "ContainerRegistry|Remove tags" ...@@ -5294,27 +5300,57 @@ msgid_plural "ContainerRegistry|Remove tags"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "ContainerRegistry|Retention policy has been Enabled"
msgstr ""
msgid "ContainerRegistry|Size" msgid "ContainerRegistry|Size"
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while deleting the image."
msgstr ""
msgid "ContainerRegistry|Something went wrong while deleting the tag."
msgstr ""
msgid "ContainerRegistry|Something went wrong while deleting the tags."
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the expiration policy." msgid "ContainerRegistry|Something went wrong while fetching the expiration policy."
msgstr "" msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the packages list."
msgstr ""
msgid "ContainerRegistry|Something went wrong while fetching the tags list."
msgstr ""
msgid "ContainerRegistry|Something went wrong while updating the expiration policy." msgid "ContainerRegistry|Something went wrong while updating the expiration policy."
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag" msgid "ContainerRegistry|Tag"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag deleted successfully"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy" msgid "ContainerRegistry|Tag expiration policy"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:" msgid "ContainerRegistry|Tag expiration policy is designed to:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Tags deleted successfully"
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 ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled and will run in %{days}. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The retention and expiration policy for this Container Registry has been enabled. For more information visit the %{linkStart}documentation%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|The value of this input should be less than 255 characters" msgid "ContainerRegistry|The value of this input should be less than 255 characters"
msgstr "" msgstr ""
...@@ -10500,12 +10536,6 @@ msgstr "" ...@@ -10500,12 +10536,6 @@ msgstr ""
msgid "Image %{imageName} was scheduled for deletion from the registry." msgid "Image %{imageName} was scheduled for deletion from the registry."
msgstr "" msgstr ""
msgid "Image ID"
msgstr ""
msgid "Image deleted successfully"
msgstr ""
msgid "Image: %{image}" msgid "Image: %{image}"
msgstr "" msgstr ""
...@@ -11374,9 +11404,6 @@ msgstr "" ...@@ -11374,9 +11404,6 @@ msgstr ""
msgid "Last Seen" msgid "Last Seen"
msgstr "" msgstr ""
msgid "Last Updated"
msgstr ""
msgid "Last accessed on" msgid "Last accessed on"
msgstr "" msgstr ""
...@@ -18192,21 +18219,12 @@ msgstr "" ...@@ -18192,21 +18219,12 @@ msgstr ""
msgid "Something went wrong while deleting description changes. Please try again." msgid "Something went wrong while deleting description changes. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while deleting the image."
msgstr ""
msgid "Something went wrong while deleting the package." msgid "Something went wrong while deleting the package."
msgstr "" msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again." msgid "Something went wrong while deleting the source branch. Please try again."
msgstr "" msgstr ""
msgid "Something went wrong while deleting the tag."
msgstr ""
msgid "Something went wrong while deleting the tags."
msgstr ""
msgid "Something went wrong while deleting your note. Please try again." msgid "Something went wrong while deleting your note. Please try again."
msgstr "" msgstr ""
...@@ -18249,9 +18267,6 @@ msgstr "" ...@@ -18249,9 +18267,6 @@ msgstr ""
msgid "Something went wrong while fetching the registry list." msgid "Something went wrong while fetching the registry list."
msgstr "" msgstr ""
msgid "Something went wrong while fetching the tags list."
msgstr ""
msgid "Something went wrong while initializing the OpenAPI viewer" msgid "Something went wrong while initializing the OpenAPI viewer"
msgstr "" msgstr ""
...@@ -19074,9 +19089,6 @@ msgstr "" ...@@ -19074,9 +19089,6 @@ msgstr ""
msgid "Tag" msgid "Tag"
msgstr "" msgstr ""
msgid "Tag deleted successfully"
msgstr ""
msgid "Tag list:" msgid "Tag list:"
msgstr "" msgstr ""
...@@ -19095,9 +19107,6 @@ msgstr "" ...@@ -19095,9 +19107,6 @@ msgstr ""
msgid "Tags" msgid "Tags"
msgstr "" msgstr ""
msgid "Tags deleted successfully"
msgstr ""
msgid "Tags feed" msgid "Tags feed"
msgstr "" msgstr ""
......
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
import * as dateTimeUtils from '~/lib/utils/datetime_utility';
import component from '~/registry/explorer/components/project_policy_alert.vue';
import {
EXPIRATION_POLICY_ALERT_TITLE,
EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON,
} from '~/registry/explorer/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('Project Policy Alert', () => {
let wrapper;
let store;
const defaultState = {
config: {
expirationPolicy: {
enabled: true,
},
settingsPath: 'foo',
expirationPolicyHelpPagePath: 'bar',
},
images: [],
isLoading: false,
};
const findAlert = () => wrapper.find(GlAlert);
const findLink = () => wrapper.find(GlLink);
const createComponent = (state = defaultState) => {
store = new Vuex.Store({
state,
});
wrapper = shallowMount(component, {
localVue,
store,
stubs: {
GlSprintf,
},
});
};
const documentationExpectation = () => {
it('contain a documentation link', () => {
createComponent();
expect(findLink().attributes('href')).toBe(defaultState.config.expirationPolicyHelpPagePath);
expect(findLink().text()).toBe('documentation');
});
};
beforeEach(() => {
jest.spyOn(dateTimeUtils, 'approximateDuration').mockReturnValue('1 day');
});
afterEach(() => {
wrapper.destroy();
});
describe('is hidden', () => {
it('when expiration policy does not exist', () => {
createComponent({ config: {} });
expect(findAlert().exists()).toBe(false);
});
it('when expiration policy exist but is disabled', () => {
createComponent({
...defaultState,
config: {
expirationPolicy: {
enabled: false,
},
},
});
expect(findAlert().exists()).toBe(false);
});
});
describe('is visible', () => {
it('when expiration policy exists and is enabled', () => {
createComponent();
expect(findAlert().exists()).toBe(true);
});
});
describe('full info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [1] });
});
it('has a primary button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(EXPIRATION_POLICY_ALERT_PRIMARY_BUTTON);
expect(alert.props('primaryButtonLink')).toBe(defaultState.config.settingsPath);
});
it('has a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(EXPIRATION_POLICY_ALERT_TITLE);
});
it('has the full message', () => {
expect(findAlert().html()).toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
describe('compact info alert', () => {
beforeEach(() => {
createComponent({ ...defaultState, images: [] });
});
it('does not have a button', () => {
const alert = findAlert();
expect(alert.props('primaryButtonText')).toBe(null);
});
it('does not have a title', () => {
const alert = findAlert();
expect(alert.props('title')).toBe(null);
});
it('has the short message', () => {
expect(findAlert().html()).not.toContain('<strong>1 day</strong>');
});
documentationExpectation();
});
});
...@@ -10,9 +10,12 @@ describe('Mutations Registry Explorer Store', () => { ...@@ -10,9 +10,12 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const payload = { endpoint: 'foo', isGroupPage: true }; const payload = { endpoint: 'foo', isGroupPage: true, expirationPolicy: { foo: 'bar' } };
const expectedState = { ...mockState, config: payload }; const expectedState = { ...mockState, config: payload };
mutations[types.SET_INITIAL_STATE](mockState, payload); mutations[types.SET_INITIAL_STATE](mockState, {
...payload,
expirationPolicy: JSON.stringify(payload.expirationPolicy),
});
expect(mockState).toEqual(expectedState); expect(mockState).toEqual(expectedState);
}); });
......
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