Commit d239af20 authored by Zack Cuddy's avatar Zack Cuddy Committed by Nicolò Maria Mezzopera

Geo Settings Form - Fetch Data from API

This MR is an attempt at MVC.

The MR hooks up the API for data fetching of
the application settings.

In the next MR validations and the POST/PUT
actions will be hooked up.
parent f0805a8b
...@@ -39,6 +39,7 @@ export default { ...@@ -39,6 +39,7 @@ export default {
vulnerabilityActionPath: '/api/:version/vulnerabilities/:id/:action', vulnerabilityActionPath: '/api/:version/vulnerabilities/:id/:action',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists', featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid', featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
applicationSettingsPath: '/api/:version/application/settings',
userSubscription(namespaceId) { userSubscription(namespaceId) {
const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId)); const url = Api.buildUrl(this.subscriptionPath).replace(':id', encodeURIComponent(namespaceId));
...@@ -349,4 +350,9 @@ export default { ...@@ -349,4 +350,9 @@ export default {
return axios.delete(url); return axios.delete(url);
}, },
getApplicationSettings() {
const url = Api.buildUrl(this.applicationSettingsPath);
return axios.get(url);
},
}; };
<script> <script>
import { mapActions, mapState } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import GeoSettingsForm from './geo_settings_form.vue'; import GeoSettingsForm from './geo_settings_form.vue';
export default { export default {
name: 'GeoSettingsApp', name: 'GeoSettingsApp',
components: { components: {
GlLoadingIcon,
GeoSettingsForm, GeoSettingsForm,
}, },
computed: {
...mapState(['isLoading']),
},
created() {
this.fetchGeoSettings();
},
methods: {
...mapActions(['fetchGeoSettings']),
},
}; };
</script> </script>
...@@ -19,6 +31,7 @@ export default { ...@@ -19,6 +31,7 @@ export default {
) )
}} }}
</p> </p>
<geo-settings-form /> <gl-loading-icon v-if="isLoading" size="xl" />
<geo-settings-form v-else />
</article> </article>
</template> </template>
<script> <script>
import { mapState } from 'vuex';
import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui'; import { GlFormGroup, GlFormInput, GlButton } from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP } from '../constants';
export default { export default {
name: 'GeoSettingsForm', name: 'GeoSettingsForm',
...@@ -10,11 +10,10 @@ export default { ...@@ -10,11 +10,10 @@ export default {
GlFormInput, GlFormInput,
GlButton, GlButton,
}, },
data() { computed: {
return { // The real connection between vuex and the component will be implemented in
timeout: DEFAULT_TIMEOUT, // a later MR, this feature is anyhow behind feature flag
allowedIp: DEFAULT_ALLOWED_IP, ...mapState(['timeout', 'allowedIp']),
};
}, },
methods: { methods: {
redirect() { redirect() {
......
import Vue from 'vue'; import Vue from 'vue';
import Translate from '~/vue_shared/translate'; import Translate from '~/vue_shared/translate';
import createStore from './store';
import GeoSettingsApp from './components/app.vue'; import GeoSettingsApp from './components/app.vue';
Vue.use(Translate); Vue.use(Translate);
...@@ -9,6 +10,7 @@ export default () => { ...@@ -9,6 +10,7 @@ export default () => {
return new Vue({ return new Vue({
el, el,
store: createStore(),
components: { components: {
GeoSettingsApp, GeoSettingsApp,
}, },
......
import Api from 'ee/api';
import createFlash from '~/flash';
import { __ } from '~/locale';
import * as types from './mutation_types';
// eslint-disable-next-line import/prefer-default-export
export const fetchGeoSettings = ({ commit }) => {
commit(types.REQUEST_GEO_SETTINGS);
Api.getApplicationSettings()
.then(({ data }) => {
commit(types.RECEIVE_GEO_SETTINGS_SUCCESS, {
timeout: data.geo_status_timeout,
allowedIp: data.geo_node_allowed_ips,
});
})
.catch(() => {
createFlash(__('There was an error fetching the Geo Settings'));
commit(types.RECEIVE_GEO_SETTINGS_ERROR);
});
};
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
import mutations from './mutations';
import createState from './state';
Vue.use(Vuex);
export default () =>
new Vuex.Store({
actions,
mutations,
state: createState(),
});
export const REQUEST_GEO_SETTINGS = 'REQUEST_GEO_SETTINGS';
export const RECEIVE_GEO_SETTINGS_SUCCESS = 'RECEIVE_GEO_SETTINGS_SUCCESS';
export const RECEIVE_GEO_SETTINGS_ERROR = 'RECEIVE_GEO_SETTINGS_ERROR';
import * as types from './mutation_types';
import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP } from '../constants';
export default {
[types.REQUEST_GEO_SETTINGS](state) {
state.isLoading = true;
},
[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, { timeout, allowedIp }) {
state.isLoading = false;
state.timeout = timeout;
state.allowedIp = allowedIp;
},
[types.RECEIVE_GEO_SETTINGS_ERROR](state) {
state.isLoading = false;
state.timeout = DEFAULT_TIMEOUT;
state.allowedIp = DEFAULT_ALLOWED_IP;
},
};
import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP } from '../constants';
export default () => ({
isLoading: false,
timeout: DEFAULT_TIMEOUT,
allowedIp: DEFAULT_ALLOWED_IP,
});
...@@ -841,4 +841,20 @@ describe('Api', () => { ...@@ -841,4 +841,20 @@ describe('Api', () => {
}); });
}); });
}); });
describe('getApplicationSettings', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/application/settings`;
const apiResponse = { mock_setting: 1, mock_setting2: 2 };
it('fetches applications settings', () => {
jest.spyOn(Api, 'buildUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get');
mock.onGet(expectedUrl).replyOnce(200, apiResponse);
return Api.getApplicationSettings().then(({ data }) => {
expect(data).toEqual(apiResponse);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
});
});
});
}); });
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import initStore from 'ee/geo_settings/store';
import * as types from 'ee/geo_settings/store/mutation_types';
import GeoSettingsApp from 'ee/geo_settings/components/app.vue'; import GeoSettingsApp from 'ee/geo_settings/components/app.vue';
import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue'; import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
describe('GeoSettingsApp', () => { describe('GeoSettingsApp', () => {
let wrapper; let wrapper;
let store;
const createStore = () => {
store = initStore();
jest.spyOn(store, 'dispatch').mockImplementation();
};
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(GeoSettingsApp); wrapper = shallowMount(GeoSettingsApp, {
store,
});
}; };
afterEach(() => { afterEach(() => {
...@@ -14,10 +31,12 @@ describe('GeoSettingsApp', () => { ...@@ -14,10 +31,12 @@ describe('GeoSettingsApp', () => {
}); });
const findGeoSettingsContainer = () => wrapper.find('[data-testid="geoSettingsContainer"]'); const findGeoSettingsContainer = () => wrapper.find('[data-testid="geoSettingsContainer"]');
const findGeoSettingsForm = () => wrapper.find(GeoSettingsForm); const containsGeoSettingsForm = () => wrapper.contains(GeoSettingsForm);
const containsGlLoadingIcon = () => wrapper.contains(GlLoadingIcon);
describe('renders', () => { describe('renders', () => {
beforeEach(() => { beforeEach(() => {
createStore();
createComponent(); createComponent();
}); });
...@@ -29,8 +48,39 @@ describe('GeoSettingsApp', () => { ...@@ -29,8 +48,39 @@ describe('GeoSettingsApp', () => {
expect(findGeoSettingsContainer().text()).toContain('Geo Settings'); expect(findGeoSettingsContainer().text()).toContain('Geo Settings');
}); });
it('Geo Settings Form', () => { describe('when not loading', () => {
expect(findGeoSettingsForm().exists()).toBe(true); it('Geo Settings Form', () => {
expect(containsGeoSettingsForm()).toBe(true);
});
it('not GlLoadingIcon', () => {
expect(containsGlLoadingIcon()).toBe(false);
});
});
describe('when loading', () => {
beforeEach(() => {
store.commit(types.REQUEST_GEO_SETTINGS);
});
it('not Geo Settings Form', () => {
expect(containsGeoSettingsForm()).toBe(false);
});
it('GlLoadingIcon', () => {
expect(containsGlLoadingIcon()).toBe(true);
});
});
});
describe('onCreate', () => {
beforeEach(() => {
createStore();
createComponent();
});
it('calls fetchGeoSettings', () => {
expect(store.dispatch).toHaveBeenCalledWith('fetchGeoSettings');
}); });
}); });
}); });
import { shallowMount } from '@vue/test-utils'; import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { visitUrl } from '~/lib/utils/url_utility'; import { visitUrl } from '~/lib/utils/url_utility';
import store from 'ee/geo_settings/store';
import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue'; import GeoSettingsForm from 'ee/geo_settings/components/geo_settings_form.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
jest.mock('~/lib/utils/url_utility', () => ({ jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn().mockName('visitUrlMock'), visitUrl: jest.fn().mockName('visitUrlMock'),
})); }));
...@@ -10,7 +15,9 @@ describe('GeoSettingsForm', () => { ...@@ -10,7 +15,9 @@ describe('GeoSettingsForm', () => {
let wrapper; let wrapper;
const createComponent = () => { const createComponent = () => {
wrapper = shallowMount(GeoSettingsForm); wrapper = shallowMount(GeoSettingsForm, {
store,
});
}; };
afterEach(() => { afterEach(() => {
......
export const MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE = {
geo_status_timeout: 10,
geo_node_allowed_ips: '0.0.0.0/0, ::/0',
};
export const MOCK_BASIC_SETTINGS_DATA = {
timeout: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_status_timeout,
allowedIp: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE.geo_node_allowed_ips,
};
import testAction from 'helpers/vuex_action_helper';
import flash from '~/flash';
import Api from 'ee/api';
import * as actions from 'ee/geo_settings/store/actions';
import * as types from 'ee/geo_settings/store/mutation_types';
import state from 'ee/geo_settings/store/state';
import { MOCK_BASIC_SETTINGS_DATA, MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE } from '../mock_data';
jest.mock('~/flash');
describe('GeoSettings Store Actions', () => {
describe('fetchGeoSettings', () => {
describe('on success', () => {
beforeEach(() => {
jest
.spyOn(Api, 'getApplicationSettings')
.mockResolvedValue({ data: MOCK_APPLICATION_SETTINGS_FETCH_RESPONSE });
});
it('should commit the request and success actions', done => {
testAction(
actions.fetchGeoSettings,
{},
state,
[
{ type: types.REQUEST_GEO_SETTINGS },
{ type: types.RECEIVE_GEO_SETTINGS_SUCCESS, payload: MOCK_BASIC_SETTINGS_DATA },
],
[],
done,
);
});
});
describe('on error', () => {
beforeEach(() => {
jest.spyOn(Api, 'getApplicationSettings').mockRejectedValue(new Error(500));
});
it('should commit the request and error actions', () => {
testAction(
actions.fetchGeoSettings,
{},
state,
[{ type: types.REQUEST_GEO_SETTINGS }, { type: types.RECEIVE_GEO_SETTINGS_ERROR }],
[],
() => {
expect(flash).toHaveBeenCalledTimes(1);
},
);
});
});
});
});
import mutations from 'ee/geo_settings/store/mutations';
import createState from 'ee/geo_settings/store/state';
import * as types from 'ee/geo_settings/store/mutation_types';
import { DEFAULT_TIMEOUT, DEFAULT_ALLOWED_IP } from 'ee/geo_settings/constants';
import { MOCK_BASIC_SETTINGS_DATA } from '../mock_data';
describe('GeoSettings Store Mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe('REQUEST_GEO_SETTINGS', () => {
it('sets isLoading to true', () => {
mutations[types.REQUEST_GEO_SETTINGS](state);
expect(state.isLoading).toEqual(true);
});
});
describe('RECEIVE_GEO_SETTINGS_SUCCESS', () => {
const mockData = MOCK_BASIC_SETTINGS_DATA;
beforeEach(() => {
state.isLoading = true;
});
it('sets isLoading to false', () => {
mutations[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, mockData);
expect(state.isLoading).toEqual(false);
});
it('sets timeout and allowedIp array with data', () => {
mutations[types.RECEIVE_GEO_SETTINGS_SUCCESS](state, mockData);
expect(state.timeout).toBe(mockData.timeout);
expect(state.allowedIp).toBe(mockData.allowedIp);
});
});
describe('RECEIVE_GEO_SETTINGS_ERROR', () => {
const mockData = MOCK_BASIC_SETTINGS_DATA;
beforeEach(() => {
state.isLoading = true;
state.timeout = mockData.timeout;
state.allowedIp = mockData.allowedIp;
});
it('sets isLoading to false', () => {
mutations[types.RECEIVE_GEO_SETTINGS_ERROR](state);
expect(state.isLoading).toEqual(false);
});
it('resets timeout and allowedIp array', () => {
mutations[types.RECEIVE_GEO_SETTINGS_ERROR](state);
expect(state.timeout).toBe(DEFAULT_TIMEOUT);
expect(state.allowedIp).toBe(DEFAULT_ALLOWED_IP);
});
});
});
...@@ -22975,6 +22975,9 @@ msgstr "" ...@@ -22975,6 +22975,9 @@ msgstr ""
msgid "There was an error fetching the %{replicableType}" msgid "There was an error fetching the %{replicableType}"
msgstr "" msgstr ""
msgid "There was an error fetching the Geo Settings"
msgstr ""
msgid "There was an error fetching the Node's Groups" msgid "There was an error fetching the Node's Groups"
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