Commit 09fe7189 authored by Zack Cuddy's avatar Zack Cuddy

Add Selective Sync Namespaces to Vue Node Form

This changes is broken down from a very large MR:
https://gitlab.com/gitlab-org/gitlab/-/merge_requests/22719

The overall goal is to convert the Geo Node Form from HAML to Vue.
Based on MVC, splitting this into smaller MRs is more feasible.

This MR adds the final fields to the Geo From in Vue:
  - Selective Sync Namespaces (Multi-Select Dropdown)

After this MR is merged, the submit actions and API will be added.
parent fa3e5b75
<script>
import { GlIcon, GlSearchBoxByType, GlDropdown, GlButton } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import { debounce } from 'underscore';
import { __, n__ } from '~/locale';
import { SELECTIVE_SYNC_NAMESPACES } from '../constants';
export default {
name: 'GeoNodeFormNamespaces',
components: {
GlIcon,
GlSearchBoxByType,
GlDropdown,
GlButton,
},
props: {
selectedNamespaces: {
type: Array,
required: true,
},
},
data() {
return {
namespaceSearch: '',
};
},
computed: {
...mapState(['synchronizationNamespaces']),
dropdownTitle() {
if (this.selectedNamespaces.length === 0) {
return __('Select groups to replicate');
}
return n__('%d group selected', '%d groups selected', this.selectedNamespaces.length);
},
noSyncNamespaces() {
return this.synchronizationNamespaces.length === 0;
},
},
watch: {
namespaceSearch: debounce(function debounceSearch() {
this.fetchSyncNamespaces(this.namespaceSearch);
}, 500),
},
methods: {
...mapActions(['fetchSyncNamespaces']),
toggleNamespace(namespace) {
const index = this.selectedNamespaces.findIndex(id => id === namespace.id);
if (index > -1) {
this.$emit('removeSyncOption', { key: SELECTIVE_SYNC_NAMESPACES, index });
} else {
this.$emit('addSyncOption', { key: SELECTIVE_SYNC_NAMESPACES, value: namespace.id });
}
},
isSelected(namespace) {
return this.selectedNamespaces.includes(namespace.id);
},
},
};
</script>
<template>
<gl-dropdown :text="dropdownTitle" @show="fetchSyncNamespaces(namespaceSearch)">
<gl-search-box-by-type v-model="namespaceSearch" class="m-2" />
<li v-for="namespace in synchronizationNamespaces" :key="namespace.id">
<gl-button class="d-flex align-items-center" @click="toggleNamespace(namespace)">
<gl-icon :class="[{ invisible: !isSelected(namespace) }]" name="mobile-issue-close" />
<span class="ml-1">{{ namespace.name }}</span>
</gl-button>
</li>
<div v-if="noSyncNamespaces" class="text-secondary p-2">{{ __('Nothing found…') }}</div>
</gl-dropdown>
</template>
<script> <script>
import { GlFormGroup, GlFormSelect } from '@gitlab/ui'; import { GlFormGroup, GlFormSelect } from '@gitlab/ui';
import GeoNodeFormNamespaces from './geo_node_form_namespaces.vue';
import GeoNodeFormShards from './geo_node_form_shards.vue'; import GeoNodeFormShards from './geo_node_form_shards.vue';
export default { export default {
...@@ -7,6 +8,7 @@ export default { ...@@ -7,6 +8,7 @@ export default {
components: { components: {
GlFormGroup, GlFormGroup,
GlFormSelect, GlFormSelect,
GeoNodeFormNamespaces,
GeoNodeFormShards, GeoNodeFormShards,
}, },
props: { props: {
...@@ -24,6 +26,9 @@ export default { ...@@ -24,6 +26,9 @@ export default {
}, },
}, },
computed: { computed: {
selectiveSyncNamespaces() {
return this.nodeData.selectiveSyncType === this.selectiveSyncTypes.NAMESPACES.value;
},
selectiveSyncShards() { selectiveSyncShards() {
return this.nodeData.selectiveSyncType === this.selectiveSyncTypes.SHARDS.value; return this.nodeData.selectiveSyncType === this.selectiveSyncTypes.SHARDS.value;
}, },
...@@ -54,6 +59,19 @@ export default { ...@@ -54,6 +59,19 @@ export default {
class="col-sm-6" class="col-sm-6"
/> />
</gl-form-group> </gl-form-group>
<gl-form-group
v-if="selectiveSyncNamespaces"
:label="__('Groups to synchronize')"
label-for="node-synchronization-namespaces-field"
:description="__('Choose which groups you wish to synchronize to this secondary node')"
>
<geo-node-form-namespaces
id="node-synchronization-namespaces-field"
:selected-namespaces="nodeData.selectiveSyncNamespaceIds"
@addSyncOption="addSyncOption"
@removeSyncOption="removeSyncOption"
/>
</gl-form-group>
<gl-form-group <gl-form-group
v-if="selectiveSyncShards" v-if="selectiveSyncShards"
:label="__('Shards to synchronize')" :label="__('Shards to synchronize')"
......
<script> <script>
import { GlIcon, GlDropdown, GlButton } from '@gitlab/ui'; import { GlIcon, GlDropdown, GlButton } from '@gitlab/ui';
import { __, sprintf } from '~/locale'; import { __, n__ } from '~/locale';
import { SELECTIVE_SYNC_SHARDS } from '../constants'; import { SELECTIVE_SYNC_SHARDS } from '../constants';
export default { export default {
...@@ -25,7 +25,8 @@ export default { ...@@ -25,7 +25,8 @@ export default {
if (this.selectedShards.length === 0) { if (this.selectedShards.length === 0) {
return __('Select shards to replicate'); return __('Select shards to replicate');
} }
return sprintf(__('Shards selected: %{count}'), { count: this.selectedShards.length });
return n__('%d shard selected', '%d shards selected', this.selectedShards.length);
}, },
noSyncShards() { noSyncShards() {
return this.syncShardsOptions.length === 0; return this.syncShardsOptions.length === 0;
......
/* eslint-disable import/prefer-default-export */
export const SELECTIVE_SYNC_SHARDS = 'selectiveSyncShards'; export const SELECTIVE_SYNC_SHARDS = 'selectiveSyncShards';
export const SELECTIVE_SYNC_NAMESPACES = 'selectiveSyncNamespaceIds';
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createStore from './store';
import GeoNodeFormApp from './components/app.vue'; import GeoNodeFormApp from './components/app.vue';
Vue.use(Translate); Vue.use(Translate);
...@@ -10,6 +11,7 @@ export default () => { ...@@ -10,6 +11,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
store: createStore(),
components: { components: {
GeoNodeFormApp, GeoNodeFormApp,
}, },
......
import Api from '~/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
export const requestSyncNamespaces = ({ commit }) => commit(types.REQUEST_SYNC_NAMESPACES);
export const receiveSyncNamespacesSuccess = ({ commit }, data) =>
commit(types.RECEIVE_SYNC_NAMESPACES_SUCCESS, data);
export const receiveSyncNamespacesError = ({ commit }) => {
createFlash(__("There was an error fetching the Node's Groups"));
commit(types.RECEIVE_SYNC_NAMESPACES_ERROR);
};
export const fetchSyncNamespaces = ({ dispatch }, search) => {
dispatch('requestSyncNamespaces');
Api.groups(search)
.then(res => {
dispatch('receiveSyncNamespacesSuccess', res);
})
.catch(() => {
dispatch('receiveSyncNamespacesError');
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
const createStore = () =>
new Vuex.Store({
actions,
mutations,
state: createState(),
});
export default createStore;
export const REQUEST_SYNC_NAMESPACES = 'REQUEST_SYNC_NAMESPACES';
export const RECEIVE_SYNC_NAMESPACES_SUCCESS = 'RECEIVE_SYNC_NAMESPACES_SUCCESS';
export const RECEIVE_SYNC_NAMESPACES_ERROR = 'RECEIVE_SYNC_NAMESPACES_ERROR';
import * as types from './mutation_types';
export default {
[types.REQUEST_SYNC_NAMESPACES](state) {
state.isLoading = true;
},
[types.RECEIVE_SYNC_NAMESPACES_SUCCESS](state, data) {
state.isLoading = false;
state.synchronizationNamespaces = data;
},
[types.RECEIVE_SYNC_NAMESPACES_ERROR](state) {
state.isLoading = false;
state.synchronizationNamespaces = [];
},
};
const createState = () => ({
isLoading: false,
synchronizationNamespaces: [],
});
export default createState;
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
import GeoNodeFormNamespaces from 'ee/geo_node_form/components/geo_node_form_namespaces.vue';
import store from 'ee/geo_node_form/store';
import { MOCK_SYNC_NAMESPACES } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/flash');
describe('GeoNodeFormNamespaces', () => {
let wrapper;
const defaultProps = {
selectedNamespaces: [],
};
const actionSpies = {
fetchSyncNamespaces: jest.fn(),
toggleNamespace: jest.fn(),
isSelected: jest.fn(),
};
const createComponent = (props = {}) => {
wrapper = shallowMount(GeoNodeFormNamespaces, {
localVue,
store,
propsData: {
...defaultProps,
...props,
},
methods: {
...actionSpies,
},
});
};
afterEach(() => {
wrapper.destroy();
});
const findGlDropdown = () => wrapper.find(GlDropdown);
const findGlDropdownSearch = () => findGlDropdown().find(GlSearchBoxByType);
const findDropdownItems = () => findGlDropdown().findAll('li');
const findDropdownItemsText = () => findDropdownItems().wrappers.map(w => w.text());
const findGlIcons = () => wrapper.findAll(GlIcon);
describe('template', () => {
beforeEach(() => {
createComponent();
});
it('renders GlDropdown', () => {
expect(findGlDropdown().exists()).toBe(true);
});
it('renders findGlDropdownSearch', () => {
expect(findGlDropdownSearch().exists()).toBe(true);
});
describe('findDropdownItems', () => {
beforeEach(() => {
delete actionSpies.isSelected;
createComponent({
selectedNamespaces: [[MOCK_SYNC_NAMESPACES[0].id]],
});
wrapper.vm.$store.state.synchronizationNamespaces = MOCK_SYNC_NAMESPACES;
});
it('renders an instance for each namespace', () => {
expect(findDropdownItemsText()).toStrictEqual(MOCK_SYNC_NAMESPACES.map(n => n.name));
});
it('hides GlIcon if namespace not in selectedNamespaces', () => {
expect(findGlIcons().wrappers.every(w => w.classes('invisible'))).toBe(true);
});
});
});
describe('watchers', () => {
describe('namespaceSearch', () => {
const namespaceSearch = 'test search';
beforeEach(() => {
createComponent();
wrapper.setData({
namespaceSearch,
});
});
it('should wait 500ms before calling fetchSyncNamespaces', () => {
expect(actionSpies.fetchSyncNamespaces).not.toHaveBeenCalledWith(namespaceSearch);
jest.advanceTimersByTime(500); // Debounce
expect(actionSpies.fetchSyncNamespaces).toHaveBeenCalledWith(namespaceSearch);
expect(actionSpies.fetchSyncNamespaces).toHaveBeenCalledTimes(1);
});
it('should call fetchSyncNamespaces once with the latest search term', () => {
expect(actionSpies.fetchSyncNamespaces).not.toHaveBeenCalledWith(namespaceSearch);
wrapper.setData({
namespaceSearch: 'test search2',
});
jest.advanceTimersByTime(500); // Debounce
expect(actionSpies.fetchSyncNamespaces).toHaveBeenCalledWith('test search2');
expect(actionSpies.fetchSyncNamespaces).toHaveBeenCalledTimes(1);
});
});
});
describe('methods', () => {
describe('toggleNamespace', () => {
beforeEach(() => {
delete actionSpies.toggleNamespace;
createComponent({
selectedNamespaces: [MOCK_SYNC_NAMESPACES[0].id],
});
});
describe('when namespace is in selectedNamespaces', () => {
it('emits `removeSyncOption`', () => {
wrapper.vm.toggleNamespace(MOCK_SYNC_NAMESPACES[0]);
expect(wrapper.emitted('removeSyncOption')).toBeTruthy();
});
});
describe('when namespace is not in selectedNamespaces', () => {
it('emits `addSyncOption`', () => {
wrapper.vm.toggleNamespace(MOCK_SYNC_NAMESPACES[1]);
expect(wrapper.emitted('addSyncOption')).toBeTruthy();
});
});
});
describe('isSelected', () => {
beforeEach(() => {
delete actionSpies.isSelected;
createComponent({
selectedNamespaces: [MOCK_SYNC_NAMESPACES[0].id],
});
});
describe('when namespace is in selectedNamespaces', () => {
it('returns `true`', () => {
expect(wrapper.vm.isSelected(MOCK_SYNC_NAMESPACES[0])).toBeTruthy();
});
});
describe('when namespace is not in selectedNamespaces', () => {
it('returns `false`', () => {
expect(wrapper.vm.isSelected(MOCK_SYNC_NAMESPACES[1])).toBeFalsy();
});
});
});
describe('computed', () => {
describe('dropdownTitle', () => {
describe('when selectedNamespaces is empty', () => {
beforeEach(() => {
createComponent({
selectedNamespaces: [],
});
});
it('returns `Select groups to replicate`', () => {
expect(wrapper.vm.dropdownTitle).toBe('Select groups to replicate');
});
});
describe('when selectedNamespaces length === 1', () => {
beforeEach(() => {
createComponent({
selectedNamespaces: [MOCK_SYNC_NAMESPACES[0].id],
});
});
it('returns `this.selectedNamespaces.length` group selected', () => {
expect(wrapper.vm.dropdownTitle).toBe(
`${wrapper.vm.selectedNamespaces.length} group selected`,
);
});
});
describe('when selectedNamespaces length > 1', () => {
beforeEach(() => {
createComponent({
selectedNamespaces: [MOCK_SYNC_NAMESPACES[0].id, MOCK_SYNC_NAMESPACES[1].id],
});
});
it('returns `this.selectedNamespaces.length` group selected', () => {
expect(wrapper.vm.dropdownTitle).toBe(
`${wrapper.vm.selectedNamespaces.length} groups selected`,
);
});
});
});
describe('noSyncNamespaces', () => {
describe('when synchronizationNamespaces.length > 0', () => {
beforeEach(() => {
createComponent();
wrapper.vm.$store.state.synchronizationNamespaces = MOCK_SYNC_NAMESPACES;
});
it('returns `false`', () => {
expect(wrapper.vm.noSyncNamespaces).toBeFalsy();
});
});
});
describe('when synchronizationNamespaces.length === 0', () => {
beforeEach(() => {
createComponent();
wrapper.vm.$store.state.synchronizationNamespaces = [];
});
it('returns `true`', () => {
expect(wrapper.vm.noSyncNamespaces).toBeTruthy();
});
});
});
});
});
import { mount } from '@vue/test-utils'; import { shallowMount } from '@vue/test-utils';
import GeoNodeFormSelectiveSync from 'ee/geo_node_form/components/geo_node_form_selective_sync.vue'; import GeoNodeFormSelectiveSync from 'ee/geo_node_form/components/geo_node_form_selective_sync.vue';
import GeoNodeFormNamespaces from 'ee/geo_node_form/components/geo_node_form_namespaces.vue';
import GeoNodeFormShards from 'ee/geo_node_form/components/geo_node_form_shards.vue'; import GeoNodeFormShards from 'ee/geo_node_form/components/geo_node_form_shards.vue';
import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data'; import { MOCK_NODE, MOCK_SELECTIVE_SYNC_TYPES, MOCK_SYNC_SHARDS } from '../mock_data';
describe('GeoNodeFormSelectiveSync', () => { describe('GeoNodeFormSelectiveSync', () => {
let wrapper; let wrapper;
const propsData = { const defaultProps = {
nodeData: MOCK_NODE, nodeData: MOCK_NODE,
selectiveSyncTypes: MOCK_SELECTIVE_SYNC_TYPES, selectiveSyncTypes: MOCK_SELECTIVE_SYNC_TYPES,
syncShardsOptions: MOCK_SYNC_SHARDS, syncShardsOptions: MOCK_SYNC_SHARDS,
}; };
const createComponent = () => { const createComponent = (props = {}) => {
wrapper = mount(GeoNodeFormSelectiveSync, { wrapper = shallowMount(GeoNodeFormSelectiveSync, {
propsData, propsData: {
...defaultProps,
...props,
},
}); });
}; };
...@@ -25,6 +29,7 @@ describe('GeoNodeFormSelectiveSync', () => { ...@@ -25,6 +29,7 @@ describe('GeoNodeFormSelectiveSync', () => {
const findGeoNodeFormSyncContainer = () => const findGeoNodeFormSyncContainer = () =>
wrapper.find({ ref: 'geoNodeFormSelectiveSyncContainer' }); wrapper.find({ ref: 'geoNodeFormSelectiveSyncContainer' });
const findGeoNodeFormSyncTypeField = () => wrapper.find('#node-selective-synchronization-field'); const findGeoNodeFormSyncTypeField = () => wrapper.find('#node-selective-synchronization-field');
const findGeoNodeFormNamespacesField = () => wrapper.find(GeoNodeFormNamespaces);
const findGeoNodeFormShardsField = () => wrapper.find(GeoNodeFormShards); const findGeoNodeFormShardsField = () => wrapper.find(GeoNodeFormShards);
describe('template', () => { describe('template', () => {
...@@ -41,18 +46,21 @@ describe('GeoNodeFormSelectiveSync', () => { ...@@ -41,18 +46,21 @@ describe('GeoNodeFormSelectiveSync', () => {
}); });
describe.each` describe.each`
syncType | showShards syncType | showNamespaces | showShards
${MOCK_SELECTIVE_SYNC_TYPES.ALL} | ${false} ${MOCK_SELECTIVE_SYNC_TYPES.ALL} | ${false} | ${false}
${MOCK_SELECTIVE_SYNC_TYPES.NAMESPACES} | ${false} ${MOCK_SELECTIVE_SYNC_TYPES.NAMESPACES} | ${true} | ${false}
${MOCK_SELECTIVE_SYNC_TYPES.SHARDS} | ${true} ${MOCK_SELECTIVE_SYNC_TYPES.SHARDS} | ${false} | ${true}
`(`sync type`, ({ syncType, showShards }) => { `(`sync type`, ({ syncType, showNamespaces, showShards }) => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({ nodeData: { ...defaultProps.nodeData, selectiveSyncType: syncType.value },
nodeData: { ...propsData.nodeData, selectiveSyncType: syncType.value },
}); });
}); });
it(`${showNamespaces ? 'show' : 'hide'} Namespaces Field`, () => {
expect(findGeoNodeFormNamespacesField().exists()).toBe(showNamespaces);
});
it(`${showShards ? 'show' : 'hide'} Shards Field`, () => { it(`${showShards ? 'show' : 'hide'} Shards Field`, () => {
expect(findGeoNodeFormShardsField().exists()).toBe(showShards); expect(findGeoNodeFormShardsField().exists()).toBe(showShards);
}); });
...@@ -73,9 +81,8 @@ describe('GeoNodeFormSelectiveSync', () => { ...@@ -73,9 +81,8 @@ describe('GeoNodeFormSelectiveSync', () => {
describe('removeSyncOption', () => { describe('removeSyncOption', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({ nodeData: { ...defaultProps.nodeData, selectiveSyncShards: [MOCK_SYNC_SHARDS[0].value] },
nodeData: { ...propsData.nodeData, selectiveSyncShards: [MOCK_SYNC_SHARDS[0].value] },
}); });
}); });
...@@ -87,16 +94,36 @@ describe('GeoNodeFormSelectiveSync', () => { ...@@ -87,16 +94,36 @@ describe('GeoNodeFormSelectiveSync', () => {
}); });
describe('computed', () => { describe('computed', () => {
const factory = (selectiveSyncType = MOCK_SELECTIVE_SYNC_TYPES.ALL.value) => {
createComponent({ nodeData: { ...defaultProps.nodeData, selectiveSyncType } });
};
describe('selectiveSyncNamespaces', () => {
describe('when selectiveSyncType is not `NAMESPACES`', () => {
beforeEach(() => {
factory();
});
it('returns `false`', () => {
expect(wrapper.vm.selectiveSyncNamespaces).toBeFalsy();
});
});
describe('when selectiveSyncType is `NAMESPACES`', () => {
beforeEach(() => {
factory(MOCK_SELECTIVE_SYNC_TYPES.NAMESPACES.value);
});
it('returns `true`', () => {
expect(wrapper.vm.selectiveSyncNamespaces).toBeTruthy();
});
});
});
describe('selectiveSyncShards', () => { describe('selectiveSyncShards', () => {
describe('when selectiveSyncType is not `SHARDS`', () => { describe('when selectiveSyncType is not `SHARDS`', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); factory(MOCK_SELECTIVE_SYNC_TYPES.ALL.value);
wrapper.setProps({
nodeData: {
...propsData.nodeData,
selectiveSyncType: MOCK_SELECTIVE_SYNC_TYPES.ALL.value,
},
});
}); });
it('returns `false`', () => { it('returns `false`', () => {
...@@ -106,13 +133,7 @@ describe('GeoNodeFormSelectiveSync', () => { ...@@ -106,13 +133,7 @@ describe('GeoNodeFormSelectiveSync', () => {
describe('when selectiveSyncType is `SHARDS`', () => { describe('when selectiveSyncType is `SHARDS`', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); factory(MOCK_SELECTIVE_SYNC_TYPES.SHARDS.value);
wrapper.setProps({
nodeData: {
...propsData.nodeData,
selectiveSyncType: MOCK_SELECTIVE_SYNC_TYPES.SHARDS.value,
},
});
}); });
it('returns `true`', () => { it('returns `true`', () => {
......
...@@ -6,7 +6,7 @@ import { MOCK_SYNC_SHARDS } from '../mock_data'; ...@@ -6,7 +6,7 @@ import { MOCK_SYNC_SHARDS } from '../mock_data';
describe('GeoNodeFormShards', () => { describe('GeoNodeFormShards', () => {
let wrapper; let wrapper;
const propsData = { const defaultProps = {
selectedShards: [], selectedShards: [],
syncShardsOptions: MOCK_SYNC_SHARDS, syncShardsOptions: MOCK_SYNC_SHARDS,
}; };
...@@ -16,9 +16,12 @@ describe('GeoNodeFormShards', () => { ...@@ -16,9 +16,12 @@ describe('GeoNodeFormShards', () => {
isSelected: jest.fn(), isSelected: jest.fn(),
}; };
const createComponent = () => { const createComponent = (props = {}) => {
wrapper = mount(GeoNodeFormShards, { wrapper = mount(GeoNodeFormShards, {
propsData, propsData: {
...defaultProps,
...props,
},
methods: { methods: {
...actionSpies, ...actionSpies,
}, },
...@@ -44,8 +47,7 @@ describe('GeoNodeFormShards', () => { ...@@ -44,8 +47,7 @@ describe('GeoNodeFormShards', () => {
describe('DropdownItems', () => { describe('DropdownItems', () => {
beforeEach(() => { beforeEach(() => {
delete actionSpies.isSelected; delete actionSpies.isSelected;
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
...@@ -80,8 +82,7 @@ describe('GeoNodeFormShards', () => { ...@@ -80,8 +82,7 @@ describe('GeoNodeFormShards', () => {
describe('when shard is in selectedShards', () => { describe('when shard is in selectedShards', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
...@@ -94,8 +95,7 @@ describe('GeoNodeFormShards', () => { ...@@ -94,8 +95,7 @@ describe('GeoNodeFormShards', () => {
describe('when shard is not in selectedShards', () => { describe('when shard is not in selectedShards', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
...@@ -114,8 +114,7 @@ describe('GeoNodeFormShards', () => { ...@@ -114,8 +114,7 @@ describe('GeoNodeFormShards', () => {
describe('when shard is in selectedShards', () => { describe('when shard is in selectedShards', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
...@@ -127,8 +126,7 @@ describe('GeoNodeFormShards', () => { ...@@ -127,8 +126,7 @@ describe('GeoNodeFormShards', () => {
describe('when shard is not in selectedShards', () => { describe('when shard is not in selectedShards', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
...@@ -143,8 +141,7 @@ describe('GeoNodeFormShards', () => { ...@@ -143,8 +141,7 @@ describe('GeoNodeFormShards', () => {
describe('dropdownTitle', () => { describe('dropdownTitle', () => {
describe('when selectedShards is empty', () => { describe('when selectedShards is empty', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [], selectedShards: [],
}); });
}); });
...@@ -154,17 +151,30 @@ describe('GeoNodeFormShards', () => { ...@@ -154,17 +151,30 @@ describe('GeoNodeFormShards', () => {
}); });
}); });
describe('when selectedShards is not empty', () => { describe('when selectedShards length === 1', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
selectedShards: [MOCK_SYNC_SHARDS[0].value], selectedShards: [MOCK_SYNC_SHARDS[0].value],
}); });
}); });
it('returns Shards selected: `this.selectedShards.length`', () => { it('returns `this.selectedShards.length` shard selected', () => {
expect(wrapper.vm.dropdownTitle).toBe(
`${wrapper.vm.selectedShards.length} shard selected`,
);
});
});
describe('when selectedShards length > 1', () => {
beforeEach(() => {
createComponent({
selectedShards: [MOCK_SYNC_SHARDS[0].value, MOCK_SYNC_SHARDS[1].value],
});
});
it('returns `this.selectedShards.length` shards selected', () => {
expect(wrapper.vm.dropdownTitle).toBe( expect(wrapper.vm.dropdownTitle).toBe(
`Shards selected: ${wrapper.vm.selectedShards.length}`, `${wrapper.vm.selectedShards.length} shards selected`,
); );
}); });
}); });
...@@ -184,8 +194,7 @@ describe('GeoNodeFormShards', () => { ...@@ -184,8 +194,7 @@ describe('GeoNodeFormShards', () => {
describe('when syncShardsOptions.length === 0', () => { describe('when syncShardsOptions.length === 0', () => {
beforeEach(() => { beforeEach(() => {
createComponent(); createComponent({
wrapper.setProps({
syncShardsOptions: [], syncShardsOptions: [],
}); });
}); });
......
...@@ -28,6 +28,21 @@ export const MOCK_SYNC_SHARDS = [ ...@@ -28,6 +28,21 @@ export const MOCK_SYNC_SHARDS = [
}, },
]; ];
export const MOCK_SYNC_NAMESPACES = [
{
name: 'Namespace 1',
id: 'namespace1',
},
{
name: 'Namespace 2',
id: 'namespace2',
},
{
name: 'namespace 3',
id: 'Namespace3',
},
];
export const MOCK_NODE = { export const MOCK_NODE = {
id: 1, id: 1,
name: 'Mock Node', name: 'Mock Node',
...@@ -35,7 +50,7 @@ export const MOCK_NODE = { ...@@ -35,7 +50,7 @@ export const MOCK_NODE = {
primary: false, primary: false,
internalUrl: '', internalUrl: '',
selectiveSyncType: '', selectiveSyncType: '',
namespaceIds: [], selectiveSyncNamespaceIds: [],
selectiveSyncShards: [], selectiveSyncShards: [],
reposMaxCapacity: 25, reposMaxCapacity: 25,
filesMaxCapacity: 10, filesMaxCapacity: 10,
......
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import flash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as actions from 'ee/geo_node_form/store/actions';
import * as types from 'ee/geo_node_form/store/mutation_types';
import createState from 'ee/geo_node_form/store/state';
import { MOCK_SYNC_NAMESPACES } from '../mock_data';
jest.mock('~/flash');
describe('GeoNodeForm Store Actions', () => {
let state;
let mock;
beforeEach(() => {
state = createState();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('requestSyncNamespaces', () => {
it('should commit mutation REQUEST_SYNC_NAMESPACES', done => {
testAction(
actions.requestSyncNamespaces,
null,
state,
[{ type: types.REQUEST_SYNC_NAMESPACES }],
[],
done,
);
});
});
describe('receiveSyncNamespacesSuccess', () => {
it('should commit mutation RECEIVE_SYNC_NAMESPACES_SUCCESS', done => {
testAction(
actions.receiveSyncNamespacesSuccess,
MOCK_SYNC_NAMESPACES,
state,
[{ type: types.RECEIVE_SYNC_NAMESPACES_SUCCESS, payload: MOCK_SYNC_NAMESPACES }],
[],
done,
);
});
});
describe('receiveSyncNamespacesError', () => {
it('should commit mutation RECEIVE_SYNC_NAMESPACES_ERROR', () => {
testAction(
actions.receiveSyncNamespacesError,
null,
state,
[{ type: types.RECEIVE_SYNC_NAMESPACES_ERROR }],
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
flash.mockClear();
},
);
});
});
describe('fetchSyncNamespaces', () => {
describe('on success', () => {
beforeEach(() => {
mock.onGet().replyOnce(200, MOCK_SYNC_NAMESPACES);
});
it('should dispatch the request and success actions', done => {
testAction(
actions.fetchSyncNamespaces,
{},
state,
[],
[
{ type: 'requestSyncNamespaces' },
{ type: 'receiveSyncNamespacesSuccess', payload: MOCK_SYNC_NAMESPACES },
],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onGet().replyOnce(500, {});
});
it('should dispatch the request and error actions', done => {
testAction(
actions.fetchSyncNamespaces,
{},
state,
[],
[{ type: 'requestSyncNamespaces' }, { type: 'receiveSyncNamespacesError' }],
done,
);
});
});
});
});
import mutations from 'ee/geo_node_form/store/mutations';
import createState from 'ee/geo_node_form/store/state';
import * as types from 'ee/geo_node_form/store/mutation_types';
import { MOCK_SYNC_NAMESPACES } from '../mock_data';
describe('GeoNodeForm Store Mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('REQUEST_SYNC_NAMESPACES', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_SYNC_NAMESPACES](state);
expect(state.isLoading).toEqual(true);
});
});
describe('RECEIVE_SYNC_NAMESPACES_SUCCESS', () => {
it('sets isLoading to false', () => {
state.isLoading = true;
mutations[types.RECEIVE_SYNC_NAMESPACES_SUCCESS](state, MOCK_SYNC_NAMESPACES);
expect(state.isLoading).toEqual(false);
});
it('sets synchronizationNamespaces array with namespace data', () => {
mutations[types.RECEIVE_SYNC_NAMESPACES_SUCCESS](state, MOCK_SYNC_NAMESPACES);
expect(state.synchronizationNamespaces).toBe(MOCK_SYNC_NAMESPACES);
});
});
describe('RECEIVE_SYNC_NAMESPACES_ERROR', () => {
it('sets isLoading to false', () => {
state.isLoading = true;
mutations[types.RECEIVE_SYNC_NAMESPACES_ERROR](state);
expect(state.isLoading).toEqual(false);
});
it('resets synchronizationNamespaces array', () => {
state.synchronizationNamespaces = MOCK_SYNC_NAMESPACES;
mutations[types.RECEIVE_SYNC_NAMESPACES_ERROR](state);
expect(state.synchronizationNamespaces).toEqual([]);
});
});
});
...@@ -111,6 +111,11 @@ msgid_plural "%d fixed test results" ...@@ -111,6 +111,11 @@ msgid_plural "%d fixed test results"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d group selected"
msgid_plural "%d groups selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d inaccessible merge request" msgid "%d inaccessible merge request"
msgid_plural "%d inaccessible merge requests" msgid_plural "%d inaccessible merge requests"
msgstr[0] "" msgstr[0] ""
...@@ -171,6 +176,11 @@ msgid_plural "%d seconds" ...@@ -171,6 +176,11 @@ msgid_plural "%d seconds"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%d shard selected"
msgid_plural "%d shards selected"
msgstr[0] ""
msgstr[1] ""
msgid "%d staged change" msgid "%d staged change"
msgid_plural "%d staged changes" msgid_plural "%d staged changes"
msgstr[0] "" msgstr[0] ""
...@@ -3599,6 +3609,9 @@ msgstr "" ...@@ -3599,6 +3609,9 @@ msgstr ""
msgid "Choose what content you want to see on a group’s overview page" msgid "Choose what content you want to see on a group’s overview page"
msgstr "" msgstr ""
msgid "Choose which groups you wish to synchronize to this secondary node"
msgstr ""
msgid "Choose which repositories you want to connect and run CI/CD pipelines." msgid "Choose which repositories you want to connect and run CI/CD pipelines."
msgstr "" msgstr ""
...@@ -9903,6 +9916,9 @@ msgstr "" ...@@ -9903,6 +9916,9 @@ msgstr ""
msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}." msgid "Groups can also be nested by creating %{subgroup_docs_link_start}subgroups%{subgroup_docs_link_end}."
msgstr "" msgstr ""
msgid "Groups to synchronize"
msgstr ""
msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}" msgid "Groups with access to %{strong_start}%{group_name}%{strong_end}"
msgstr "" msgstr ""
...@@ -17118,6 +17134,9 @@ msgstr "" ...@@ -17118,6 +17134,9 @@ msgstr ""
msgid "Select group or project" msgid "Select group or project"
msgstr "" msgstr ""
msgid "Select groups to replicate"
msgstr ""
msgid "Select labels" msgid "Select labels"
msgstr "" msgstr ""
...@@ -17517,9 +17536,6 @@ msgstr "" ...@@ -17517,9 +17536,6 @@ msgstr ""
msgid "Severity: %{severity}" msgid "Severity: %{severity}"
msgstr "" msgstr ""
msgid "Shards selected: %{count}"
msgstr ""
msgid "Shards to synchronize" msgid "Shards to synchronize"
msgstr "" msgstr ""
...@@ -19434,6 +19450,9 @@ msgstr "" ...@@ -19434,6 +19450,9 @@ msgstr ""
msgid "There was an error fetching the Designs" msgid "There was an error fetching the Designs"
msgstr "" msgstr ""
msgid "There was an error fetching the Node's Groups"
msgstr ""
msgid "There was an error fetching the environments information." msgid "There was an error fetching the environments information."
msgstr "" msgstr ""
......
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