Commit 8ffb109d authored by Nicolò Maria Mezzopera's avatar Nicolò Maria Mezzopera Committed by Mark Florian

Update registry settings store with new state

- state
- actions
- getters
- mutations
parent bff085a5
<script> <script>
import { mapActions, mapState } from 'vuex'; import { mapActions, mapGetters, mapState } from 'vuex';
import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import { s__ } from '~/locale'; import { s__ } from '~/locale';
...@@ -15,8 +15,15 @@ export default { ...@@ -15,8 +15,15 @@ export default {
GlLink, GlLink,
}, },
i18n: { i18n: {
unavailableFeatureText: s__( unavailableFeatureTitle: s__(
'ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}', `ContainerRegistry|Container Registry tag expiration and retention policy is disabled`,
),
unavailableFeatureIntroText: s__(
`ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled.`,
),
unavailableUserFeatureText: s__(`ContainerRegistry|Please contact your administrator.`),
unavailableAdminFeatureText: s__(
`ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature.`,
), ),
fetchSettingsErrorText: FETCH_SETTINGS_ERROR_MESSAGE, fetchSettingsErrorText: FETCH_SETTINGS_ERROR_MESSAGE,
}, },
...@@ -26,10 +33,19 @@ export default { ...@@ -26,10 +33,19 @@ export default {
}; };
}, },
computed: { computed: {
...mapState(['isDisabled']), ...mapState(['isAdmin', 'adminSettingsPath']),
...mapGetters({ isDisabled: 'getIsDisabled' }),
showSettingForm() { showSettingForm() {
return !this.isDisabled && !this.fetchSettingsError; return !this.isDisabled && !this.fetchSettingsError;
}, },
showDisabledFormMessage() {
return this.isDisabled && !this.fetchSettingsError;
},
unavailableFeatureMessage() {
return this.isAdmin
? this.$options.i18n.unavailableAdminFeatureText
: this.$options.i18n.unavailableUserFeatureText;
},
}, },
mounted() { mounted() {
this.fetchSettings().catch(() => { this.fetchSettings().catch(() => {
...@@ -59,16 +75,21 @@ export default { ...@@ -59,16 +75,21 @@ export default {
</ul> </ul>
<settings-form v-if="showSettingForm" /> <settings-form v-if="showSettingForm" />
<template v-else> <template v-else>
<gl-alert v-if="isDisabled" :dismissible="false"> <gl-alert
<p> v-if="showDisabledFormMessage"
<gl-sprintf :message="$options.i18n.unavailableFeatureText"> :dismissible="false"
<template #link="{content}"> :title="$options.i18n.unavailableFeatureTitle"
<gl-link href="https://gitlab.com/gitlab-org/gitlab/issues/196124" target="_blank"> variant="tip"
>
{{ $options.i18n.unavailableFeatureIntroText }}
<gl-sprintf :message="unavailableFeatureMessage">
<template #link="{ content }">
<gl-link :href="adminSettingsPath" target="_blank">
{{ content }} {{ content }}
</gl-link> </gl-link>
</template> </template>
</gl-sprintf> </gl-sprintf>
</p>
</gl-alert> </gl-alert>
<gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false"> <gl-alert v-else-if="fetchSettingsError" variant="warning" :dismissible="false">
<gl-sprintf :message="$options.i18n.fetchSettingsErrorText" /> <gl-sprintf :message="$options.i18n.fetchSettingsErrorText" />
......
...@@ -5,11 +5,7 @@ export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_ST ...@@ -5,11 +5,7 @@ export const setInitialState = ({ commit }, data) => commit(types.SET_INITIAL_ST
export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data); export const updateSettings = ({ commit }, data) => commit(types.UPDATE_SETTINGS, data);
export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING); export const toggleLoading = ({ commit }) => commit(types.TOGGLE_LOADING);
export const receiveSettingsSuccess = ({ commit }, data) => { export const receiveSettingsSuccess = ({ commit }, data) => {
if (data) {
commit(types.SET_SETTINGS, data); commit(types.SET_SETTINGS, data);
} else {
commit(types.SET_IS_DISABLED, true);
}
}; };
export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS); export const resetSettings = ({ commit }) => commit(types.RESET_SETTINGS);
......
...@@ -19,3 +19,7 @@ export const getSettings = (state, getters) => ({ ...@@ -19,3 +19,7 @@ export const getSettings = (state, getters) => ({
}); });
export const getIsEdited = state => !isEqual(state.original, state.settings); export const getIsEdited = state => !isEqual(state.original, state.settings);
export const getIsDisabled = state => {
return !(state.original || state.enableHistoricEntries);
};
...@@ -3,4 +3,3 @@ export const UPDATE_SETTINGS = 'UPDATE_SETTINGS'; ...@@ -3,4 +3,3 @@ export const UPDATE_SETTINGS = 'UPDATE_SETTINGS';
export const TOGGLE_LOADING = 'TOGGLE_LOADING'; export const TOGGLE_LOADING = 'TOGGLE_LOADING';
export const SET_SETTINGS = 'SET_SETTINGS'; export const SET_SETTINGS = 'SET_SETTINGS';
export const RESET_SETTINGS = 'RESET_SETTINGS'; export const RESET_SETTINGS = 'RESET_SETTINGS';
export const SET_IS_DISABLED = 'SET_IS_DISABLED';
import { parseBoolean } from '~/lib/utils/common_utils';
import * as types from './mutation_types'; import * as types from './mutation_types';
export default { export default {
...@@ -8,19 +9,19 @@ export default { ...@@ -8,19 +9,19 @@ export default {
keepN: JSON.parse(initialState.keepNOptions), keepN: JSON.parse(initialState.keepNOptions),
olderThan: JSON.parse(initialState.olderThanOptions), olderThan: JSON.parse(initialState.olderThanOptions),
}; };
state.enableHistoricEntries = parseBoolean(initialState.enableHistoricEntries);
state.isAdmin = parseBoolean(initialState.isAdmin);
state.adminSettingsPath = initialState.adminSettingsPath;
}, },
[types.UPDATE_SETTINGS](state, data) { [types.UPDATE_SETTINGS](state, data) {
state.settings = { ...state.settings, ...data.settings }; state.settings = { ...state.settings, ...data.settings };
}, },
[types.SET_SETTINGS](state, settings) { [types.SET_SETTINGS](state, settings) {
state.settings = settings; state.settings = settings ?? state.settings;
state.original = Object.freeze(settings); state.original = Object.freeze(settings);
}, },
[types.SET_IS_DISABLED](state, isDisabled) {
state.isDisabled = isDisabled;
},
[types.RESET_SETTINGS](state) { [types.RESET_SETTINGS](state) {
state.settings = { ...state.original }; state.settings = Object.assign({}, state.original);
}, },
[types.TOGGLE_LOADING](state) { [types.TOGGLE_LOADING](state) {
state.isLoading = !state.isLoading; state.isLoading = !state.isLoading;
......
...@@ -8,9 +8,17 @@ export default () => ({ ...@@ -8,9 +8,17 @@ export default () => ({
*/ */
isLoading: false, isLoading: false,
/* /*
* Boolean to determine if the user is allowed to interact with the form * Boolean to determine if the user is an admin
*/ */
isDisabled: false, isAdmin: false,
/*
* String containing the full path to the admin config page for CI/CD
*/
adminSettingsPath: '',
/*
* Boolean to determine if project created before 12.8 can use this feature
*/
enableHistoricEntries: false,
/* /*
* This contains the data shown and manipulated in the UI * This contains the data shown and manipulated in the UI
* Has the following structure: * Has the following structure:
...@@ -24,9 +32,9 @@ export default () => ({ ...@@ -24,9 +32,9 @@ export default () => ({
*/ */
settings: {}, settings: {},
/* /*
* Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel' * Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel', initialized to null
*/ */
original: {}, original: null,
/* /*
* Contains the options used to populate the form selects * Contains the options used to populate the form selects
*/ */
......
#js-registry-settings{ data: { project_id: @project.id, #js-registry-settings{ data: { project_id: @project.id,
cadence_options: cadence_options.to_json, cadence_options: cadence_options.to_json,
keep_n_options: keep_n_options.to_json, keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json} } older_than_options: older_than_options.to_json,
is_admin: current_user&.admin.to_s,
admin_settings_path: ci_cd_admin_application_settings_path(anchor: 'js-registry-settings'),
enable_historic_entries: Gitlab::CurrentSettings.try(:container_expiration_policies_enable_historic_entries).to_s} }
...@@ -5456,6 +5456,9 @@ msgstr "" ...@@ -5456,6 +5456,9 @@ msgstr ""
msgid "Container repositories sync capacity" msgid "Container repositories sync capacity"
msgstr "" msgstr ""
msgid "ContainerRegistry| Please visit the %{linkStart}administration settings%{linkEnd} to enable this feature."
msgstr ""
msgid "ContainerRegistry|%{imageName} tags" msgid "ContainerRegistry|%{imageName} tags"
msgstr "" msgstr ""
...@@ -5471,6 +5474,9 @@ msgstr "" ...@@ -5471,6 +5474,9 @@ msgstr ""
msgid "ContainerRegistry|Container Registry" msgid "ContainerRegistry|Container Registry"
msgstr "" msgstr ""
msgid "ContainerRegistry|Container Registry tag expiration and retention policy is disabled"
msgstr ""
msgid "ContainerRegistry|Copy build command" msgid "ContainerRegistry|Copy build command"
msgstr "" msgstr ""
...@@ -5480,9 +5486,6 @@ msgstr "" ...@@ -5480,9 +5486,6 @@ msgstr ""
msgid "ContainerRegistry|Copy push command" msgid "ContainerRegistry|Copy push command"
msgstr "" msgstr ""
msgid "ContainerRegistry|Currently, the Container Registry tag expiration feature is not available for projects created before GitLab version 12.8. For updates and more information, visit Issue %{linkStart}#196124%{linkEnd}"
msgstr ""
msgid "ContainerRegistry|Docker connection error" msgid "ContainerRegistry|Docker connection error"
msgstr "" msgstr ""
...@@ -5531,6 +5534,9 @@ msgstr "" ...@@ -5531,6 +5534,9 @@ msgstr ""
msgid "ContainerRegistry|Number of tags to retain:" msgid "ContainerRegistry|Number of tags to retain:"
msgstr "" msgstr ""
msgid "ContainerRegistry|Please contact your administrator."
msgstr ""
msgid "ContainerRegistry|Push an image" msgid "ContainerRegistry|Push an image"
msgstr "" msgstr ""
...@@ -5587,6 +5593,9 @@ msgstr "" ...@@ -5587,6 +5593,9 @@ msgstr ""
msgid "ContainerRegistry|Tags deleted successfully" msgid "ContainerRegistry|Tags deleted successfully"
msgstr "" msgstr ""
msgid "ContainerRegistry|The Container Registry tag expiration and retention policies for this project have not been enabled."
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 { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui'; import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
import component from '~/registry/settings/components/registry_settings_app.vue'; import component from '~/registry/settings/components/registry_settings_app.vue';
import SettingsForm from '~/registry/settings/components/settings_form.vue'; import SettingsForm from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/'; import { createStore } from '~/registry/settings/store/';
import { SET_IS_DISABLED } from '~/registry/settings/store/mutation_types'; import { SET_SETTINGS, SET_INITIAL_STATE } from '~/registry/settings/store/mutation_types';
import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/shared/constants'; import { FETCH_SETTINGS_ERROR_MESSAGE } from '~/registry/shared/constants';
import { stringifiedFormOptions } from '../../shared/mock_data';
describe('Registry Settings App', () => { describe('Registry Settings App', () => {
let wrapper; let wrapper;
...@@ -13,14 +14,14 @@ describe('Registry Settings App', () => { ...@@ -13,14 +14,14 @@ describe('Registry Settings App', () => {
const findSettingsComponent = () => wrapper.find(SettingsForm); const findSettingsComponent = () => wrapper.find(SettingsForm);
const findAlert = () => wrapper.find(GlAlert); const findAlert = () => wrapper.find(GlAlert);
const mountComponent = ({ dispatchMock = 'mockResolvedValue', isDisabled = false } = {}) => { const mountComponent = ({ dispatchMock = 'mockResolvedValue' } = {}) => {
store = createStore();
store.commit(SET_IS_DISABLED, isDisabled);
const dispatchSpy = jest.spyOn(store, 'dispatch'); const dispatchSpy = jest.spyOn(store, 'dispatch');
if (dispatchMock) {
dispatchSpy[dispatchMock](); dispatchSpy[dispatchMock]();
}
wrapper = shallowMount(component, { wrapper = shallowMount(component, {
stubs: {
GlSprintf,
},
mocks: { mocks: {
$toast: { $toast: {
show: jest.fn(), show: jest.fn(),
...@@ -30,11 +31,16 @@ describe('Registry Settings App', () => { ...@@ -30,11 +31,16 @@ describe('Registry Settings App', () => {
}); });
}; };
beforeEach(() => {
store = createStore();
});
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
it('renders', () => { it('renders', () => {
store.commit(SET_SETTINGS, { foo: 'bar' });
mountComponent(); mountComponent();
expect(wrapper.element).toMatchSnapshot(); expect(wrapper.element).toMatchSnapshot();
}); });
...@@ -45,13 +51,15 @@ describe('Registry Settings App', () => { ...@@ -45,13 +51,15 @@ describe('Registry Settings App', () => {
}); });
it('renders the setting form', () => { it('renders the setting form', () => {
store.commit(SET_SETTINGS, { foo: 'bar' });
mountComponent(); mountComponent();
expect(findSettingsComponent().exists()).toBe(true); expect(findSettingsComponent().exists()).toBe(true);
}); });
describe('isDisabled', () => { describe('the form is disabled', () => {
beforeEach(() => { beforeEach(() => {
mountComponent({ isDisabled: true }); store.commit(SET_SETTINGS, undefined);
mountComponent();
}); });
it('the form is hidden', () => { it('the form is hidden', () => {
...@@ -59,9 +67,27 @@ describe('Registry Settings App', () => { ...@@ -59,9 +67,27 @@ describe('Registry Settings App', () => {
}); });
it('shows an alert', () => { it('shows an alert', () => {
expect(findAlert().html()).toContain( const text = findAlert().text();
'Currently, the Container Registry tag expiration feature is not available', expect(text).toContain(
'The Container Registry tag expiration and retention policies for this project have not been enabled.',
); );
expect(text).toContain('Please contact your administrator.');
});
describe('an admin is visiting the page', () => {
beforeEach(() => {
store.commit(SET_INITIAL_STATE, {
...stringifiedFormOptions,
isAdmin: true,
adminSettingsPath: 'foo',
});
});
it('shows the admin part of the alert message', () => {
const sprintf = findAlert().find(GlSprintf);
expect(sprintf.text()).toBe('administration settings');
expect(sprintf.find(GlLink).attributes('href')).toBe('foo');
});
}); });
}); });
......
...@@ -20,7 +20,7 @@ describe('Actions Registry Store', () => { ...@@ -20,7 +20,7 @@ describe('Actions Registry Store', () => {
); );
describe('receiveSettingsSuccess', () => { describe('receiveSettingsSuccess', () => {
it('calls SET_SETTINGS when data is present', () => { it('calls SET_SETTINGS', () => {
testAction( testAction(
actions.receiveSettingsSuccess, actions.receiveSettingsSuccess,
'foo', 'foo',
...@@ -29,15 +29,6 @@ describe('Actions Registry Store', () => { ...@@ -29,15 +29,6 @@ describe('Actions Registry Store', () => {
[], [],
); );
}); });
it('calls SET_IS_DISABLED when data is not present', () => {
testAction(
actions.receiveSettingsSuccess,
null,
{},
[{ type: types.SET_IS_DISABLED, payload: true }],
[],
);
});
}); });
describe('fetchSettings', () => { describe('fetchSettings', () => {
......
...@@ -29,7 +29,7 @@ describe('Getters registry settings store', () => { ...@@ -29,7 +29,7 @@ describe('Getters registry settings store', () => {
}); });
}); });
describe('getIsDisabled', () => { describe('getIsEdited', () => {
it('returns false when original is equal to settings', () => { it('returns false when original is equal to settings', () => {
const same = { foo: 'bar' }; const same = { foo: 'bar' };
expect(getters.getIsEdited({ original: same, settings: same })).toBe(false); expect(getters.getIsEdited({ original: same, settings: same })).toBe(false);
...@@ -41,4 +41,18 @@ describe('Getters registry settings store', () => { ...@@ -41,4 +41,18 @@ describe('Getters registry settings store', () => {
); );
}); });
}); });
describe('getIsDisabled', () => {
it.each`
original | enableHistoricEntries | result
${undefined} | ${false} | ${true}
${{ foo: 'bar' }} | ${undefined} | ${false}
${{}} | ${false} | ${false}
`(
'returns $result when original is $original and enableHistoricEntries is $enableHistoricEntries',
({ original, enableHistoricEntries, result }) => {
expect(getters.getIsDisabled({ original, enableHistoricEntries })).toBe(result);
},
);
});
}); });
...@@ -12,14 +12,19 @@ describe('Mutations Registry Store', () => { ...@@ -12,14 +12,19 @@ describe('Mutations Registry Store', () => {
describe('SET_INITIAL_STATE', () => { describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => { it('should set the initial state', () => {
const expectedState = { ...mockState, projectId: 'foo', formOptions }; const payload = {
mutations[types.SET_INITIAL_STATE](mockState, {
projectId: 'foo', projectId: 'foo',
enableHistoricEntries: false,
adminSettingsPath: 'foo',
isAdmin: true,
};
const expectedState = { ...mockState, ...payload, formOptions };
mutations[types.SET_INITIAL_STATE](mockState, {
...payload,
...stringifiedFormOptions, ...stringifiedFormOptions,
}); });
expect(mockState.projectId).toEqual(expectedState.projectId); expect(mockState).toEqual(expectedState);
expect(mockState.formOptions).toEqual(expectedState.formOptions);
}); });
}); });
...@@ -41,6 +46,13 @@ describe('Mutations Registry Store', () => { ...@@ -41,6 +46,13 @@ describe('Mutations Registry Store', () => {
expect(mockState.settings).toEqual(expectedState.settings); expect(mockState.settings).toEqual(expectedState.settings);
expect(mockState.original).toEqual(expectedState.settings); expect(mockState.original).toEqual(expectedState.settings);
}); });
it('should keep the default state when settings is not present', () => {
const originalSettings = { ...mockState.settings };
mutations[types.SET_SETTINGS](mockState);
expect(mockState.settings).toEqual(originalSettings);
expect(mockState.original).toEqual(undefined);
});
}); });
describe('RESET_SETTINGS', () => { describe('RESET_SETTINGS', () => {
...@@ -50,6 +62,13 @@ describe('Mutations Registry Store', () => { ...@@ -50,6 +62,13 @@ describe('Mutations Registry Store', () => {
mutations[types.RESET_SETTINGS](mockState); mutations[types.RESET_SETTINGS](mockState);
expect(mockState.settings).toEqual(mockState.original); expect(mockState.settings).toEqual(mockState.original);
}); });
it('if original is undefined it should initialize to empty object', () => {
mockState.settings = { foo: 'bar' };
mockState.original = undefined;
mutations[types.RESET_SETTINGS](mockState);
expect(mockState.settings).toEqual({});
});
}); });
describe('TOGGLE_LOADING', () => { describe('TOGGLE_LOADING', () => {
...@@ -58,11 +77,4 @@ describe('Mutations Registry Store', () => { ...@@ -58,11 +77,4 @@ describe('Mutations Registry Store', () => {
expect(mockState.isLoading).toEqual(true); expect(mockState.isLoading).toEqual(true);
}); });
}); });
describe('SET_IS_DISABLED', () => {
it('should set isDisabled', () => {
mutations[types.SET_IS_DISABLED](mockState, true);
expect(mockState.isDisabled).toEqual(true);
});
});
}); });
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