Commit 19ac21c1 authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Natalia Tepluhina

Create new alert component

- expiration policy alert
- wire it in the index page
parent bae06faf
<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 = __(
'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}',
);
......@@ -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 {
<template>
<div class="w-100 slide-enter-from-element">
<project-policy-alert v-if="!config.isGroupPage" />
<gl-empty-state
v-if="config.characterError"
:title="s__('ContainerRegistry|Docker connection error')"
......
......@@ -5,6 +5,7 @@ export default {
[types.SET_INITIAL_STATE](state, config) {
state.config = {
...config,
expirationPolicy: config.expirationPolicy ? JSON.parse(config.expirationPolicy) : undefined,
isGroupPage: config.isGroupPage !== undefined,
};
},
......
......@@ -6,6 +6,8 @@
.col-12
- if Feature.enabled?(:vue_container_registry_explorer, @project.group)
#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'),
"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),
......
......@@ -5247,6 +5247,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 ""
......@@ -5265,6 +5268,9 @@ msgstr ""
msgid "ContainerRegistry|Image ID"
msgstr ""
msgid "ContainerRegistry|Image deleted successfully"
msgstr ""
msgid "ContainerRegistry|Keep and protect the images that matter most."
msgstr ""
......@@ -5291,27 +5297,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 ""
......@@ -10497,12 +10533,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 ""
......@@ -11371,9 +11401,6 @@ msgstr ""
msgid "Last Seen"
msgstr ""
msgid "Last Updated"
msgstr ""
msgid "Last accessed on"
msgstr ""
......@@ -18189,21 +18216,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 ""
......@@ -18246,9 +18264,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 ""
......
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', () => {
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);
});
......
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