Commit 20d0479c authored by Natalia Tepluhina's avatar Natalia Tepluhina

Merge branch '15398-connect-backend-helpers' into 'master'

Connect options helper in container expiration settings

See merge request gitlab-org/gitlab!22112
parents ac4ad52f 71e2f36d
<script>
import { mapActions } from 'vuex';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { GlFormGroup, GlToggle, GlFormSelect, GlFormTextarea, GlButton, GlCard } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import { NAME_REGEX_LENGTH } from '../constants';
import { mapComputed } from '~/vuex_shared/bindings';
......@@ -12,19 +12,25 @@ export default {
GlFormSelect,
GlFormTextarea,
GlButton,
GlCard,
},
labelsConfig: {
cols: 3,
align: 'right',
},
computed: {
...mapComputed('settings', 'updateSettings', [
'enabled',
'cadence',
'older_than',
'keep_n',
'name_regex',
]),
...mapState(['formOptions']),
...mapComputed(
[
'enabled',
{ key: 'cadence', getter: 'getCadence' },
{ key: 'older_than', getter: 'getOlderThan' },
{ key: 'keep_n', getter: 'getKeepN' },
'name_regex',
],
'updateSettings',
'settings',
),
policyEnabledText() {
return this.enabled ? __('enabled') : __('disabled');
},
......@@ -66,12 +72,12 @@ export default {
</script>
<template>
<div class="card">
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
<div class="card-header">
<form ref="form-element" @submit.prevent="saveSettings" @reset.prevent="resetSettings">
<gl-card>
<template #header>
{{ s__('ContainerRegistry|Tag expiration policy') }}
</div>
<div class="card-body">
</template>
<template>
<gl-form-group
id="expiration-policy-toggle-group"
:label-cols="$options.labelsConfig.cols"
......@@ -92,9 +98,10 @@ export default {
label-for="expiration-policy-interval"
:label="s__('ContainerRegistry|Expiration interval:')"
>
<gl-form-select id="expiration-policy-interval" v-model="older_than">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
<gl-form-select id="expiration-policy-interval" v-model="older_than" :disabled="!enabled">
<option v-for="option in formOptions.olderThan" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
......@@ -105,9 +112,10 @@ export default {
label-for="expiration-policy-schedule"
:label="s__('ContainerRegistry|Expiration schedule:')"
>
<gl-form-select id="expiration-policy-schedule" v-model="cadence">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
<gl-form-select id="expiration-policy-schedule" v-model="cadence" :disabled="!enabled">
<option v-for="option in formOptions.cadence" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
......@@ -118,9 +126,10 @@ export default {
label-for="expiration-policy-latest"
:label="s__('ContainerRegistry|Expiration latest:')"
>
<gl-form-select id="expiration-policy-latest" v-model="keep_n">
<option value="1">{{ __('Option 1') }}</option>
<option value="2">{{ __('Option 2') }}</option>
<gl-form-select id="expiration-policy-latest" v-model="keep_n" :disabled="!enabled">
<option v-for="option in formOptions.keepN" :key="option.key" :value="option.key">
{{ option.label }}
</option>
</gl-form-select>
</gl-form-group>
......@@ -140,19 +149,30 @@ export default {
v-model="name_regex"
:placeholder="nameRegexPlaceholder"
:state="nameRegexState"
:disabled="!enabled"
trim
/>
<template #description>
<span ref="regex-description" v-html="regexHelpText"></span>
</template>
</gl-form-group>
</div>
<div class="card-footer text-right">
<gl-button ref="cancel-button" type="reset">{{ __('Cancel') }}</gl-button>
<gl-button ref="save-button" type="submit" :disabled="formIsValid" variant="success">
{{ __('Save Expiration Policy') }}
</gl-button>
</div>
</form>
</div>
</template>
<template #footer>
<div class="d-flex justify-content-end">
<gl-button ref="cancel-button" type="reset" class="mr-2 d-block">{{
__('Cancel')
}}</gl-button>
<gl-button
ref="save-button"
type="submit"
:disabled="formIsValid"
variant="success"
class="d-block"
>
{{ __('Save expiration policy') }}
</gl-button>
</div>
</template>
</gl-card>
</form>
</template>
import { findDefaultOption } from '../utils';
export const getCadence = state =>
state.settings.cadence || findDefaultOption(state.formOptions.cadence);
export const getKeepN = state =>
state.settings.keep_n || findDefaultOption(state.formOptions.keepN);
export const getOlderThan = state =>
state.settings.older_than || findDefaultOption(state.formOptions.olderThan);
......@@ -2,6 +2,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import * as getters from './getters';
import state from './state';
Vue.use(Vuex);
......@@ -11,6 +12,7 @@ export const createStore = () =>
state,
actions,
mutations,
getters,
});
export default createStore();
......@@ -3,6 +3,11 @@ import * as types from './mutation_types';
export default {
[types.SET_INITIAL_STATE](state, initialState) {
state.projectId = initialState.projectId;
state.formOptions = {
cadence: JSON.parse(initialState.cadenceOptions),
keepN: JSON.parse(initialState.keepNOptions),
olderThan: JSON.parse(initialState.olderThanOptions),
};
},
[types.UPDATE_SETTINGS](state, settings) {
state.settings = { ...state.settings, ...settings };
......
......@@ -23,4 +23,8 @@ export default () => ({
* Same structure as settings, above but Frozen object and used only in case the user clicks 'cancel'
*/
original: {},
/*
* Contains the options used to populate the form selects
*/
formOptions: {},
});
export const findDefaultOption = options => {
const item = options.find(o => o.default);
return item ? item.key : null;
};
export default () => {};
export const mapComputed = (root, updateFn, list) => {
/**
* Returns computed properties two way bound to vuex
*
* @param {(string[]|Object[])} list - list of string matching state keys or list objects
* @param {string} list[].key - the key matching the key present in the vuex state
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
* @param {string} root - the key of the state where to search fo they keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
export const mapComputed = (list, defaultUpdateFn, root) => {
const result = {};
list.forEach(key => {
list.forEach(item => {
const [getter, key, updateFn] =
typeof item === 'string'
? [false, item, defaultUpdateFn]
: [item.getter, item.key, item.updateFn || defaultUpdateFn];
result[key] = {
get() {
return this.$store.state[root][key];
if (getter) {
return this.$store.getters[getter];
} else if (root) {
return this.$store.state[root][key];
}
return this.$store.state[key];
},
set(value) {
this.$store.dispatch(updateFn, { [key]: value });
......
#js-registry-settings{ data: { project_id: @project.id, } }
#js-registry-settings{ data: { project_id: @project.id,
cadence_options: cadence_options.to_json,
keep_n_options: keep_n_options.to_json,
older_than_options: older_than_options.to_json} }
......@@ -12639,12 +12639,6 @@ msgstr ""
msgid "OperationsDashboard|The operations dashboard provides a summary of each project's operational health, including pipeline and alert statuses."
msgstr ""
msgid "Option 1"
msgstr ""
msgid "Option 2"
msgstr ""
msgid "Optional"
msgstr ""
......@@ -15784,9 +15778,6 @@ msgstr ""
msgid "Save Changes"
msgstr ""
msgid "Save Expiration Policy"
msgstr ""
msgid "Save anyway"
msgstr ""
......@@ -15802,6 +15793,9 @@ msgstr ""
msgid "Save comment"
msgstr ""
msgid "Save expiration policy"
msgstr ""
msgid "Save password"
msgstr ""
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Settings Form renders 1`] = `
<div
class="card"
>
<form>
<form>
<div
class="card"
>
<!---->
<div
class="card-header"
>
......@@ -12,11 +13,13 @@ exports[`Settings Form renders 1`] = `
Tag expiration policy
</div>
<div
class="card-body"
>
<gl-form-group-stub
<!---->
<!---->
<glformgroup-stub
id="expiration-policy-toggle-group"
label="Expiration policy:"
label-align="right"
......@@ -26,7 +29,7 @@ exports[`Settings Form renders 1`] = `
<div
class="d-flex align-items-start"
>
<gl-toggle-stub
<gltoggle-stub
id="expiration-policy-toggle"
labeloff="Toggle Status: OFF"
labelon="Toggle Status: ON"
......@@ -41,81 +44,96 @@ exports[`Settings Form renders 1`] = `
</strong>
</span>
</div>
</gl-form-group-stub>
</glformgroup-stub>
<gl-form-group-stub
<glformgroup-stub
id="expiration-policy-interval-group"
label="Expiration interval:"
label-align="right"
label-cols="3"
label-for="expiration-policy-interval"
>
<gl-form-select-stub
<glformselect-stub
disabled="true"
id="expiration-policy-interval"
value="bar"
>
<option
value="1"
value="foo"
>
Option 1
Foo
</option>
<option
value="2"
value="bar"
>
Option 2
Bar
</option>
</gl-form-select-stub>
</gl-form-group-stub>
</glformselect-stub>
</glformgroup-stub>
<gl-form-group-stub
<glformgroup-stub
id="expiration-policy-schedule-group"
label="Expiration schedule:"
label-align="right"
label-cols="3"
label-for="expiration-policy-schedule"
>
<gl-form-select-stub
<glformselect-stub
disabled="true"
id="expiration-policy-schedule"
value="bar"
>
<option
value="1"
value="foo"
>
Option 1
Foo
</option>
<option
value="2"
value="bar"
>
Option 2
Bar
</option>
</gl-form-select-stub>
</gl-form-group-stub>
</glformselect-stub>
</glformgroup-stub>
<gl-form-group-stub
<glformgroup-stub
id="expiration-policy-latest-group"
label="Expiration latest:"
label-align="right"
label-cols="3"
label-for="expiration-policy-latest"
>
<gl-form-select-stub
<glformselect-stub
disabled="true"
id="expiration-policy-latest"
value="bar"
>
<option
value="1"
value="foo"
>
Option 1
Foo
</option>
<option
value="2"
value="bar"
>
Option 2
Bar
</option>
</gl-form-select-stub>
</gl-form-group-stub>
</glformselect-stub>
</glformgroup-stub>
<gl-form-group-stub
<glformgroup-stub
id="expiration-policy-name-matching-group"
invalid-feedback="The value of this input should be less than 255 characters"
label="Expire Docker tags with name matching:"
......@@ -123,33 +141,41 @@ exports[`Settings Form renders 1`] = `
label-cols="3"
label-for="expiration-policy-name-matching"
>
<gl-form-textarea-stub
<glformtextarea-stub
disabled="true"
id="expiration-policy-name-matching"
placeholder=".*"
trim=""
value=""
/>
</gl-form-group-stub>
</glformgroup-stub>
</div>
<div
class="card-footer text-right"
class="card-footer"
>
<gl-button-stub
type="reset"
>
Cancel
</gl-button-stub>
<gl-button-stub
type="submit"
variant="success"
<div
class="d-flex justify-content-end"
>
<glbutton-stub
class="mr-2 d-block"
type="reset"
>
Cancel
</glbutton-stub>
<glbutton-stub
class="d-block"
type="submit"
variant="success"
>
Save expiration policy
Save Expiration Policy
</gl-button-stub>
</glbutton-stub>
</div>
</div>
</form>
</div>
<!---->
</div>
</form>
`;
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { mount, createLocalVue } from '@vue/test-utils';
import stubChildren from 'helpers/stub_children';
import component from '~/registry/settings/components/settings_form.vue';
import { createStore } from '~/registry/settings/store/';
import { NAME_REGEX_LENGTH } from '~/registry/settings/constants';
import { stringifiedFormOptions } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
......@@ -13,7 +15,6 @@ describe('Settings Form', () => {
let saveSpy;
let resetSpy;
const helpPagePath = 'foo';
const findFormGroup = name => wrapper.find(`#expiration-policy-${name}-group`);
const findFormElements = (name, father = wrapper) => father.find(`#expiration-policy-${name}`);
const findCancelButton = () => wrapper.find({ ref: 'cancel-button' });
......@@ -23,7 +24,11 @@ describe('Settings Form', () => {
const mountComponent = (options = {}) => {
saveSpy = jest.fn();
resetSpy = jest.fn();
wrapper = shallowMount(component, {
wrapper = mount(component, {
stubs: {
...stubChildren(component),
GlCard: false,
},
store,
methods: {
saveSettings: saveSpy,
......@@ -35,7 +40,7 @@ describe('Settings Form', () => {
beforeEach(() => {
store = createStore();
store.dispatch('setInitialState', { helpPagePath });
store.dispatch('setInitialState', stringifiedFormOptions);
mountComponent();
});
......@@ -48,13 +53,13 @@ describe('Settings Form', () => {
});
describe.each`
elementName | modelName | value
${'toggle'} | ${'enabled'} | ${true}
${'interval'} | ${'older_than'} | ${'foo'}
${'schedule'} | ${'cadence'} | ${'foo'}
${'latest'} | ${'keep_n'} | ${'foo'}
${'name-matching'} | ${'name_regex'} | ${'foo'}
`('%s form element', ({ elementName, modelName, value }) => {
elementName | modelName | value | disabledByToggle
${'toggle'} | ${'enabled'} | ${true} | ${'not disabled'}
${'interval'} | ${'older_than'} | ${'foo'} | ${'disabled'}
${'schedule'} | ${'cadence'} | ${'foo'} | ${'disabled'}
${'latest'} | ${'keep_n'} | ${'foo'} | ${'disabled'}
${'name-matching'} | ${'name_regex'} | ${'foo'} | ${'disabled'}
`('$elementName form element', ({ elementName, modelName, value, disabledByToggle }) => {
let formGroup;
beforeEach(() => {
formGroup = findFormGroup(elementName);
......@@ -89,6 +94,12 @@ describe('Settings Form', () => {
expect(wrapper.vm[modelName]).toBe(value);
});
});
it(`${elementName} is ${disabledByToggle} by enabled set to false`, () => {
store.dispatch('updateSettings', { enabled: false });
const expectation = disabledByToggle === 'disabled' ? 'true' : undefined;
expect(findFormElements(elementName, formGroup).attributes('disabled')).toBe(expectation);
});
});
describe('form actions', () => {
......
export const options = [{ key: 'foo', label: 'Foo' }, { key: 'bar', label: 'Bar', default: true }];
export const stringifiedOptions = JSON.stringify(options);
export const stringifiedFormOptions = {
cadenceOptions: stringifiedOptions,
keepNOptions: stringifiedOptions,
olderThanOptions: stringifiedOptions,
};
export const formOptions = {
cadence: options,
keepN: options,
olderThan: options,
};
import mutations from '~/registry/settings/store/mutations';
import * as types from '~/registry/settings/store/mutation_types';
import createState from '~/registry/settings/store/state';
import { formOptions, stringifiedFormOptions } from '../mock_data';
describe('Mutations Registry Store', () => {
let mockState;
......@@ -11,11 +12,14 @@ describe('Mutations Registry Store', () => {
describe('SET_INITIAL_STATE', () => {
it('should set the initial state', () => {
const payload = { helpPagePath: 'foo', projectId: 'bar' };
const expectedState = { ...mockState, ...payload };
mutations[types.SET_INITIAL_STATE](mockState, payload);
const expectedState = { ...mockState, projectId: 'foo', formOptions };
mutations[types.SET_INITIAL_STATE](mockState, {
projectId: 'foo',
...stringifiedFormOptions,
});
expect(mockState.projectId).toEqual(expectedState.projectId);
expect(mockState.formOptions).toEqual(expectedState.formOptions);
});
});
......
......@@ -3,49 +3,77 @@ import { mapComputed } from '~/vuex_shared/bindings';
describe('Binding utils', () => {
describe('mapComputed', () => {
const dummyComponent = {
const defaultArgs = [['baz'], 'bar', 'foo'];
const createDummy = (mapComputedArgs = defaultArgs) => ({
computed: {
...mapComputed('foo', 'bar', ['baz']),
...mapComputed(...mapComputedArgs),
},
render() {
return null;
},
});
const mocks = {
$store: {
state: {
baz: 2,
foo: {
baz: 1,
},
},
getters: {
getBaz: 'foo',
},
dispatch: jest.fn(),
},
};
it('returns an object with keys equal to the last fn parameter ', () => {
it('returns an object with keys equal to the first fn parameter ', () => {
const keyList = ['foo1', 'foo2'];
const result = mapComputed('foo', 'bar', keyList);
const result = mapComputed(keyList, 'foo', 'bar');
expect(Object.keys(result)).toEqual(keyList);
});
it('returned object has set and get function', () => {
const result = mapComputed('foo', 'bar', ['baz']);
const result = mapComputed(['baz'], 'foo', 'bar');
expect(result.baz.set).toBeDefined();
expect(result.baz.get).toBeDefined();
});
it('set function invokes $store.dispatch', () => {
const context = shallowMount(dummyComponent, {
mocks: {
$store: {
dispatch: jest.fn(),
},
},
describe('set function', () => {
it('invokes $store.dispatch', () => {
const context = shallowMount(createDummy(), { mocks });
context.vm.baz = 'a';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
});
it('uses updateFn in list object mode if updateFn exists', () => {
const context = shallowMount(createDummy([[{ key: 'foo', updateFn: 'baz' }]]), { mocks });
context.vm.foo = 'b';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('baz', { foo: 'b' });
});
it('in list object mode defaults to defaultUpdateFn if updateFn do not exists', () => {
const context = shallowMount(createDummy([[{ key: 'foo' }], 'defaultFn']), { mocks });
context.vm.foo = 'c';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('defaultFn', { foo: 'c' });
});
context.vm.baz = 'a';
expect(context.vm.$store.dispatch).toHaveBeenCalledWith('bar', { baz: 'a' });
});
it('get function returns $store.state[root][key]', () => {
const context = shallowMount(dummyComponent, {
mocks: {
$store: {
state: {
foo: {
baz: 1,
},
},
},
},
describe('get function', () => {
it('if root is set returns $store.state[root][key]', () => {
const context = shallowMount(createDummy(), { mocks });
expect(context.vm.baz).toBe(mocks.$store.state.foo.baz);
});
it('if root is not set returns $store.state[key]', () => {
const context = shallowMount(createDummy([['baz'], 'bar']), { mocks });
expect(context.vm.baz).toBe(mocks.$store.state.baz);
});
it('when using getters it invoke the appropriate getter', () => {
const context = shallowMount(createDummy([[{ getter: 'getBaz', key: 'baz' }]]), { mocks });
expect(context.vm.baz).toBe(mocks.$store.getters.getBaz);
});
expect(context.vm.baz).toBe(1);
});
});
});
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