Commit 554d612a authored by Vitaly Slobodin's avatar Vitaly Slobodin

Merge branch 'astoicescu-seats-table-01' into 'master'

Add "seats in use" table to billings page

See merge request gitlab-org/gitlab!44489
parents c958ac18 831dadd8
...@@ -9,11 +9,15 @@ table { ...@@ -9,11 +9,15 @@ table {
* This is a temporary workaround until we fix the neutral * This is a temporary workaround until we fix the neutral
* color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570 * color palette in https://gitlab.com/gitlab-org/gitlab/-/issues/213570
* *
* The overwrites here affected the security dashboard tables, when removing * The overwrites here affected the following areas:
* this code, table-th-transparent and original-text-color classes should * - The security dashboard tables. When removing
* be removed there. * this code, table-th-transparent and original-text-color classes should
* be removed there.
* - The subscription seats table. When removing this code, the .seats-table
* <th> and margin overrides should be removed there.
* *
* Remove this code as soon as this happens * Remove this code as soon as this happens
*
*/ */
&.gl-table { &.gl-table {
@include gl-text-gray-500; @include gl-text-gray-500;
......
<script> <script>
import { mapActions } from 'vuex'; import { mapActions, mapState } from 'vuex';
import SubscriptionTable from './subscription_table.vue'; import SubscriptionTable from './subscription_table.vue';
import SubscriptionSeats from './subscription_seats.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default { export default {
name: 'SubscriptionApp', name: 'SubscriptionApp',
components: { components: {
SubscriptionTable, SubscriptionTable,
SubscriptionSeats,
}, },
mixins: [glFeatureFlagsMixin()],
props: { props: {
planUpgradeHref: { planUpgradeHref: {
type: String, type: String,
...@@ -28,19 +32,38 @@ export default { ...@@ -28,19 +32,38 @@ export default {
required: true, required: true,
}, },
}, },
computed: {
...mapState('subscription', ['hasBillableGroupMembers']),
isFeatureFlagEnabled() {
return this.glFeatures?.apiBillableMemberList;
},
},
created() { created() {
this.setNamespaceId(this.namespaceId); this.setNamespaceId(this.namespaceId);
if (this.isFeatureFlagEnabled) {
this.fetchHasBillableGroupMembers();
}
}, },
methods: { methods: {
...mapActions('subscription', ['setNamespaceId']), ...mapActions('subscription', ['setNamespaceId', 'fetchHasBillableGroupMembers']),
}, },
}; };
</script> </script>
<template> <template>
<subscription-table <div>
:namespace-name="namespaceName" <subscription-table
:plan-upgrade-href="planUpgradeHref" :namespace-name="namespaceName"
:customer-portal-url="customerPortalUrl" :plan-upgrade-href="planUpgradeHref"
/> :customer-portal-url="customerPortalUrl"
/>
<subscription-seats
v-if="isFeatureFlagEnabled && hasBillableGroupMembers"
:namespace-name="namespaceName"
:namespace-id="namespaceId"
class="gl-mt-7"
/>
</div>
</template> </template>
<script>
import { mapActions, mapState } from 'vuex';
import { GlTable, GlAvatarLabeled, GlAvatarLink, GlPagination, GlLoadingIcon } from '@gitlab/ui';
import { parseInt } from 'lodash';
import { s__, sprintf } from '~/locale';
const AVATAR_SIZE = 32;
export default {
components: {
GlTable,
GlAvatarLabeled,
GlAvatarLink,
GlPagination,
GlLoadingIcon,
},
props: {
namespaceName: {
type: String,
required: true,
},
namespaceId: {
type: String,
required: true,
},
},
data() {
return {
fields: ['user'],
};
},
computed: {
...mapState('seats', ['members', 'isLoading', 'page', 'perPage', 'total']),
items() {
return this.members.map(({ name, username, avatar_url, web_url }) => {
const formattedUserName = `@${username}`;
return { user: { name, username: formattedUserName, avatar_url, web_url } };
});
},
headingText() {
return sprintf(s__('Billing|Users occupying seats in %{namespaceName} Group (%{total})'), {
total: this.total,
namespaceName: this.namespaceName,
});
},
subHeadingText() {
return s__('Billing|Updated live');
},
currentPage: {
get() {
return parseInt(this.page, 10);
},
set(val) {
this.fetchBillableMembersList(val);
},
},
perPageFormatted() {
return parseInt(this.perPage, 10);
},
totalFormatted() {
return parseInt(this.total, 10);
},
},
created() {
this.setNamespaceId(this.namespaceId);
this.fetchBillableMembersList(1);
},
methods: {
...mapActions('seats', ['setNamespaceId', 'fetchBillableMembersList']),
inputHandler(val) {
this.fetchBillableMembersList(val);
},
},
avatarSize: AVATAR_SIZE,
};
</script>
<template>
<div>
<h4 data-testid="heading">{{ headingText }}</h4>
<p>{{ subHeadingText }}</p>
<gl-table
data-testid="seats-table"
class="seats-table"
:items="items"
:fields="fields"
:busy="isLoading"
:show-empty="true"
>
<template #cell(user)="data">
<gl-avatar-link target="blank" :href="data.value.web_url" :alt="data.value.name">
<gl-avatar-labeled
:src="data.value.avatar_url"
:size="$options.avatarSize"
:label="data.value.name"
:sub-label="data.value.username"
/>
</gl-avatar-link>
</template>
<template #empty>
{{ s__('Billing|No users to display.') }}
</template>
<template #table-busy>
<gl-loading-icon size="lg" color="dark" class="gl-mt-5" />
</template>
</gl-table>
<gl-pagination
v-if="currentPage"
v-model="currentPage"
:per-page="perPageFormatted"
:total-items="totalFormatted"
align="center"
class="gl-mt-5"
/>
</div>
</template>
...@@ -29,7 +29,13 @@ export default { ...@@ -29,7 +29,13 @@ export default {
}, },
}, },
computed: { computed: {
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'tables', 'endpoint']), ...mapState('subscription', [
'isLoadingSubscription',
'hasErrorSubscription',
'plan',
'tables',
'endpoint',
]),
...mapGetters('subscription', ['isFreePlan']), ...mapGetters('subscription', ['isFreePlan']),
subscriptionHeader() { subscriptionHeader() {
const planName = this.isFreePlan ? s__('SubscriptionTable|Free') : escape(this.plan.name); const planName = this.isFreePlan ? s__('SubscriptionTable|Free') : escape(this.plan.name);
...@@ -85,7 +91,7 @@ export default { ...@@ -85,7 +91,7 @@ export default {
<template> <template>
<div> <div>
<div <div
v-if="!isLoading && !hasError" v-if="!isLoadingSubscription && !hasErrorSubscription"
class="card gl-mt-3 subscription-table js-subscription-table" class="card gl-mt-3 subscription-table js-subscription-table"
> >
<div class="js-subscription-header card-header"> <div class="js-subscription-header card-header">
...@@ -115,7 +121,7 @@ export default { ...@@ -115,7 +121,7 @@ export default {
</div> </div>
<gl-loading-icon <gl-loading-icon
v-else-if="isLoading && !hasError" v-else-if="isLoadingSubscription && !hasErrorSubscription"
:label="s__('SubscriptionTable|Loading subscriptions')" :label="s__('SubscriptionTable|Loading subscriptions')"
size="lg" size="lg"
class="gl-mt-3 gl-mb-3" class="gl-mt-3 gl-mb-3"
......
import API from 'ee/api'; import ApiEe from 'ee/api';
import Api from '~/api';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { deprecatedCreateFlash as createFlash } from '~/flash'; import createFlash from '~/flash';
import { __ } from '~/locale'; import { s__ } from '~/locale';
/**
* SUBSCRIPTION TABLE
*/
export const setNamespaceId = ({ commit }, namespaceId) => { export const setNamespaceId = ({ commit }, namespaceId) => {
commit(types.SET_NAMESPACE_ID, namespaceId); commit(types.SET_NAMESPACE_ID, namespaceId);
}; };
/**
* Subscription Table
*/
export const fetchSubscription = ({ dispatch, state }) => { export const fetchSubscription = ({ dispatch, state }) => {
dispatch('requestSubscription'); dispatch('requestSubscription');
return API.userSubscription(state.namespaceId) return ApiEe.userSubscription(state.namespaceId)
.then(({ data }) => dispatch('receiveSubscriptionSuccess', data)) .then(({ data }) => dispatch('receiveSubscriptionSuccess', data))
.catch(() => dispatch('receiveSubscriptionError')); .catch(() => dispatch('receiveSubscriptionError'));
}; };
...@@ -24,6 +25,32 @@ export const receiveSubscriptionSuccess = ({ commit }, response) => ...@@ -24,6 +25,32 @@ export const receiveSubscriptionSuccess = ({ commit }, response) =>
commit(types.RECEIVE_SUBSCRIPTION_SUCCESS, response); commit(types.RECEIVE_SUBSCRIPTION_SUCCESS, response);
export const receiveSubscriptionError = ({ commit }) => { export const receiveSubscriptionError = ({ commit }) => {
createFlash(__('An error occurred while loading the subscription details.')); createFlash({
message: s__('SubscriptionTable|An error occurred while loading the subscription details.'),
});
commit(types.RECEIVE_SUBSCRIPTION_ERROR); commit(types.RECEIVE_SUBSCRIPTION_ERROR);
}; };
/**
* Billable Members
*/
export const fetchHasBillableGroupMembers = ({ dispatch, state }) => {
dispatch('requestHasBillableGroupMembers');
return Api.fetchBillableGroupMembersList(state.namespaceId, { per_page: 1, page: 1 })
.then(data => dispatch('receiveHasBillableGroupMembersSuccess', data))
.catch(() => dispatch('receiveHasBillableGroupMembersError'));
};
export const requestHasBillableGroupMembers = ({ commit }) =>
commit(types.REQUEST_HAS_BILLABLE_MEMBERS);
export const receiveHasBillableGroupMembersSuccess = ({ commit }, response) =>
commit(types.RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS, response);
export const receiveHasBillableGroupMembersError = ({ commit }) => {
createFlash({
message: s__('SubscriptionTable|An error occurred while loading billable members list'),
});
commit(types.RECEIVE_HAS_BILLABLE_MEMBERS_ERROR);
};
export const SET_NAMESPACE_ID = 'SET_NAMESPACE_ID'; export const SET_NAMESPACE_ID = 'SET_NAMESPACE_ID';
export const REQUEST_SUBSCRIPTION = 'REQUEST_SUBSCRIPTION'; export const REQUEST_SUBSCRIPTION = 'REQUEST_SUBSCRIPTION';
export const RECEIVE_SUBSCRIPTION_SUCCESS = 'RECEIVE_SUBSCRIPTION_SUCCESS'; export const RECEIVE_SUBSCRIPTION_SUCCESS = 'RECEIVE_SUBSCRIPTION_SUCCESS';
export const RECEIVE_SUBSCRIPTION_ERROR = 'RECEIVE_SUBSCRIPTION_ERROR'; export const RECEIVE_SUBSCRIPTION_ERROR = 'RECEIVE_SUBSCRIPTION_ERROR';
export const REQUEST_HAS_BILLABLE_MEMBERS = 'REQUEST_HAS_BILLABLE_MEMBERS';
export const RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS = 'RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS';
export const RECEIVE_HAS_BILLABLE_MEMBERS_ERROR = 'RECEIVE_HAS_BILLABLE_MEMBERS_ERROR';
import Vue from 'vue'; import Vue from 'vue';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from '../../../constants'; import { parseInt } from 'lodash';
import {
TABLE_TYPE_DEFAULT,
TABLE_TYPE_FREE,
TABLE_TYPE_TRIAL,
HEADER_TOTAL_ENTRIES,
} from 'ee/billings/constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
...@@ -9,8 +15,8 @@ export default { ...@@ -9,8 +15,8 @@ export default {
}, },
[types.REQUEST_SUBSCRIPTION](state) { [types.REQUEST_SUBSCRIPTION](state) {
state.isLoading = true; state.isLoadingSubscription = true;
state.hasError = false; state.hasErrorSubscription = false;
}, },
[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) { [types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) {
...@@ -36,11 +42,29 @@ export default { ...@@ -36,11 +42,29 @@ export default {
}); });
}); });
state.isLoading = false; state.isLoadingSubscription = false;
}, },
[types.RECEIVE_SUBSCRIPTION_ERROR](state) { [types.RECEIVE_SUBSCRIPTION_ERROR](state) {
state.isLoading = false; state.isLoadingSubscription = false;
state.hasError = true; state.hasErrorSubscription = true;
},
[types.REQUEST_HAS_BILLABLE_MEMBERS](state) {
state.isLoadingHasBillableMembers = true;
state.hasErrorHasBillableMembers = false;
},
[types.RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS](state, payload) {
const { headers } = payload;
const hasBillableGroupMembers = parseInt(headers[HEADER_TOTAL_ENTRIES], 10) > 0;
state.hasBillableGroupMembers = hasBillableGroupMembers;
state.isLoadingHasBillableMembers = false;
},
[types.RECEIVE_HAS_BILLABLE_MEMBERS_ERROR](state) {
state.isLoadinggHasBillableMembers = false;
state.hasErrorHasBillableMembers = true;
}, },
}; };
import { s__ } from '~/locale'; import { s__ } from '~/locale';
export default () => ({ export default () => ({
isLoading: false, isLoadingSubscription: false,
hasError: false, hasErrorSubscription: false,
isLoadingBillableMembers: false,
hasErrorBillableMembers: false,
namespaceId: null, namespaceId: null,
plan: { plan: {
code: null, code: null,
...@@ -167,4 +169,5 @@ export default () => ({ ...@@ -167,4 +169,5 @@ export default () => ({
], ],
}, },
}, },
hasBillableGroupMembers: false,
}); });
...@@ -107,3 +107,37 @@ ...@@ -107,3 +107,37 @@
} }
} }
} }
.seats-table {
/*
* TODO
* Remove these overrides when the ones inside of
* app/assets/stylesheets/framework/tables.scss
* will be removed.
*
* Read more about this in the comments of that file.
*/
&.table.gl-table {
@include gl-mb-0;
@include gl-text-gray-500;
tr {
th,
td {
@include gl-display-flex;
@include gl-border-b-solid;
@include gl-border-b-1;
@include gl-p-5;
}
th {
@include gl-border-gray-200;
@include gl-bg-transparent;
}
td {
@include gl-border-gray-100;
}
}
}
}
...@@ -4,6 +4,10 @@ class Groups::BillingsController < Groups::ApplicationController ...@@ -4,6 +4,10 @@ class Groups::BillingsController < Groups::ApplicationController
before_action :authorize_admin_group! before_action :authorize_admin_group!
before_action :verify_namespace_plan_check_enabled before_action :verify_namespace_plan_check_enabled
before_action only: [:index] do
push_frontend_feature_flag(:api_billable_member_list)
end
layout 'group_settings' layout 'group_settings'
feature_category :purchase feature_category :purchase
......
...@@ -73,7 +73,7 @@ module BillingPlansHelper ...@@ -73,7 +73,7 @@ module BillingPlansHelper
def seats_data_last_update_info def seats_data_last_update_info
last_enqueue_time = UpdateMaxSeatsUsedForGitlabComSubscriptionsWorker.last_enqueue_time&.utc last_enqueue_time = UpdateMaxSeatsUsedForGitlabComSubscriptionsWorker.last_enqueue_time&.utc
return _("Seats usage data as of %{last_enqueue_time}" % { last_enqueue_time: last_enqueue_time }) if last_enqueue_time return _("Seats usage data as of %{last_enqueue_time} (Updated daily)" % { last_enqueue_time: last_enqueue_time }) if last_enqueue_time
_('Seats usage data is updated every day at 12:00pm UTC') _('Seats usage data is updated every day at 12:00pm UTC')
end end
......
...@@ -10,7 +10,7 @@ ...@@ -10,7 +10,7 @@
= render 'shared/billings/billing_plan', namespace: namespace, plan: plan, current_plan: current_plan = render 'shared/billings/billing_plan', namespace: namespace, plan: plan, current_plan: current_plan
- if namespace.actual_plan&.paid? - if namespace.actual_plan&.paid?
.center .center.gl-mb-7
&= s_('BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}.').html_safe % { support_link_start: support_link_start, support_link_end: support_link_end } &= s_('BillingPlans|If you would like to downgrade your plan please contact %{support_link_start}Customer Support%{support_link_end}.').html_safe % { support_link_start: support_link_start, support_link_end: support_link_end }
%p= seats_data_last_update_info %p= seats_data_last_update_info
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SubscriptionApp component on creation matches the snapshot 1`] = `
<subscription-table-stub
customerportalurl="https://customers.gitlab.com/subscriptions"
namespacename="bronze"
planupgradehref="/url"
/>
`;
...@@ -2,6 +2,9 @@ import { shallowMount } from '@vue/test-utils'; ...@@ -2,6 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import createStore from 'ee/billings/stores'; import createStore from 'ee/billings/stores';
import SubscriptionApp from 'ee/billings/components/app.vue'; import SubscriptionApp from 'ee/billings/components/app.vue';
import SubscriptionTable from 'ee/billings/components/subscription_table.vue'; import SubscriptionTable from 'ee/billings/components/subscription_table.vue';
import SubscriptionSeats from 'ee/billings/components/subscription_seats.vue';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types';
import { mockDataSeats } from '../mock_data';
describe('SubscriptionApp component', () => { describe('SubscriptionApp component', () => {
let store; let store;
...@@ -14,13 +17,16 @@ describe('SubscriptionApp component', () => { ...@@ -14,13 +17,16 @@ describe('SubscriptionApp component', () => {
customerPortalUrl: 'https://customers.gitlab.com/subscriptions', customerPortalUrl: 'https://customers.gitlab.com/subscriptions',
}; };
const factory = (props = appProps) => { const factory = (props = appProps, isFeatureEnabledApiBillableMemberList = true) => {
store = createStore(); store = createStore();
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(SubscriptionApp, { wrapper = shallowMount(SubscriptionApp, {
store, store,
propsData: { ...props }, propsData: { ...props },
provide: {
glFeatures: { apiBillableMemberList: isFeatureEnabledApiBillableMemberList },
},
}); });
}; };
...@@ -31,6 +37,8 @@ describe('SubscriptionApp component', () => { ...@@ -31,6 +37,8 @@ describe('SubscriptionApp component', () => {
expect(componentWrapper.props()).toEqual(expect.objectContaining(props)); expect(componentWrapper.props()).toEqual(expect.objectContaining(props));
}; };
const findSubscriptionSeatsTable = () => wrapper.find(SubscriptionSeats);
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
}); });
...@@ -38,18 +46,16 @@ describe('SubscriptionApp component', () => { ...@@ -38,18 +46,16 @@ describe('SubscriptionApp component', () => {
describe('on creation', () => { describe('on creation', () => {
beforeEach(() => { beforeEach(() => {
factory(); factory();
store.commit(`subscription/${types.RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS}`, mockDataSeats);
}); });
it('dispatches the setNamespaceId on mounted', () => { it('dispatches expected actions on created', () => {
expect(store.dispatch.mock.calls).toEqual([ expect(store.dispatch.mock.calls).toEqual([
['subscription/setNamespaceId', appProps.namespaceId], ['subscription/setNamespaceId', '42'],
['subscription/fetchHasBillableGroupMembers', undefined],
]); ]);
}); });
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
});
it('passes the correct props to the subscriptions table', () => { it('passes the correct props to the subscriptions table', () => {
expectComponentWithProps(SubscriptionTable, { expectComponentWithProps(SubscriptionTable, {
namespaceName: appProps.namespaceName, namespaceName: appProps.namespaceName,
...@@ -57,5 +63,43 @@ describe('SubscriptionApp component', () => { ...@@ -57,5 +63,43 @@ describe('SubscriptionApp component', () => {
customerPortalUrl: appProps.customerPortalUrl, customerPortalUrl: appProps.customerPortalUrl,
}); });
}); });
it('passes the correct props to the subscriptions seats component', () => {
expectComponentWithProps(SubscriptionSeats, {
namespaceName: appProps.namespaceName,
namespaceId: appProps.namespaceId,
});
});
});
describe('when there are no billable members', () => {
beforeEach(() => {
factory();
store.commit(`subscription/${types.RECEIVE_HAS_BILLABLE_MEMBERS_SUCCESS}`, {
data: [],
headers: {},
});
});
it('does not render the subscription seats table', () => {
expect(findSubscriptionSeatsTable().exists()).toBe(false);
});
});
describe('when feature flag is disabled', () => {
beforeEach(() => {
factory(appProps, false);
});
it('does not dispatch fetchBillableGroupMembers action on created', () => {
expect(store.dispatch.mock.calls).not.toContainEqual([
'subscription/fetchBillableGroupMembers',
undefined,
]);
});
it('does not render the subscription seats table', () => {
expect(findSubscriptionSeatsTable().exists()).toBe(false);
});
}); });
}); });
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlPagination } from '@gitlab/ui';
import Vuex from 'vuex';
import SubscriptionSeats from 'ee/billings/components/subscription_seats.vue';
import { mockDataSeats, seatsTableItems } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
const actionSpies = {
setNamespaceId: jest.fn(),
fetchBillableMembersList: jest.fn(),
};
const tableProps = {
namespaceName: 'Test Group Name',
namespaceId: '1000',
};
const fakeStore = ({ initialState }) =>
new Vuex.Store({
modules: {
seats: {
namespaced: true,
actions: actionSpies,
state: {
isLoading: false,
hasError: false,
...initialState,
},
},
},
});
const createComponent = ({ props = {}, options = {}, initialState = {} } = {}) => {
return shallowMount(SubscriptionSeats, {
propsData: { ...tableProps, ...props },
store: fakeStore({ initialState }),
localVue,
...options,
stubs: {
GlTable: { template: '<div></div>', props: { items: Array, fields: Array, busy: Boolean } },
},
});
};
describe('Subscription Seats', () => {
let wrapper;
const findTable = () => wrapper.find('[data-testid="seats-table"]');
const findHeading = () => wrapper.find('[data-testid="heading"]');
const findPagination = () => wrapper.find(GlPagination);
beforeEach(() => {
wrapper = createComponent({
initialState: {
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: 1,
perPage: 5,
},
});
});
it('correct actions are called on create', () => {
expect(actionSpies.setNamespaceId).toHaveBeenCalledWith(
expect.any(Object),
tableProps.namespaceId,
);
expect(actionSpies.fetchBillableMembersList).toHaveBeenCalledWith(expect.any(Object), 1);
});
describe('heading text', () => {
it('contains the group name and total seats number', () => {
expect(findHeading().text()).toMatch(tableProps.namespaceName);
expect(findHeading().text()).toMatch('300');
});
});
describe('table', () => {
it('is rendered and passed correct values', () => {
expect(findTable().props('fields')).toEqual(['user']);
expect(findTable().props('busy')).toBe(false);
expect(findTable().props('items')).toEqual(seatsTableItems);
});
});
describe('pagination', () => {
it('is rendered and passed correct values', () => {
expect(findPagination().vm.value).toBe(1);
expect(findPagination().props('perPage')).toBe(5);
expect(findPagination().props('totalItems')).toBe(300);
});
it.each([null, NaN, undefined, 'a string', false])(
'will not render given %s for currentPage',
value => {
wrapper = createComponent({
initialState: {
namespaceId: null,
members: [...mockDataSeats.data],
total: 300,
page: value,
perPage: 5,
},
});
expect(findPagination().exists()).toBe(false);
},
);
});
});
...@@ -41,7 +41,7 @@ describe('SubscriptionTable component', () => { ...@@ -41,7 +41,7 @@ describe('SubscriptionTable component', () => {
}, },
}); });
Object.assign(store.state.subscription, { isLoading: true }); Object.assign(store.state.subscription, { isLoadingSubscription: true });
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
}); });
...@@ -63,7 +63,7 @@ describe('SubscriptionTable component', () => { ...@@ -63,7 +63,7 @@ describe('SubscriptionTable component', () => {
beforeEach(() => { beforeEach(() => {
factory({ propsData: { namespaceName: TEST_NAMESPACE_NAME } }); factory({ propsData: { namespaceName: TEST_NAMESPACE_NAME } });
store.state.subscription.isLoading = false; store.state.subscription.isLoadingSubscription = false;
store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold); store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold);
return wrapper.vm.$nextTick(); return wrapper.vm.$nextTick();
...@@ -104,7 +104,7 @@ describe('SubscriptionTable component', () => { ...@@ -104,7 +104,7 @@ describe('SubscriptionTable component', () => {
}); });
Object.assign(store.state.subscription, { Object.assign(store.state.subscription, {
isLoading: false, isLoadingSubscription: false,
isFreePlan, isFreePlan,
plan: { plan: {
code: planName, code: planName,
......
...@@ -68,31 +68,22 @@ export const mockDataSubscription = { ...@@ -68,31 +68,22 @@ export const mockDataSubscription = {
export const mockDataSeats = { export const mockDataSeats = {
data: [ data: [
{ {
id: 1,
name: 'Administrator', name: 'Administrator',
username: 'root', username: 'root',
state: 'active', avatar_url: 'path/to/img',
avatar_url: web_url: 'path/to/user',
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon',
web_url: 'http://192.168.1.209:3001/root',
}, },
{ {
id: 3,
name: 'Agustin Walker', name: 'Agustin Walker',
username: 'lester.orn', username: 'lester.orn',
state: 'active', avatar_url: 'path/to/img',
avatar_url: web_url: 'path/to/user',
'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', name: 'Joella Miller',
username: 'era', username: 'era',
state: 'active', avatar_url: 'path/to/img',
avatar_url: web_url: 'path/to/user',
'https://www.gravatar.com/avatar/8b306a0c173657865f6a5a6c7120b408?s=80\u0026d=identicon',
web_url: 'http://192.168.1.209:3001/era',
}, },
], ],
headers: { headers: {
...@@ -101,3 +92,30 @@ export const mockDataSeats = { ...@@ -101,3 +92,30 @@ export const mockDataSeats = {
[HEADER_ITEMS_PER_PAGE]: '1', [HEADER_ITEMS_PER_PAGE]: '1',
}, },
}; };
export const seatsTableItems = [
{
user: {
name: 'Administrator',
username: '@root',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
},
},
{
user: {
name: 'Agustin Walker',
username: '@lester.orn',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
},
},
{
user: {
name: 'Joella Miller',
username: '@era',
avatar_url: 'path/to/img',
web_url: 'path/to/user',
},
},
];
...@@ -29,12 +29,12 @@ describe('EE billings subscription module mutations', () => { ...@@ -29,12 +29,12 @@ describe('EE billings subscription module mutations', () => {
mutations[types.REQUEST_SUBSCRIPTION](state); mutations[types.REQUEST_SUBSCRIPTION](state);
}); });
it('sets isLoading to true', () => { it('sets isLoadingSubscription to true', () => {
expect(state.isLoading).toBeTruthy(); expect(state.isLoadingSubscription).toBeTruthy();
}); });
it('sets hasError to false', () => { it('sets hasErrorSubscription to false', () => {
expect(state.hasError).toBeFalsy(); expect(state.hasErrorSubscription).toBeFalsy();
}); });
}); });
...@@ -57,12 +57,12 @@ describe('EE billings subscription module mutations', () => { ...@@ -57,12 +57,12 @@ describe('EE billings subscription module mutations', () => {
${'with Gold trial'} | ${mockDataSubscription.trial} | ${TABLE_TYPE_TRIAL} ${'with Gold trial'} | ${mockDataSubscription.trial} | ${TABLE_TYPE_TRIAL}
`('$desc', ({ subscription, tableKey }) => { `('$desc', ({ subscription, tableKey }) => {
beforeEach(() => { beforeEach(() => {
state.isLoading = true; state.isLoadingSubscription = true;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, subscription); mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, subscription);
}); });
it('sets isLoading to false', () => { it('sets isLoadingSubscription to false', () => {
expect(state.isLoading).toBeFalsy(); expect(state.isLoadingSubscription).toBeFalsy();
}); });
it('sets plan', () => { it('sets plan', () => {
...@@ -82,12 +82,12 @@ describe('EE billings subscription module mutations', () => { ...@@ -82,12 +82,12 @@ describe('EE billings subscription module mutations', () => {
mutations[types.RECEIVE_SUBSCRIPTION_ERROR](state); mutations[types.RECEIVE_SUBSCRIPTION_ERROR](state);
}); });
it('sets isLoading to false', () => { it('sets isLoadingSubscription to false', () => {
expect(state.isLoading).toBeFalsy(); expect(state.isLoadingSubscription).toBeFalsy();
}); });
it('sets hasError to true', () => { it('sets hasErrorSubscription to true', () => {
expect(state.hasError).toBeTruthy(); expect(state.hasErrorSubscription).toBeTruthy();
}); });
}); });
}); });
...@@ -3024,9 +3024,6 @@ msgstr "" ...@@ -3024,9 +3024,6 @@ msgstr ""
msgid "An error occurred while loading the pipelines jobs." msgid "An error occurred while loading the pipelines jobs."
msgstr "" msgstr ""
msgid "An error occurred while loading the subscription details."
msgstr ""
msgid "An error occurred while making the request." msgid "An error occurred while making the request."
msgstr "" msgstr ""
...@@ -4202,6 +4199,15 @@ msgstr "" ...@@ -4202,6 +4199,15 @@ msgstr ""
msgid "Billing|An error occurred while loading billable members list" msgid "Billing|An error occurred while loading billable members list"
msgstr "" msgstr ""
msgid "Billing|No users to display."
msgstr ""
msgid "Billing|Updated live"
msgstr ""
msgid "Billing|Users occupying seats in %{namespaceName} Group (%{total})"
msgstr ""
msgid "Bitbucket Server Import" msgid "Bitbucket Server Import"
msgstr "" msgstr ""
...@@ -23182,7 +23188,7 @@ msgstr "" ...@@ -23182,7 +23188,7 @@ msgstr ""
msgid "Seat Link is disabled, and cannot be configured through this form." msgid "Seat Link is disabled, and cannot be configured through this form."
msgstr "" msgstr ""
msgid "Seats usage data as of %{last_enqueue_time}" msgid "Seats usage data as of %{last_enqueue_time} (Updated daily)"
msgstr "" msgstr ""
msgid "Seats usage data is updated every day at 12:00pm UTC" msgid "Seats usage data is updated every day at 12:00pm UTC"
...@@ -25366,6 +25372,12 @@ msgstr "" ...@@ -25366,6 +25372,12 @@ msgstr ""
msgid "Subscription successfully deleted." msgid "Subscription successfully deleted."
msgstr "" msgstr ""
msgid "SubscriptionTable|An error occurred while loading billable members list"
msgstr ""
msgid "SubscriptionTable|An error occurred while loading the subscription details."
msgstr ""
msgid "SubscriptionTable|Billing" msgid "SubscriptionTable|Billing"
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