diff --git a/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue b/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue
new file mode 100644
index 0000000000000000000000000000000000000000..6acf366e5315d7a15f2325b379a004dcdba5817d
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/project_policy_alert.vue
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+ {{ timeTillRun }}
+
+
+
+ {{ content }}
+
+
+
+
+
diff --git a/app/assets/javascripts/registry/explorer/constants.js b/app/assets/javascripts/registry/explorer/constants.js
index bb31115762742ce80794549b366c7068e5dc1269..ef72c085972c5b40a84ffb04d01fe99c360802aa 100644
--- a/app/assets/javascripts/registry/explorer/constants.js
+++ b/app/assets/javascripts/registry/explorer/constants.js
@@ -1,18 +1,24 @@
-import { __ } from '~/locale';
+import { s__ } from '~/locale';
-export const FETCH_IMAGES_LIST_ERROR_MESSAGE = __(
- 'Something went wrong while fetching the packages list.',
+export const FETCH_IMAGES_LIST_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while fetching the packages list.',
);
-export const FETCH_TAGS_LIST_ERROR_MESSAGE = __(
- 'Something went wrong while fetching the tags list.',
+export const FETCH_TAGS_LIST_ERROR_MESSAGE = s__(
+ '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_SUCCESS_MESSAGE = __('Image deleted successfully');
-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_TAGS_ERROR_MESSAGE = __('Something went wrong while deleting the tags.');
-export const DELETE_TAGS_SUCCESS_MESSAGE = __('Tags deleted successfully');
+export const DELETE_IMAGE_ERROR_MESSAGE = s__(
+ 'ContainerRegistry|Something went wrong while deleting the image.',
+);
+export const DELETE_IMAGE_SUCCESS_MESSAGE = s__('ContainerRegistry|Image deleted successfully');
+export const DELETE_TAG_ERROR_MESSAGE = s__(
+ '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_SIZE = 10;
@@ -26,7 +32,18 @@ export const LIST_KEY_LAST_UPDATED = 'created_at';
export const LIST_KEY_ACTIONS = 'actions';
export const LIST_KEY_CHECKBOX = 'checkbox';
-export const LIST_LABEL_TAG = __('Tag');
-export const LIST_LABEL_IMAGE_ID = __('Image ID');
-export const LIST_LABEL_SIZE = __('Size');
-export const LIST_LABEL_LAST_UPDATED = __('Last Updated');
+export const LIST_LABEL_TAG = s__('ContainerRegistry|Tag');
+export const LIST_LABEL_IMAGE_ID = s__('ContainerRegistry|Image ID');
+export const LIST_LABEL_SIZE = s__('ContainerRegistry|Size');
+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}',
+);
diff --git a/app/assets/javascripts/registry/explorer/pages/list.vue b/app/assets/javascripts/registry/explorer/pages/list.vue
index 4e9f0a8350166c90844dd7e28ee8600814395866..c6ba06cd68c45b97cc0dfd6881cdc53d55dac436 100644
--- a/app/assets/javascripts/registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/registry/explorer/pages/list.vue
@@ -15,6 +15,7 @@ import Tracking from '~/tracking';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import ProjectEmptyState from '../components/project_empty_state.vue';
import GroupEmptyState from '../components/group_empty_state.vue';
+import ProjectPolicyAlert from '../components/project_policy_alert.vue';
export default {
name: 'RegistryListApp',
@@ -23,6 +24,7 @@ export default {
GlPagination,
ProjectEmptyState,
GroupEmptyState,
+ ProjectPolicyAlert,
ClipboardButton,
GlButton,
GlIcon,
@@ -84,6 +86,8 @@ export default {
+
+
help_page_path('user/packages/container_registry/index'),
"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'),
@@ -13,6 +15,7 @@
"containers_error_image" => image_path('illustrations/docker-error-state.svg'),
"repository_url" => escape_once(@project.container_registry_url),
"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 } }
- else
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json),
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 18cfcea9335e80ab3bebfb8ac5f66cf76f255c2c..4d917cbe94332572c5b8d96a9d23ef41df99e1e9 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5250,6 +5250,9 @@ msgstr ""
msgid "ContainerRegistry|Docker tags with names matching this regex pattern will expire:"
msgstr ""
+msgid "ContainerRegistry|Edit Settings"
+msgstr ""
+
msgid "ContainerRegistry|Expiration interval:"
msgstr ""
@@ -5268,6 +5271,9 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
+msgid "ContainerRegistry|Image deleted successfully"
+msgstr ""
+
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
@@ -5294,27 +5300,57 @@ msgid_plural "ContainerRegistry|Remove tags"
msgstr[0] ""
msgstr[1] ""
+msgid "ContainerRegistry|Retention policy has been Enabled"
+msgstr ""
+
msgid "ContainerRegistry|Size"
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."
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."
msgstr ""
msgid "ContainerRegistry|Tag"
msgstr ""
+msgid "ContainerRegistry|Tag deleted successfully"
+msgstr ""
+
msgid "ContainerRegistry|Tag expiration policy"
msgstr ""
msgid "ContainerRegistry|Tag expiration policy is designed to:"
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."
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"
msgstr ""
@@ -10500,12 +10536,6 @@ msgstr ""
msgid "Image %{imageName} was scheduled for deletion from the registry."
msgstr ""
-msgid "Image ID"
-msgstr ""
-
-msgid "Image deleted successfully"
-msgstr ""
-
msgid "Image: %{image}"
msgstr ""
@@ -11374,9 +11404,6 @@ msgstr ""
msgid "Last Seen"
msgstr ""
-msgid "Last Updated"
-msgstr ""
-
msgid "Last accessed on"
msgstr ""
@@ -18192,21 +18219,12 @@ msgstr ""
msgid "Something went wrong while deleting description changes. Please try again."
msgstr ""
-msgid "Something went wrong while deleting the image."
-msgstr ""
-
msgid "Something went wrong while deleting the package."
msgstr ""
msgid "Something went wrong while deleting the source branch. Please try again."
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."
msgstr ""
@@ -18249,9 +18267,6 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong while fetching the tags list."
-msgstr ""
-
msgid "Something went wrong while initializing the OpenAPI viewer"
msgstr ""
@@ -19074,9 +19089,6 @@ msgstr ""
msgid "Tag"
msgstr ""
-msgid "Tag deleted successfully"
-msgstr ""
-
msgid "Tag list:"
msgstr ""
@@ -19095,9 +19107,6 @@ msgstr ""
msgid "Tags"
msgstr ""
-msgid "Tags deleted successfully"
-msgstr ""
-
msgid "Tags feed"
msgstr ""
diff --git a/spec/frontend/registry/explorer/components/project_policy_alert_spec.js b/spec/frontend/registry/explorer/components/project_policy_alert_spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..89c37e55398a8c671b45281b73c01c0b05a3ee3d
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/project_policy_alert_spec.js
@@ -0,0 +1,132 @@
+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('1 day');
+ });
+
+ 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('1 day');
+ });
+
+ documentationExpectation();
+ });
+});
diff --git a/spec/frontend/registry/explorer/stores/mutations_spec.js b/spec/frontend/registry/explorer/stores/mutations_spec.js
index 43f6a95db10ee00462393f8810be2d546384f394..1d5055c02d2ccd7f2daa165976d8dfa30174b346 100644
--- a/spec/frontend/registry/explorer/stores/mutations_spec.js
+++ b/spec/frontend/registry/explorer/stores/mutations_spec.js
@@ -10,9 +10,12 @@ describe('Mutations Registry Explorer Store', () => {
describe('SET_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 };
- mutations[types.SET_INITIAL_STATE](mockState, payload);
+ mutations[types.SET_INITIAL_STATE](mockState, {
+ ...payload,
+ expirationPolicy: JSON.stringify(payload.expirationPolicy),
+ });
expect(mockState).toEqual(expectedState);
});