Commit 921bae09 authored by Phil Hughes's avatar Phil Hughes

Merge branch '8621-move-index-vuex-store' into 'master'

Transforms Feature Flag store into a module

See merge request gitlab-org/gitlab-ee!9214
parents e6b5f452 a4db6ef7
<script> <script>
import { mapState, mapActions } from 'vuex'; import { createNamespacedHelpers } from 'vuex';
import _ from 'underscore'; import _ from 'underscore';
import { GlEmptyState, GlLoadingIcon, GlButton } from '@gitlab/ui'; import { GlEmptyState, GlLoadingIcon, GlButton } from '@gitlab/ui';
import FeatureFlagsTable from './feature_flags_table.vue'; import FeatureFlagsTable from './feature_flags_table.vue';
...@@ -13,6 +13,8 @@ import { ...@@ -13,6 +13,8 @@ import {
buildUrlWithCurrentLocation, buildUrlWithCurrentLocation,
} from '~/lib/utils/common_utils'; } from '~/lib/utils/common_utils';
const { mapState, mapActions } = createNamespacedHelpers('index');
export default { export default {
store, store,
components: { components: {
......
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import state from './state'; import indexModule from './modules/index';
import * as actions from './actions';
import mutations from './mutations';
Vue.use(Vuex); Vue.use(Vuex);
export const createStore = () => export const createStore = () =>
new Vuex.Store({ new Vuex.Store({
actions, modules: {
mutations, index: indexModule,
state, },
}); });
export default createStore(); export default createStore();
import * as types from './mutation_types'; import * as types from './mutation_types';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
export const requestFeatureFlags = ({ commit }) => commit(types.REQUEST_FEATURE_FLAGS); export const setFeatureFlagsEndpoint = ({ commit }, endpoint) =>
export const receiveFeatureFlagsSuccess = ({ commit }, response) => commit(types.SET_FEATURE_FLAGS_ENDPOINT, endpoint);
commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response);
export const receiveFeatureFlagsError = ({ commit }, error) => export const setFeatureFlagsOptions = ({ commit }, options) =>
commit(types.RECEIVE_FEATURE_FLAGS_ERROR, error); commit(types.SET_FEATURE_FLAGS_OPTIONS, options);
export const fetchFeatureFlags = ({ state, dispatch }) => { export const fetchFeatureFlags = ({ state, dispatch }) => {
dispatch('requestFeatureFlags'); dispatch('requestFeatureFlags');
...@@ -14,15 +14,19 @@ export const fetchFeatureFlags = ({ state, dispatch }) => { ...@@ -14,15 +14,19 @@ export const fetchFeatureFlags = ({ state, dispatch }) => {
.get(state.endpoint, { .get(state.endpoint, {
params: state.options, params: state.options,
}) })
.then(response => dispatch('receiveFeatureFlagsSuccess', response)) .then(response =>
.catch(error => dispatch('receiveFeatureFlagsError', error)); dispatch('receiveFeatureFlagsSuccess', {
data: response.data || {},
headers: response.headers,
}),
)
.catch(() => dispatch('receiveFeatureFlagsError'));
}; };
export const setFeatureFlagsEndpoint = ({ commit }, endpoint) => export const requestFeatureFlags = ({ commit }) => commit(types.REQUEST_FEATURE_FLAGS);
commit(types.SET_FEATURE_FLAGS_ENDPOINT, endpoint); export const receiveFeatureFlagsSuccess = ({ commit }, response) =>
commit(types.RECEIVE_FEATURE_FLAGS_SUCCESS, response);
export const setFeatureFlagsOptions = ({ commit }, options) => export const receiveFeatureFlagsError = ({ commit }) => commit(types.RECEIVE_FEATURE_FLAGS_ERROR);
commit(types.SET_FEATURE_FLAGS_OPTIONS, options);
// prevent babel-plugin-rewire from generating an invalid default during karma tests // prevent babel-plugin-rewire from generating an invalid default during karma tests
export default () => {}; export default () => {};
import state from './state';
import * as actions from './actions';
import mutations from './mutations';
export default {
namespaced: true,
actions,
mutations,
state: state(),
};
...@@ -13,6 +13,7 @@ export default { ...@@ -13,6 +13,7 @@ export default {
}, },
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) { [types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false; state.isLoading = false;
state.hasError = false;
state.featureFlags = response.data.feature_flags; state.featureFlags = response.data.feature_flags;
state.count = response.data.count; state.count = response.data.count;
......
...@@ -2,13 +2,13 @@ import Vue from 'vue'; ...@@ -2,13 +2,13 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import featureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue'; import featureFlagsComponent from 'ee/feature_flags/components/feature_flags.vue';
import { createStore } from 'ee/feature_flags/store'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mountComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { TEST_HOST } from 'spec/test_constants';
import { featureFlag } from './mock_data'; import { getRequestData } from '../mock_data';
describe('Feature Flags', () => { describe('Feature Flags', () => {
const mockData = { const mockData = {
endpoint: 'feature_flags.json', endpoint: `${TEST_HOST}/endpoint.json`,
csrfToken: 'testToken', csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg', errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags', featureFlagsHelpPagePath: '/help/feature-flags',
...@@ -16,7 +16,6 @@ describe('Feature Flags', () => { ...@@ -16,7 +16,6 @@ describe('Feature Flags', () => {
newFeatureFlagPath: 'feature-flags/new', newFeatureFlagPath: 'feature-flags/new',
}; };
let store;
let FeatureFlagsComponent; let FeatureFlagsComponent;
let component; let component;
let mock; let mock;
...@@ -28,13 +27,13 @@ describe('Feature Flags', () => { ...@@ -28,13 +27,13 @@ describe('Feature Flags', () => {
}); });
afterEach(() => { afterEach(() => {
component.$destroy();
mock.restore(); mock.restore();
component.$destroy();
}); });
describe('without permissions', () => { describe('without permissions', () => {
const props = { const props = {
endpoint: 'feature_flags.json', endpoint: `${TEST_HOST}/endpoint.json`,
csrfToken: 'testToken', csrfToken: 'testToken',
errorStateSvgPath: '/assets/illustrations/feature_flag.svg', errorStateSvgPath: '/assets/illustrations/feature_flag.svg',
featureFlagsHelpPagePath: '/help/feature-flags', featureFlagsHelpPagePath: '/help/feature-flags',
...@@ -42,18 +41,11 @@ describe('Feature Flags', () => { ...@@ -42,18 +41,11 @@ describe('Feature Flags', () => {
}; };
beforeEach(done => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { mock
feature_flags: [], .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: 'all', page: '1' } })
count: { .reply(200, getRequestData, {});
all: 0,
enabled: 0, component = mountComponent(FeatureFlagsComponent, props);
disabled: 0,
},
});
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props,
});
setTimeout(() => { setTimeout(() => {
done(); done();
...@@ -71,19 +63,11 @@ describe('Feature Flags', () => { ...@@ -71,19 +63,11 @@ describe('Feature Flags', () => {
describe('loading state', () => { describe('loading state', () => {
it('renders a loading icon', done => { it('renders a loading icon', done => {
mock.onGet(mockData.endpoint).reply(200, { mock
feature_flags: [], .onGet(`${TEST_HOST}/endpoint.json`, { params: { scope: 'all', page: '1' } })
count: { .replyOnce(200, getRequestData, {});
all: 0,
enabled: 0,
disabled: 0,
},
});
component = mountComponentWithStore(FeatureFlagsComponent, { component = mountComponent(FeatureFlagsComponent, mockData);
store,
props: mockData,
});
const loadingElement = component.$el.querySelector('.js-loading-state'); const loadingElement = component.$el.querySelector('.js-loading-state');
...@@ -101,19 +85,20 @@ describe('Feature Flags', () => { ...@@ -101,19 +85,20 @@ describe('Feature Flags', () => {
describe('successful request', () => { describe('successful request', () => {
describe('without feature flags', () => { describe('without feature flags', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(200, { mock.onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } }).replyOnce(
200,
{
feature_flags: [], feature_flags: [],
count: { count: {
all: 0, all: 0,
enabled: 0, enabled: 0,
disabled: 0, disabled: 0,
}, },
}); },
{},
);
component = mountComponentWithStore(FeatureFlagsComponent, { component = mountComponent(FeatureFlagsComponent, mockData);
store,
props: mockData,
});
setTimeout(() => { setTimeout(() => {
done(); done();
...@@ -135,33 +120,18 @@ describe('Feature Flags', () => { ...@@ -135,33 +120,18 @@ describe('Feature Flags', () => {
describe('with paginated feature flags', () => { describe('with paginated feature flags', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply( mock
200, .onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } })
{ .replyOnce(200, getRequestData, {
feature_flags: [featureFlag], 'x-next-page': '2',
count: {
all: 37,
enabled: 5,
disabled: 32,
},
},
{
'X-nExt-pAge': '2',
'x-page': '1', 'x-page': '1',
'X-Per-Page': '1', 'X-Per-Page': '2',
'X-Prev-Page': '', 'X-Prev-Page': '',
'X-TOTAL': '37', 'X-TOTAL': '37',
'X-Total-Pages': '2', 'X-Total-Pages': '5',
},
);
store = createStore();
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props: mockData,
}); });
component = mountComponent(FeatureFlagsComponent, mockData);
setTimeout(() => { setTimeout(() => {
done(); done();
}, 0); }, 0);
...@@ -170,11 +140,11 @@ describe('Feature Flags', () => { ...@@ -170,11 +140,11 @@ describe('Feature Flags', () => {
it('should render a table with feature flags', () => { it('should render a table with feature flags', () => {
expect(component.$el.querySelectorAll('.js-feature-flag-table')).not.toBeNull(); expect(component.$el.querySelectorAll('.js-feature-flag-table')).not.toBeNull();
expect(component.$el.querySelector('.feature-flag-name').textContent.trim()).toEqual( expect(component.$el.querySelector('.feature-flag-name').textContent.trim()).toEqual(
featureFlag.name, getRequestData.feature_flags[0].name,
); );
expect(component.$el.querySelector('.feature-flag-description').textContent.trim()).toEqual( expect(component.$el.querySelector('.feature-flag-description').textContent.trim()).toEqual(
featureFlag.description, getRequestData.feature_flags[0].description,
); );
}); });
...@@ -188,7 +158,7 @@ describe('Feature Flags', () => { ...@@ -188,7 +158,7 @@ describe('Feature Flags', () => {
describe('pagination', () => { describe('pagination', () => {
it('should render pagination', () => { it('should render pagination', () => {
expect(component.$el.querySelectorAll('.gl-pagination li').length).toEqual(5); expect(component.$el.querySelectorAll('.gl-pagination')).not.toBeNull();
}); });
it('should make an API request when page is clicked', done => { it('should make an API request when page is clicked', done => {
...@@ -198,7 +168,7 @@ describe('Feature Flags', () => { ...@@ -198,7 +168,7 @@ describe('Feature Flags', () => {
expect(component.updateFeatureFlagOptions).toHaveBeenCalledWith({ expect(component.updateFeatureFlagOptions).toHaveBeenCalledWith({
scope: 'all', scope: 'all',
page: '2', page: '4',
}); });
done(); done();
}, 0); }, 0);
...@@ -222,13 +192,9 @@ describe('Feature Flags', () => { ...@@ -222,13 +192,9 @@ describe('Feature Flags', () => {
describe('unsuccessful request', () => { describe('unsuccessful request', () => {
beforeEach(done => { beforeEach(done => {
mock.onGet(mockData.endpoint).reply(500, {}); mock.onGet(mockData.endpoint, { params: { scope: 'all', page: '1' } }).replyOnce(500, {});
store = createStore(); component = mountComponent(FeatureFlagsComponent, mockData);
component = mountComponentWithStore(FeatureFlagsComponent, {
store,
props: mockData,
});
setTimeout(() => { setTimeout(() => {
done(); done();
......
import Vue from 'vue'; import Vue from 'vue';
import featureFlagsTableComponent from 'ee/feature_flags/components/feature_flags_table.vue'; import featureFlagsTableComponent from 'ee/feature_flags/components/feature_flags_table.vue';
import mountComponent from 'spec/helpers/vue_mount_component_helper'; import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { featureFlag } from './mock_data'; import { featureFlag } from '../mock_data';
describe('Feature Flag table', () => { describe('Feature Flag table', () => {
let Component; let Component;
......
...@@ -37,3 +37,46 @@ export const featureFlag = { ...@@ -37,3 +37,46 @@ export const featureFlag = {
}, },
], ],
}; };
export const getRequestData = {
feature_flags: [
{
id: 3,
active: true,
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
name: 'ci_live_trace',
description: 'For the new live trace architecture',
edit_path: '/root/per-environment-feature-flags/-/feature_flags/3/edit',
destroy_path: '/root/per-environment-feature-flags/-/feature_flags/3',
scopes: [
{
id: 1,
active: true,
environment_scope: '*',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
},
{
id: 2,
active: false,
environment_scope: 'production',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
},
{
id: 3,
active: false,
environment_scope: 'review/*',
created_at: '2019-01-14T06:41:40.987Z',
updated_at: '2019-01-14T06:41:40.987Z',
},
],
},
],
count: {
all: 1,
disabled: 1,
enabled: 0,
},
};
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import {
requestFeatureFlags,
receiveFeatureFlagsSuccess,
receiveFeatureFlagsError,
fetchFeatureFlags,
setFeatureFlagsEndpoint,
setFeatureFlagsOptions,
} from 'ee/feature_flags/store/modules/index/actions';
import state from 'ee/feature_flags/store/modules/index/state';
import * as types from 'ee/feature_flags/store/modules/index/mutation_types';
import testAction from 'spec/helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import { getRequestData } from '../../mock_data';
describe('Feature flags actions', () => {
let mockedState;
beforeEach(() => {
mockedState = state();
});
describe('setFeatureFlagsEndpoint', () => {
it('should commit SET_FEATURE_FLAGS_ENDPOINT mutation', done => {
testAction(
setFeatureFlagsEndpoint,
'feature_flags.json',
mockedState,
[{ type: types.SET_FEATURE_FLAGS_ENDPOINT, payload: 'feature_flags.json' }],
[],
done,
);
});
});
describe('setFeatureFlagsOptions', () => {
it('should commit SET_FEATURE_FLAGS_OPTIONS mutation', done => {
testAction(
setFeatureFlagsOptions,
{ page: '1', scope: 'all' },
mockedState,
[{ type: types.SET_FEATURE_FLAGS_OPTIONS, payload: { page: '1', scope: 'all' } }],
[],
done,
);
});
});
describe('fetchFeatureFlags', () => {
let mock;
beforeEach(() => {
mockedState.endpoint = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
});
describe('success', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {});
testAction(
fetchFeatureFlags,
null,
mockedState,
[],
[
{
type: 'requestFeatureFlags',
},
{
payload: { data: getRequestData, headers: {} },
type: 'receiveFeatureFlagsSuccess',
},
],
done,
);
});
});
describe('error', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsError ', done => {
mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
testAction(
fetchFeatureFlags,
null,
mockedState,
[],
[
{
type: 'requestFeatureFlags',
},
{
type: 'receiveFeatureFlagsError',
},
],
done,
);
});
});
});
describe('requestFeatureFlags', () => {
it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', done => {
testAction(
requestFeatureFlags,
null,
mockedState,
[{ type: types.REQUEST_FEATURE_FLAGS }],
[],
done,
);
});
});
describe('receiveFeatureFlagsSuccess', () => {
it('should commit RECEIVE_FEATURE_FLAGS_SUCCESS mutation', done => {
testAction(
receiveFeatureFlagsSuccess,
{ data: getRequestData, headers: {} },
mockedState,
[
{
type: types.RECEIVE_FEATURE_FLAGS_SUCCESS,
payload: { data: getRequestData, headers: {} },
},
],
[],
done,
);
});
});
describe('receiveFeatureFlagsError', () => {
it('should commit RECEIVE_FEATURE_FLAGS_ERROR mutation', done => {
testAction(
receiveFeatureFlagsError,
null,
mockedState,
[{ type: types.RECEIVE_FEATURE_FLAGS_ERROR }],
[],
done,
);
});
});
});
import state from 'ee/feature_flags/store/modules/index/state';
import mutations from 'ee/feature_flags/store/modules/index/mutations';
import * as types from 'ee/feature_flags/store/modules/index/mutation_types';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import { getRequestData } from '../../mock_data';
describe('Feature flags store Mutations', () => {
let stateCopy;
beforeEach(() => {
stateCopy = state();
});
describe('SET_FEATURE_FLAGS_ENDPOINT', () => {
it('should set endpoint', () => {
mutations[types.SET_FEATURE_FLAGS_ENDPOINT](stateCopy, 'feature_flags.json');
expect(stateCopy.endpoint).toEqual('feature_flags.json');
});
});
describe('SET_FEATURE_FLAGS_OPTIONS', () => {
it('should set provided options', () => {
mutations[types.SET_FEATURE_FLAGS_OPTIONS](stateCopy, { page: '1', scope: 'all' });
expect(stateCopy.options).toEqual({ page: '1', scope: 'all' });
});
});
describe('REQUEST_FEATURE_FLAGS', () => {
it('should set isLoading to true', () => {
mutations[types.REQUEST_FEATURE_FLAGS](stateCopy);
expect(stateCopy.isLoading).toEqual(true);
});
});
describe('RECEIVE_FEATURE_FLAGS_SUCCESS', () => {
const headers = {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
'X-Prev-Page': '',
'X-TOTAL': '37',
'X-Total-Pages': '5',
};
beforeEach(() => {
mutations[types.RECEIVE_FEATURE_FLAGS_SUCCESS](stateCopy, { data: getRequestData, headers });
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to false', () => {
expect(stateCopy.hasError).toEqual(false);
});
it('should set featureFlags with the given data', () => {
expect(stateCopy.featureFlags).toEqual(getRequestData.feature_flags);
});
it('should set count with the given data', () => {
expect(stateCopy.count).toEqual(getRequestData.count);
});
it('should set pagination', () => {
expect(stateCopy.pageInfo).toEqual(parseIntPagination(normalizeHeaders(headers)));
});
});
describe('RECEIVE_FEATURE_FLAGS_ERROR', () => {
beforeEach(() => {
mutations[types.RECEIVE_FEATURE_FLAGS_ERROR](stateCopy);
});
it('should set isLoading to false', () => {
expect(stateCopy.isLoading).toEqual(false);
});
it('should set hasError to true', () => {
expect(stateCopy.hasError).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