Commit e9eb7967 authored by Filipa Lacerda's avatar Filipa Lacerda

Merge branch '8838-adapt-subscriptions-page-for-free-plans-and-trials' into 'master'

Resolve "Adapt subscriptions page for free plans and trials"

Closes #8838

See merge request gitlab-org/gitlab-ee!8838
parents 71381976 9feeceba
...@@ -3,7 +3,12 @@ import _ from 'underscore'; ...@@ -3,7 +3,12 @@ import _ from 'underscore';
import { mapActions, mapState, mapGetters } from 'vuex'; import { mapActions, mapState, mapGetters } from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui'; import { GlLoadingIcon } from '@gitlab/ui';
import SubscriptionTableRow from './subscription_table_row.vue'; import SubscriptionTableRow from './subscription_table_row.vue';
import { CUSTOMER_PORTAL_URL } from '../constants'; import {
CUSTOMER_PORTAL_URL,
TABLE_TYPE_DEFAULT,
TABLE_TYPE_FREE,
TABLE_TYPE_TRIAL,
} from '../constants';
import { s__, sprintf } from '~/locale'; import { s__, sprintf } from '~/locale';
export default { export default {
...@@ -13,12 +18,12 @@ export default { ...@@ -13,12 +18,12 @@ export default {
GlLoadingIcon, GlLoadingIcon,
}, },
computed: { computed: {
...mapState('subscription', ['isLoading', 'hasError', 'plan', 'rows', 'endpoint']), ...mapState('subscription', ['isLoading', 'hasError', 'plan', 'tables', 'endpoint']),
...mapGetters('subscription', ['isFreePlan']), ...mapGetters('subscription', ['isFreePlan']),
subscriptionHeader() { subscriptionHeader() {
let suffix = this.isFreePlan ? '' : s__('SubscriptionTable|subscription'); let suffix = '';
if (!this.isFreePlan && this.plan.trial) { if (!this.isFreePlan && this.plan.trial) {
suffix += ` - ${s__('SubscriptionTable|Trial')}`; suffix = `${s__('SubscriptionTable|Trial')}`;
} }
return sprintf(s__('SubscriptionTable|GitLab.com %{planName} %{suffix}'), { return sprintf(s__('SubscriptionTable|GitLab.com %{planName} %{suffix}'), {
planName: this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name), planName: this.isFreePlan ? s__('SubscriptionTable|Free') : _.escape(this.plan.name),
...@@ -28,6 +33,17 @@ export default { ...@@ -28,6 +33,17 @@ export default {
actionButtonText() { actionButtonText() {
return this.isFreePlan ? s__('SubscriptionTable|Upgrade') : s__('SubscriptionTable|Manage'); return this.isFreePlan ? s__('SubscriptionTable|Upgrade') : s__('SubscriptionTable|Manage');
}, },
visibleRows() {
let tableKey = TABLE_TYPE_DEFAULT;
if (this.plan.code === null) {
tableKey = TABLE_TYPE_FREE;
} else if (this.plan.trial) {
tableKey = TABLE_TYPE_TRIAL;
}
return this.tables[tableKey].rows;
},
}, },
mounted() { mounted() {
this.fetchSubscription(); this.fetchSubscription();
...@@ -60,7 +76,7 @@ export default { ...@@ -60,7 +76,7 @@ export default {
</div> </div>
<div class="card-body flex-grid d-flex flex-column flex-sm-row flex-md-row flex-lg-column"> <div class="card-body flex-grid d-flex flex-column flex-sm-row flex-md-row flex-lg-column">
<subscription-table-row <subscription-table-row
v-for="(row, i) in rows" v-for="(row, i) in visibleRows"
:key="`subscription-rows-${i}`" :key="`subscription-rows-${i}`"
:header="row.header" :header="row.header"
:columns="row.columns" :columns="row.columns"
......
...@@ -56,7 +56,11 @@ export default { ...@@ -56,7 +56,11 @@ export default {
</span> </span>
</div> </div>
<template v-for="(col, i) in columns"> <template v-for="(col, i) in columns">
<div :key="`subscription-col-${i}`" class="grid-cell" :class="[col.hidden ? 'no-value' : '']"> <div
:key="`subscription-col-${i}`"
class="grid-cell"
:class="[col.hideContent ? 'no-value' : '']"
>
<span class="property-label"> {{ col.label }} </span> <span class="property-label"> {{ col.label }} </span>
<popover v-if="col.popover" :options="getPopoverOptions(col)" /> <popover v-if="col.popover" :options="getPopoverOptions(col)" />
<p <p
......
export const USAGE_ROW_INDEX = 0;
export const BILLING_ROW_INDEX = 1;
export const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions'; export const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions';
export const TABLE_TYPE_DEFAULT = 'default';
export const TABLE_TYPE_FREE = 'free';
export const TABLE_TYPE_TRIAL = 'trial';
import Vue from 'vue'; import Vue from 'vue';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from '../../../constants';
import * as types from './mutation_types'; import * as types from './mutation_types';
import { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from '../../../constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
export default { export default {
...@@ -16,22 +16,22 @@ export default { ...@@ -16,22 +16,22 @@ export default {
[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) { [types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload) {
const data = convertObjectPropsToCamelCase(payload, { deep: true }); const data = convertObjectPropsToCamelCase(payload, { deep: true });
const { plan, usage, billing } = data; const { plan, usage, billing } = data;
let tableKey = TABLE_TYPE_DEFAULT;
state.plan = plan; state.plan = plan;
/* if (state.plan.code === null) {
* Update column values for billing and usage row. tableKey = TABLE_TYPE_FREE;
* We iterate over the rows within the state } else if (state.plan.trial) {
* and update only the column's value property in the state tableKey = TABLE_TYPE_TRIAL;
* with the data we received from the API for the given column }
*/
[USAGE_ROW_INDEX, BILLING_ROW_INDEX].forEach(rowIdx => { state.tables[tableKey].rows.forEach(row => {
const currentRow = state.rows[rowIdx]; row.columns.forEach(col => {
currentRow.columns.forEach(currentCol => { if (Object.prototype.hasOwnProperty.call(usage, col.id)) {
if (rowIdx === USAGE_ROW_INDEX) { Vue.set(col, 'value', usage[col.id]);
Vue.set(currentCol, 'value', usage[currentCol.id]); } else if (Object.prototype.hasOwnProperty.call(billing, col.id)) {
} else if (rowIdx === BILLING_ROW_INDEX) { Vue.set(col, 'value', billing[col.id]);
Vue.set(currentCol, 'value', billing[currentCol.id]);
} }
}); });
}); });
......
...@@ -9,6 +9,70 @@ export default () => ({ ...@@ -9,6 +9,70 @@ export default () => ({
name: null, name: null,
trial: false, trial: false,
}, },
tables: {
free: {
rows: [
{
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__(
'SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan.',
),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Subscription start date'),
value: null,
isDate: true,
},
],
},
],
},
trial: {
rows: [
{
header: {
icon: 'monitor',
title: s__('SubscriptionTable|Usage'),
},
columns: [
{
id: 'seatsInUse',
label: s__('SubscriptionTable|Seats currently in use'),
value: null,
colClass: 'number',
popover: {
content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
},
},
{
id: 'subscriptionStartDate',
label: s__('SubscriptionTable|Trial start date'),
value: null,
isDate: true,
},
{
id: 'subscriptionEndDate',
label: s__('SubscriptionTable|Trial end date'),
value: null,
isDate: true,
},
],
},
],
},
default: {
rows: [ rows: [
{ {
header: { header: {
...@@ -28,7 +92,7 @@ export default () => ({ ...@@ -28,7 +92,7 @@ export default () => ({
value: null, value: null,
colClass: 'number', colClass: 'number',
popover: { popover: {
content: s__(`SubscriptionTable|Usage count is performed once a day at 12:00 PM.`), content: s__('SubscriptionTable|Usage count is performed once a day at 12:00 PM.'),
}, },
}, },
{ {
...@@ -83,7 +147,7 @@ export default () => ({ ...@@ -83,7 +147,7 @@ export default () => ({
'SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances.', 'SubscriptionTable|This is the last time the GitLab.com team was in contact with you to settle any outstanding balances.',
), ),
}, },
hidden: true, hideContent: true, // temporarily display a blank cell (as we don't have content yet)
}, },
{ {
id: 'nextInvoice', id: 'nextInvoice',
...@@ -95,9 +159,11 @@ export default () => ({ ...@@ -95,9 +159,11 @@ export default () => ({
'SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances.', 'SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances.',
), ),
}, },
hidden: true, hideContent: true, // temporarily display a blank cell (as we don't have content yet)
}, },
], ],
}, },
], ],
},
},
}); });
---
title: Adapt subscriptions page for free plans and trials
merge_request: 8838
author:
type: other
...@@ -37,10 +37,17 @@ describe('Subscription Table Row', () => { ...@@ -37,10 +37,17 @@ describe('Subscription Table Row', () => {
vm = mountComponent(Component, props); vm = mountComponent(Component, props);
}); });
it(`should render one header cell and ${columns.length} columns in total`, () => { it(`should render one header cell and ${columns.length} visible columns in total`, () => {
expect(vm.$el.querySelectorAll('.grid-cell')).toHaveLength(columns.length + 1); expect(vm.$el.querySelectorAll('.grid-cell')).toHaveLength(columns.length + 1);
}); });
it(`should not render a hidden column`, () => {
const hiddenColIdx = columns.find(c => !c.display);
const hiddenCol = vm.$el.querySelectorAll('.grid-cell:not(.header-cell)')[hiddenColIdx];
expect(hiddenCol).toBe(undefined);
});
it('should render a title in the header cell', () => { it('should render a title in the header cell', () => {
expect(vm.$el.querySelector('.header-cell').textContent).toContain(props.header.title); expect(vm.$el.querySelector('.header-cell').textContent).toContain(props.header.title);
}); });
......
...@@ -2,8 +2,8 @@ import Vue from 'vue'; ...@@ -2,8 +2,8 @@ import Vue from 'vue';
import component from 'ee/billings/components/subscription_table.vue'; import component from 'ee/billings/components/subscription_table.vue';
import createStore from 'ee/billings/stores'; 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 mockDataSubscription from 'ee/billings/stores/modules/subscription/mock_data_subscription.json';
import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper'; import { createComponentWithStore } from 'spec/helpers/vue_mount_component_helper';
import mockDataSubscription from '../mock_data';
import { resetStore } from '../helpers'; import { resetStore } from '../helpers';
describe('Subscription Table', () => { describe('Subscription Table', () => {
...@@ -41,14 +41,17 @@ describe('Subscription Table', () => { ...@@ -41,14 +41,17 @@ describe('Subscription Table', () => {
beforeEach(done => { beforeEach(done => {
vm.$store.state.subscription.namespaceId = namespaceId; vm.$store.state.subscription.namespaceId = namespaceId;
vm.$store.commit(`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription); vm.$store.commit(
`subscription/${types.RECEIVE_SUBSCRIPTION_SUCCESS}`,
mockDataSubscription.gold,
);
vm.$store.state.subscription.isLoading = false; vm.$store.state.subscription.isLoading = false;
vm.$nextTick(done); vm.$nextTick(done);
}); });
it('should render the card title "GitLab.com Gold subscription"', () => { it('should render the card title "GitLab.com Gold"', () => {
expect(vm.$el.querySelector('.js-subscription-header strong').textContent.trim()).toBe( expect(vm.$el.querySelector('.js-subscription-header strong').textContent.trim()).toBe(
'GitLab.com Gold subscription', 'GitLab.com Gold',
); );
}); });
......
export default {
gold: {
plan: {
name: 'Gold',
code: 'gold',
trial: false,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 98,
max_seats_used: 104,
seats_owed: 4,
},
billing: {
subscription_start_date: '2018-07-11',
subscription_end_date: '2019-07-11',
last_invoice: '2018-09-01',
next_invoice: '2018-10-01',
},
},
free: {
plan: {
name: null,
code: null,
trial: null,
},
usage: {
seats_in_subscription: 0,
seats_in_use: 0,
max_seats_used: 5,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-10-30',
subscription_end_date: null,
trial_ends_on: null,
},
},
trial: {
plan: {
name: 'Gold',
code: 'gold',
trial: true,
},
usage: {
seats_in_subscription: 100,
seats_in_use: 1,
max_seats_used: 0,
seats_owed: 0,
},
billing: {
subscription_start_date: '2018-12-13',
subscription_end_date: '2019-12-13',
trial_ends_on: '2019-12-13',
},
},
};
...@@ -6,7 +6,7 @@ import state from 'ee/billings/stores/modules/subscription/state'; ...@@ -6,7 +6,7 @@ import state from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; 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 mockDataSubscription from './data/mock_data_subscription.json'; import mockDataSubscription from '../../../mock_data';
describe('subscription actions', () => { describe('subscription actions', () => {
let mockedState; let mockedState;
...@@ -51,7 +51,7 @@ describe('subscription actions', () => { ...@@ -51,7 +51,7 @@ describe('subscription actions', () => {
beforeEach(() => { beforeEach(() => {
mock mock
.onGet(/\/api\/v4\/namespaces\/\d+\/gitlab_subscription(.*)$/) .onGet(/\/api\/v4\/namespaces\/\d+\/gitlab_subscription(.*)$/)
.replyOnce(200, mockDataSubscription); .replyOnce(200, mockDataSubscription.gold);
}); });
it('should dispatch the request and success actions', done => { it('should dispatch the request and success actions', done => {
...@@ -64,7 +64,7 @@ describe('subscription actions', () => { ...@@ -64,7 +64,7 @@ describe('subscription actions', () => {
{ type: 'requestSubscription' }, { type: 'requestSubscription' },
{ {
type: 'receiveSubscriptionSuccess', type: 'receiveSubscriptionSuccess',
payload: mockDataSubscription, payload: mockDataSubscription.gold,
}, },
], ],
done, done,
...@@ -107,12 +107,12 @@ describe('subscription actions', () => { ...@@ -107,12 +107,12 @@ describe('subscription actions', () => {
it('should commit the success mutation', done => { it('should commit the success mutation', done => {
testAction( testAction(
actions.receiveSubscriptionSuccess, actions.receiveSubscriptionSuccess,
mockDataSubscription, mockDataSubscription.gold,
mockedState, mockedState,
[ [
{ {
type: types.RECEIVE_SUBSCRIPTION_SUCCESS, type: types.RECEIVE_SUBSCRIPTION_SUCCESS,
payload: mockDataSubscription, payload: mockDataSubscription.gold,
}, },
], ],
[], [],
......
{
"plan": {
"name": "Gold",
"code": "gold",
"trial": false
},
"usage": {
"seats_in_subscription": 100,
"seats_in_use": 98,
"max_seats_used": 104,
"seats_owed": 4
},
"billing": {
"subscription_start_date": "2018-07-11",
"subscription_end_date": "2019-07-11",
"last_invoice": "2018-09-01",
"next_invoice": "2018-10-01"
}
}
import createState from 'ee/billings/stores/modules/subscription/state'; import createState from 'ee/billings/stores/modules/subscription/state';
import * as types from 'ee/billings/stores/modules/subscription/mutation_types'; 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 { USAGE_ROW_INDEX, BILLING_ROW_INDEX } from 'ee/billings/constants';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils'; import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import mockData from './data/mock_data_subscription.json'; import mockData from '../../../mock_data';
describe('subscription module mutations', () => { describe('subscription module mutations', () => {
describe('SET_PNAMESPACE_ID', () => { describe('SET_NAMESPACE_ID', () => {
it('should set "namespaceId" to "1"', () => { it('should set "namespaceId" to "1"', () => {
const state = createState(); const state = createState();
const namespaceId = '1'; const namespaceId = '1';
...@@ -34,9 +33,10 @@ describe('subscription module mutations', () => { ...@@ -34,9 +33,10 @@ describe('subscription module mutations', () => {
let payload; let payload;
let state; let state;
describe('Gold subscription', () => {
beforeEach(() => { beforeEach(() => {
payload = mockData;
state = createState(); state = createState();
payload = mockData.gold;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload); mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
}); });
...@@ -51,7 +51,7 @@ describe('subscription module mutations', () => { ...@@ -51,7 +51,7 @@ describe('subscription module mutations', () => {
}); });
it('should set the column values on the "Usage" row', () => { it('should set the column values on the "Usage" row', () => {
const usageRow = state.rows[USAGE_ROW_INDEX]; const usageRow = state.tables.default.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true }); const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => { usageRow.columns.forEach(column => {
expect(column.value).toBe(data.usage[column.id]); expect(column.value).toBe(data.usage[column.id]);
...@@ -59,14 +59,73 @@ describe('subscription module mutations', () => { ...@@ -59,14 +59,73 @@ describe('subscription module mutations', () => {
}); });
it('should set the column values on the "Billing" row', () => { it('should set the column values on the "Billing" row', () => {
const billingow = state.rows[BILLING_ROW_INDEX]; const billingRow = state.tables.default.rows[1];
const data = convertObjectPropsToCamelCase(payload, { deep: true }); const data = convertObjectPropsToCamelCase(payload, { deep: true });
billingow.columns.forEach(column => { billingRow.columns.forEach(column => {
expect(column.value).toBe(data.billing[column.id]); expect(column.value).toBe(data.billing[column.id]);
}); });
}); });
}); });
describe('Free plan', () => {
beforeEach(() => {
state = createState();
payload = mockData.free;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" from "billings row" correctly', () => {
const usageRow = state.tables.free.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
}
});
});
});
describe('Gold trial', () => {
beforeEach(() => {
state = createState();
payload = mockData.trial;
mutations[types.RECEIVE_SUBSCRIPTION_SUCCESS](state, payload);
});
it('should set "isLoading" to "false"', () => {
expect(state.isLoading).toBeFalsy();
});
it('should set "plan" attributes', () => {
expect(state.plan.code).toBe(payload.plan.code);
expect(state.plan.name).toBe(payload.plan.name);
expect(state.plan.trial).toBe(payload.plan.trial);
});
it('should populate "subscriptionStartDate" and "subscriptionEndDate" from "billings row" correctly', () => {
const usageRow = state.tables.trial.rows[0];
const data = convertObjectPropsToCamelCase(payload, { deep: true });
usageRow.columns.forEach(column => {
if (column.id === 'subscriptionStartDate') {
expect(column.value).toBe(data.billing.subscriptionStartDate);
} else if (column.id === 'subscriptionEndDate') {
expect(column.value).toBe(data.billing.subscriptionEndDate);
}
});
});
});
});
describe('RECEIVE_SUBSCRIPTION_ERROR', () => { describe('RECEIVE_SUBSCRIPTION_ERROR', () => {
let state; let state;
......
...@@ -8406,9 +8406,18 @@ msgstr "" ...@@ -8406,9 +8406,18 @@ msgstr ""
msgid "SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances." msgid "SubscriptionTable|This is the next date when the GitLab.com team is scheduled to get in contact with you to settle any outstanding balances."
msgstr "" msgstr ""
msgid "SubscriptionTable|This is the number of seats you will be required to purchase if you update to a paid plan."
msgstr ""
msgid "SubscriptionTable|Trial" msgid "SubscriptionTable|Trial"
msgstr "" msgstr ""
msgid "SubscriptionTable|Trial end date"
msgstr ""
msgid "SubscriptionTable|Trial start date"
msgstr ""
msgid "SubscriptionTable|Upgrade" msgid "SubscriptionTable|Upgrade"
msgstr "" msgstr ""
...@@ -8418,9 +8427,6 @@ msgstr "" ...@@ -8418,9 +8427,6 @@ msgstr ""
msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM." msgid "SubscriptionTable|Usage count is performed once a day at 12:00 PM."
msgstr "" msgstr ""
msgid "SubscriptionTable|subscription"
msgstr ""
msgid "Suggested change" msgid "Suggested change"
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