Commit a9ac7c25 authored by Andrei Stoicescu's avatar Andrei Stoicescu Committed by Miguel Rincon

Add billable seats to VueX state

  - add a VueX module that handles billable seats
parent 803d87fe
...@@ -68,6 +68,7 @@ const Api = { ...@@ -68,6 +68,7 @@ const Api = {
usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users', usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
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',
billableGroupMembersPath: '/api/:version/groups/:id/billable_members',
group(groupId, callback = () => {}) { group(groupId, callback = () => {}) {
const url = Api.buildUrl(Api.groupPath).replace(':id', groupId); const url = Api.buildUrl(Api.groupPath).replace(':id', groupId);
...@@ -756,6 +757,26 @@ const Api = { ...@@ -756,6 +757,26 @@ const Api = {
return axios.delete(url); return axios.delete(url);
}, },
fetchBillableGroupMembersList(namespaceId, options = {}, callback = () => {}) {
const url = Api.buildUrl(this.billableGroupMembersPath).replace(':id', namespaceId);
const defaults = {
per_page: DEFAULT_PER_PAGE,
page: 1,
};
return axios
.get(url, {
params: {
...defaults,
...options,
},
})
.then(({ data, headers }) => {
callback(data);
return { data, headers };
});
},
}; };
export default Api; export default Api;
export const TABLE_TYPE_DEFAULT = 'default'; export const TABLE_TYPE_DEFAULT = 'default';
export const TABLE_TYPE_FREE = 'free'; export const TABLE_TYPE_FREE = 'free';
export const TABLE_TYPE_TRIAL = 'trial'; export const TABLE_TYPE_TRIAL = 'trial';
// Billable Seats HTTP headers
export const HEADER_TOTAL_ENTRIES = 'x-total';
export const HEADER_PAGE_NUMBER = 'x-page';
export const HEADER_ITEMS_PER_PAGE = 'x-per-page';
import Vue from 'vue'; import Vue from 'vue';
import Vuex from 'vuex'; import Vuex from 'vuex';
import subscription from './modules/subscription/index'; import subscription from './modules/subscription/index';
import seats from './modules/seats/index';
Vue.use(Vuex); Vue.use(Vuex);
...@@ -8,5 +9,6 @@ export default () => ...@@ -8,5 +9,6 @@ export default () =>
new Vuex.Store({ new Vuex.Store({
modules: { modules: {
subscription, subscription,
seats,
}, },
}); });
import Api from '~/api';
import * as types from './mutation_types';
import createFlash from '~/flash';
import { s__ } from '~/locale';
export const setNamespaceId = ({ commit }, namespaceId) => {
commit(types.SET_NAMESPACE_ID, namespaceId);
};
export const fetchBillableMembersList = ({ dispatch, state }, page) => {
dispatch('requestBillableMembersList');
return Api.fetchBillableGroupMembersList(state.namespaceId, { page })
.then(data => dispatch('receiveBillableMembersListSuccess', data))
.catch(() => dispatch('receiveBillableMembersListError'));
};
export const requestBillableMembersList = ({ commit }) => commit(types.REQUEST_BILLABLE_MEMBERS);
export const receiveBillableMembersListSuccess = ({ commit }, response) =>
commit(types.RECEIVE_BILLABLE_MEMBERS_SUCCESS, response);
export const receiveBillableMembersListError = ({ commit }) => {
createFlash({
message: s__('Billing|An error occurred while loading billable members list'),
});
commit(types.RECEIVE_BILLABLE_MEMBERS_ERROR);
};
import * as actions from './actions';
import mutations from './mutations';
import state from './state';
export default {
namespaced: true,
actions,
mutations,
state,
};
export const SET_NAMESPACE_ID = 'SET_NAMESPACE_ID';
export const REQUEST_BILLABLE_MEMBERS = 'REQUEST_BILLABLE_MEMBERS';
export const RECEIVE_BILLABLE_MEMBERS_SUCCESS = 'RECEIVE_BILLABLE_MEMBERS_SUCCESS';
export const RECEIVE_BILLABLE_MEMBERS_ERROR = 'RECEIVE_BILLABLE_MEMBERS_ERROR';
import * as types from './mutation_types';
import {
HEADER_TOTAL_ENTRIES,
HEADER_PAGE_NUMBER,
HEADER_ITEMS_PER_PAGE,
} from '../../../constants';
export default {
[types.SET_NAMESPACE_ID](state, payload) {
state.namespaceId = payload;
},
[types.REQUEST_BILLABLE_MEMBERS](state) {
state.isLoading = true;
state.hasError = false;
},
[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, payload) {
const { data, headers } = payload;
state.members = data;
state.total = headers[HEADER_TOTAL_ENTRIES];
state.page = headers[HEADER_PAGE_NUMBER];
state.perPage = headers[HEADER_ITEMS_PER_PAGE];
state.isLoading = false;
},
[types.RECEIVE_BILLABLE_MEMBERS_ERROR](state) {
state.isLoading = false;
state.hasError = true;
},
};
export default () => ({
isLoading: false,
hasError: false,
namespaceId: null,
members: [],
total: null,
page: null,
perPage: null,
});
...@@ -869,4 +869,24 @@ describe('Api', () => { ...@@ -869,4 +869,24 @@ describe('Api', () => {
}); });
}); });
}); });
describe('Billable members list', () => {
let expectedUrl;
let namespaceId;
beforeEach(() => {
namespaceId = 1000;
expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${namespaceId}/billable_members`;
});
describe('fetchBillableGroupMembersList', () => {
it('GETs the right url', () => {
mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
return Api.fetchBillableGroupMembersList(namespaceId).then(({ data }) => {
expect(data).toEqual([]);
});
});
});
});
}); });
...@@ -5,7 +5,7 @@ import createStore from 'ee/billings/stores'; ...@@ -5,7 +5,7 @@ import createStore from 'ee/billings/stores';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import SubscriptionTable from 'ee/billings/components/subscription_table.vue'; import SubscriptionTable from 'ee/billings/components/subscription_table.vue';
import SubscriptionTableRow from 'ee/billings/components/subscription_table_row.vue'; import SubscriptionTableRow from 'ee/billings/components/subscription_table_row.vue';
import mockDataSubscription from '../mock_data'; import { mockDataSubscription } from '../mock_data';
const TEST_NAMESPACE_NAME = 'GitLab.com'; const TEST_NAMESPACE_NAME = 'GitLab.com';
const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions'; const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions';
......
export default { import {
HEADER_TOTAL_ENTRIES,
HEADER_PAGE_NUMBER,
HEADER_ITEMS_PER_PAGE,
} from 'ee/billings/constants';
export const mockDataSubscription = {
gold: { gold: {
plan: { plan: {
name: 'Gold', name: 'Gold',
...@@ -58,3 +64,40 @@ export default { ...@@ -58,3 +64,40 @@ export default {
}, },
}, },
}; };
export const mockDataSeats = {
data: [
{
id: 1,
name: 'Administrator',
username: 'root',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://192.168.1.209:3001/root',
},
{
id: 3,
name: 'Agustin Walker',
username: 'lester.orn',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/772352aed294c4b3e6f236b0624764b6?s=80\u0026d=identicon',
web_url: 'http://192.168.1.209:3001/lester.orn',
},
{
id: 5,
name: 'Joella Miller',
username: 'era',
state: 'active',
avatar_url:
'https://www.gravatar.com/avatar/8b306a0c173657865f6a5a6c7120b408?s=80\u0026d=identicon',
web_url: 'http://192.168.1.209:3001/era',
},
],
headers: {
[HEADER_TOTAL_ENTRIES]: '3',
[HEADER_PAGE_NUMBER]: '1',
[HEADER_ITEMS_PER_PAGE]: '1',
},
};
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import state from 'ee/billings/stores/modules/seats/state';
import * as types from 'ee/billings/stores/modules/seats/mutation_types';
import * as actions from 'ee/billings/stores/modules/seats/actions';
import axios from '~/lib/utils/axios_utils';
import createFlash from '~/flash';
import { mockDataSeats } from '../../../mock_data';
jest.mock('~/flash');
describe('seats actions', () => {
let mockedState;
let mock;
beforeEach(() => {
mockedState = state();
mock = new MockAdapter(axios);
});
afterEach(() => {
mock.restore();
createFlash.mockClear();
});
describe('setNamespaceId', () => {
it('should commit the correct mutuation', () => {
const namespaceId = 1;
testAction(
actions.setNamespaceId,
namespaceId,
mockedState,
[
{
type: types.SET_NAMESPACE_ID,
payload: namespaceId,
},
],
[],
);
});
});
describe('fetchBillableMembersList', () => {
beforeEach(() => {
gon.api_version = 'v4';
mockedState.namespaceId = 1;
});
describe('on success', () => {
beforeEach(() => {
mock
.onGet('/api/v4/groups/1/billable_members')
.replyOnce(200, mockDataSeats.data, mockDataSeats.headers);
});
it('should dispatch the request and success actions', () => {
testAction(
actions.fetchBillableMembersList,
{},
mockedState,
[],
[
{ type: 'requestBillableMembersList' },
{
type: 'receiveBillableMembersListSuccess',
payload: mockDataSeats,
},
],
);
});
});
describe('on error', () => {
beforeEach(() => {
mock.onGet('/api/v4/groups/1/billable_members').replyOnce(404, {});
});
it('should dispatch the request and error actions', () => {
testAction(
actions.fetchBillableMembersList,
{},
mockedState,
[],
[{ type: 'requestBillableMembersList' }, { type: 'receiveBillableMembersListError' }],
);
});
});
});
describe('requestBillableMembersList', () => {
it('should commit the request mutation', () => {
testAction(
actions.requestBillableMembersList,
{},
state,
[{ type: types.REQUEST_BILLABLE_MEMBERS }],
[],
);
});
});
describe('receiveBillableMembersListSuccess', () => {
it('should commit the success mutation', () => {
testAction(
actions.receiveBillableMembersListSuccess,
mockDataSeats,
mockedState,
[
{
type: types.RECEIVE_BILLABLE_MEMBERS_SUCCESS,
payload: mockDataSeats,
},
],
[],
);
});
});
describe('receiveBillableMembersListError', () => {
it('should commit the error mutation', done => {
testAction(
actions.receiveBillableMembersListError,
{},
mockedState,
[{ type: types.RECEIVE_BILLABLE_MEMBERS_ERROR }],
[],
() => {
expect(createFlash).toHaveBeenCalled();
done();
},
);
});
});
});
import createState from 'ee/billings/stores/modules/seats/state';
import * as types from 'ee/billings/stores/modules/seats/mutation_types';
import mutations from 'ee/billings/stores/modules/seats/mutations';
import { mockDataSeats } from '../../../mock_data';
describe('EE billings seats module mutations', () => {
let state;
beforeEach(() => {
state = createState();
});
describe(types.SET_NAMESPACE_ID, () => {
it('sets namespaceId', () => {
const expectedNamespaceId = 'test';
expect(state.namespaceId).toBeNull();
mutations[types.SET_NAMESPACE_ID](state, expectedNamespaceId);
expect(state.namespaceId).toEqual(expectedNamespaceId);
});
});
describe(types.REQUEST_BILLABLE_MEMBERS, () => {
beforeEach(() => {
mutations[types.REQUEST_BILLABLE_MEMBERS](state);
});
it('sets isLoading to true', () => {
expect(state.isLoading).toBeTruthy();
});
it('sets hasError to false', () => {
expect(state.hasError).toBeFalsy();
});
});
describe(types.RECEIVE_BILLABLE_MEMBERS_SUCCESS, () => {
beforeEach(() => {
mutations[types.RECEIVE_BILLABLE_MEMBERS_SUCCESS](state, mockDataSeats);
});
it('sets state as expected', () => {
expect(state.total).toBe('3');
expect(state.page).toBe('1');
expect(state.perPage).toBe('1');
});
it('sets isLoading to false', () => {
expect(state.isLoading).toBeFalsy();
});
});
describe(types.RECEIVE_BILLABLE_MEMBERS_ERROR, () => {
beforeEach(() => {
mutations[types.RECEIVE_BILLABLE_MEMBERS_ERROR](state);
});
it('sets isLoading to false', () => {
expect(state.isLoading).toBeFalsy();
});
it('sets hasError to true', () => {
expect(state.hasError).toBeTruthy();
});
});
});
...@@ -6,7 +6,7 @@ import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; ...@@ -6,7 +6,7 @@ import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import * as actions from 'ee/billings/stores/modules/subscription/actions'; import * as actions from 'ee/billings/stores/modules/subscription/actions';
import axios from '~/lib/utils/axios_utils'; import axios from '~/lib/utils/axios_utils';
import mockDataSubscription from '../../../mock_data'; import { mockDataSubscription } from '../../../mock_data';
describe('subscription actions', () => { describe('subscription actions', () => {
let mockedState; let mockedState;
......
...@@ -3,7 +3,7 @@ import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; ...@@ -3,7 +3,7 @@ import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import mutations from 'ee/billings/stores/modules/subscription/mutations'; import mutations from 'ee/billings/stores/modules/subscription/mutations';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from 'ee/billings/constants'; import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from 'ee/billings/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockSubscriptionData from '../../../mock_data'; import { mockDataSubscription } from '../../../mock_data';
describe('EE billings subscription module mutations', () => { describe('EE billings subscription module mutations', () => {
let state; let state;
...@@ -52,9 +52,9 @@ describe('EE billings subscription module mutations', () => { ...@@ -52,9 +52,9 @@ describe('EE billings subscription module mutations', () => {
describe.each` describe.each`
desc | subscription | tableKey desc | subscription | tableKey
${'with Gold subscription'} | ${mockSubscriptionData.gold} | ${TABLE_TYPE_DEFAULT} ${'with Gold subscription'} | ${mockDataSubscription.gold} | ${TABLE_TYPE_DEFAULT}
${'with Free plan'} | ${mockSubscriptionData.free} | ${TABLE_TYPE_FREE} ${'with Free plan'} | ${mockDataSubscription.free} | ${TABLE_TYPE_FREE}
${'with Gold trial'} | ${mockSubscriptionData.trial} | ${TABLE_TYPE_TRIAL} ${'with Gold trial'} | ${mockDataSubscription.trial} | ${TABLE_TYPE_TRIAL}
`('$desc', ({ subscription, tableKey }) => { `('$desc', ({ subscription, tableKey }) => {
beforeEach(() => { beforeEach(() => {
state.isLoading = true; state.isLoading = true;
......
...@@ -4131,6 +4131,9 @@ msgstr "" ...@@ -4131,6 +4131,9 @@ msgstr ""
msgid "BillingPlan|Upgrade" msgid "BillingPlan|Upgrade"
msgstr "" msgstr ""
msgid "Billing|An error occurred while loading billable members list"
msgstr ""
msgid "Bitbucket Server Import" msgid "Bitbucket Server Import"
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