Commit d31464dd authored by David O'Regan's avatar David O'Regan

Merge branch 'ag-2305-cdot-fe' into 'master'

Add 'Add Seats' Button to Billing section

See merge request gitlab-org/gitlab!49548
parents 53e51823 5af515d6
...@@ -5,7 +5,9 @@ import { GlLoadingIcon } from '@gitlab/ui'; ...@@ -5,7 +5,9 @@ import { GlLoadingIcon } from '@gitlab/ui';
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 { s__ } from '~/locale'; import { s__ } from '~/locale';
import SubscriptionTableRow from './subscription_table_row.vue'; import SubscriptionTableRow from './subscription_table_row.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import glFeaturesFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const createButtonProps = (text, href, testId) => ({ text, href, testId });
export default { export default {
name: 'SubscriptionTable', name: 'SubscriptionTable',
...@@ -13,7 +15,7 @@ export default { ...@@ -13,7 +15,7 @@ export default {
SubscriptionTableRow, SubscriptionTableRow,
GlLoadingIcon, GlLoadingIcon,
}, },
mixins: [glFeatureFlagsMixin()], mixins: [glFeaturesFlagMixin()],
props: { props: {
namespaceName: { namespaceName: {
type: String, type: String,
...@@ -35,6 +37,11 @@ export default { ...@@ -35,6 +37,11 @@ export default {
default: '', default: '',
}, },
}, },
inject: {
addSeatsHref: {
default: '',
},
},
computed: { computed: {
...mapState(['isLoadingSubscription', 'hasErrorSubscription', 'plan', 'tables', 'endpoint']), ...mapState(['isLoadingSubscription', 'hasErrorSubscription', 'plan', 'tables', 'endpoint']),
...mapGetters(['isFreePlan']), ...mapGetters(['isFreePlan']),
...@@ -44,43 +51,45 @@ export default { ...@@ -44,43 +51,45 @@ export default {
return `${this.namespaceName}: ${planName} ${suffix}`; return `${this.namespaceName}: ${planName} ${suffix}`;
}, },
canAddSeats() {
return this.glFeatures.saasAddSeatsButton && !this.isFreePlan;
},
canRenew() {
return this.glFeatures.saasManualRenewButton && !this.isFreePlan;
},
canUpgrade() {
return this.isFreePlan || this.plan.upgradable;
},
canUpgradeEEPlan() {
return !this.isFreePlan && this.planUpgradeHref;
},
addSeatsButton() {
return this.canAddSeats
? createButtonProps(s__('SubscriptionTable|Add seats'), this.addSeatsHref, 'add-seats')
: null;
},
upgradeButton() { upgradeButton() {
if (!this.isFreePlan && !this.plan.upgradable) { return this.canUpgrade
return null; ? createButtonProps(s__('SubscriptionTable|Upgrade'), this.upgradeButtonHref)
} : null;
},
return { upgradeButtonHref() {
text: s__('SubscriptionTable|Upgrade'), return this.canUpgradeEEPlan ? this.planUpgradeHref : this.customerPortalUrl;
href:
!this.isFreePlan && this.planUpgradeHref ? this.planUpgradeHref : this.customerPortalUrl,
};
}, },
renewButton() { renewButton() {
if (!this.glFeatures.saasManualRenewButton) { return this.canRenew
return null; ? createButtonProps(s__('SubscriptionTable|Renew'), this.planRenewHref)
} : null;
if (this.isFreePlan) {
return null;
}
return {
text: s__('SubscriptionTable|Renew'),
href: this.planRenewHref,
};
}, },
manageButton() { manageButton() {
if (this.isFreePlan) { return !this.isFreePlan
return null; ? createButtonProps(s__('SubscriptionTable|Manage'), this.customerPortalUrl)
} : null;
return {
text: s__('SubscriptionTable|Manage'),
href: this.customerPortalUrl,
};
}, },
buttons() { buttons() {
return [this.upgradeButton, this.renewButton, this.manageButton].filter(Boolean); return [this.upgradeButton, this.addSeatsButton, this.renewButton, this.manageButton].filter(
Boolean,
);
}, },
visibleRows() { visibleRows() {
let tableKey = TABLE_TYPE_DEFAULT; let tableKey = TABLE_TYPE_DEFAULT;
...@@ -119,7 +128,8 @@ export default { ...@@ -119,7 +128,8 @@ export default {
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
class="btn btn-inverted-secondary" class="btn btn-inverted-secondary"
:class="{ 'ml-2': index !== 0 }" :class="{ 'gl-ml-3': index !== 0 }"
:data-testid="button.testId"
>{{ button.text }}</a >{{ button.text }}</a
> >
</div> </div>
......
...@@ -15,6 +15,7 @@ export default (containerId = 'js-billing-plans') => { ...@@ -15,6 +15,7 @@ export default (containerId = 'js-billing-plans') => {
const { const {
namespaceId, namespaceId,
namespaceName, namespaceName,
addSeatsHref,
planUpgradeHref, planUpgradeHref,
planRenewHref, planRenewHref,
customerPortalUrl, customerPortalUrl,
...@@ -27,6 +28,7 @@ export default (containerId = 'js-billing-plans') => { ...@@ -27,6 +28,7 @@ export default (containerId = 'js-billing-plans') => {
provide: { provide: {
namespaceId, namespaceId,
namespaceName, namespaceName,
addSeatsHref,
planUpgradeHref, planUpgradeHref,
planRenewHref, planRenewHref,
customerPortalUrl, customerPortalUrl,
......
...@@ -5,9 +5,9 @@ require 'spec_helper' ...@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe 'Groups > Billing', :js do RSpec.describe 'Groups > Billing', :js do
include StubRequests include StubRequests
let!(:user) { create(:user) } let_it_be(:user) { create(:user) }
let!(:group) { create(:group) } let_it_be(:group) { create(:group) }
let!(:bronze_plan) { create(:bronze_plan) } let_it_be(:bronze_plan) { create(:bronze_plan) }
def formatted_date(date) def formatted_date(date)
date.strftime("%B %-d, %Y") date.strftime("%B %-d, %Y")
...@@ -19,6 +19,7 @@ RSpec.describe 'Groups > Billing', :js do ...@@ -19,6 +19,7 @@ RSpec.describe 'Groups > Billing', :js do
before do before do
stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan}") stub_full_request("#{EE::SUBSCRIPTIONS_URL}/gitlab_plans?plan=#{plan}")
.with(headers: { 'Accept' => 'application/json' })
.to_return(status: 200, body: File.new(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json'))) .to_return(status: 200, body: File.new(Rails.root.join('ee/spec/fixtures/gitlab_com_plans.json')))
allow(Gitlab).to receive(:com?).and_return(true) allow(Gitlab).to receive(:com?).and_return(true)
...@@ -50,21 +51,40 @@ RSpec.describe 'Groups > Billing', :js do ...@@ -50,21 +51,40 @@ RSpec.describe 'Groups > Billing', :js do
context 'with a paid plan' do context 'with a paid plan' do
let(:plan) { 'bronze' } let(:plan) { 'bronze' }
let!(:subscription) do let_it_be(:subscription) do
create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, seats: 15) create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, seats: 15)
end end
it 'shows the proper title and subscription data' do it 'shows the proper title and subscription data' do
visit group_billings_path(group) extra_seats_url = "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/extra_seats"
renew_url = "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/renew"
upgrade_url = upgrade_url =
"#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/bronze-external-id" "#{EE::SUBSCRIPTIONS_URL}/gitlab/namespaces/#{group.id}/upgrade/bronze-external-id"
visit group_billings_path(group)
expect(page).to have_content("#{group.name} is currently using the Bronze plan") expect(page).to have_content("#{group.name} is currently using the Bronze plan")
within subscription_table do within subscription_table do
expect(page).to have_content("start date #{formatted_date(subscription.start_date)}") expect(page).to have_content("start date #{formatted_date(subscription.start_date)}")
expect(page).to have_link("Upgrade", href: upgrade_url) expect(page).to have_link("Upgrade", href: upgrade_url)
expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions") expect(page).to have_link("Manage", href: "#{EE::SUBSCRIPTIONS_URL}/subscriptions")
expect(page).to have_link("Add seats", href: extra_seats_url)
expect(page).to have_link("Renew", href: renew_url)
end
end
context 'with disabled feature flags' do
before do
stub_feature_flags(saas_manual_renew_button: false)
stub_feature_flags(saas_add_seats_button: false)
visit group_billings_path(group)
end
it 'does not show "Add Seats" button' do
within subscription_table do
expect(page).not_to have_link("Add seats")
expect(page).not_to have_link("Renew")
end
end end
end end
end end
...@@ -86,4 +106,37 @@ RSpec.describe 'Groups > Billing', :js do ...@@ -86,4 +106,37 @@ RSpec.describe 'Groups > Billing', :js do
end end
end end
end end
context 'with feature flags' do
using RSpec::Parameterized::TableSyntax
where(:saas_manual_renew_button, :saas_add_seats_button) do
true | true
true | false
false | true
false | false
end
let(:plan) { 'bronze' }
let_it_be(:subscription) do
create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, seats: 15)
end
with_them do
before do
stub_feature_flags(saas_manual_renew_button: saas_manual_renew_button)
stub_feature_flags(saas_add_seats_button: saas_add_seats_button)
end
it 'pushes the correct feature flags' do
visit group_billings_path(group)
expect(page).to have_pushed_frontend_feature_flags(
saasAddSeatsButton: saas_add_seats_button,
saasManualRenewButton: saas_manual_renew_button
)
end
end
end
end end
...@@ -7,6 +7,7 @@ import * as types from 'ee/billings/subscriptions/store/mutation_types'; ...@@ -7,6 +7,7 @@ import * as types from 'ee/billings/subscriptions/store/mutation_types';
import { mockDataSubscription } from 'ee_jest/billings/mock_data'; import { mockDataSubscription } from 'ee_jest/billings/mock_data';
import { TEST_HOST } from 'helpers/test_constants'; import { TEST_HOST } from 'helpers/test_constants';
import Vuex from 'vuex'; import Vuex from 'vuex';
import { extendedWrapper } from '../../../../../../spec/frontend/helpers/vue_test_utils_helper';
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';
...@@ -18,28 +19,41 @@ describe('SubscriptionTable component', () => { ...@@ -18,28 +19,41 @@ describe('SubscriptionTable component', () => {
let store; let store;
let wrapper; let wrapper;
const findAddSeatsButton = () => wrapper.findByTestId('add-seats');
const findButtonProps = () => const findButtonProps = () =>
wrapper.findAll('a').wrappers.map(x => ({ text: x.text(), href: x.attributes('href') })); wrapper.findAll('a').wrappers.map(x => ({ text: x.text(), href: x.attributes('href') }));
const findRenewButton = () => findButtonProps().filter(({ text }) => text === 'Renew'); const findRenewButton = () => findButtonProps().filter(({ text }) => text === 'Renew');
const factory = (options = {}) => { const createComponent = (
options = {},
{ saasManualRenewButton = false, saasAddSeatsButton = false } = {},
) => {
store = new Vuex.Store(initialStore()); store = new Vuex.Store(initialStore());
jest.spyOn(store, 'dispatch').mockImplementation(); jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(SubscriptionTable, { wrapper = extendedWrapper(
...options, shallowMount(SubscriptionTable, {
store, store,
localVue, localVue,
}); provide: {
glFeatures: {
saasManualRenewButton,
saasAddSeatsButton,
},
},
...options,
}),
);
}; };
afterEach(() => { afterEach(() => {
wrapper.destroy(); wrapper.destroy();
wrapper = null;
}); });
describe('when created', () => { describe('when created', () => {
beforeEach(() => { beforeEach(() => {
factory({ createComponent({
propsData: { propsData: {
namespaceName: TEST_NAMESPACE_NAME, namespaceName: TEST_NAMESPACE_NAME,
planUpgradeHref: '/url/', planUpgradeHref: '/url/',
...@@ -49,8 +63,6 @@ describe('SubscriptionTable component', () => { ...@@ -49,8 +63,6 @@ describe('SubscriptionTable component', () => {
}); });
Object.assign(store.state, { isLoadingSubscription: true }); Object.assign(store.state, { isLoadingSubscription: true });
return wrapper.vm.$nextTick();
}); });
it('shows loading icon', () => { it('shows loading icon', () => {
...@@ -68,7 +80,7 @@ describe('SubscriptionTable component', () => { ...@@ -68,7 +80,7 @@ describe('SubscriptionTable component', () => {
describe('with success', () => { describe('with success', () => {
beforeEach(() => { beforeEach(() => {
factory({ propsData: { namespaceName: TEST_NAMESPACE_NAME } }); createComponent({ propsData: { namespaceName: TEST_NAMESPACE_NAME } });
store.state.isLoadingSubscription = false; store.state.isLoadingSubscription = false;
store.commit(`${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold); store.commit(`${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold);
...@@ -103,7 +115,7 @@ describe('SubscriptionTable component', () => { ...@@ -103,7 +115,7 @@ describe('SubscriptionTable component', () => {
const planUpgradeHref = `${TEST_HOST}/plan/upgrade/${planName}`; const planUpgradeHref = `${TEST_HOST}/plan/upgrade/${planName}`;
const planRenewHref = `${TEST_HOST}/plan/renew`; const planRenewHref = `${TEST_HOST}/plan/renew`;
factory({ createComponent({
propsData: { propsData: {
namespaceName: TEST_NAMESPACE_NAME, namespaceName: TEST_NAMESPACE_NAME,
customerPortalUrl: CUSTOMER_PORTAL_URL, customerPortalUrl: CUSTOMER_PORTAL_URL,
...@@ -121,8 +133,6 @@ describe('SubscriptionTable component', () => { ...@@ -121,8 +133,6 @@ describe('SubscriptionTable component', () => {
upgradable, upgradable,
}, },
}); });
return wrapper.vm.$nextTick();
}); });
it(snapshotDesc, () => { it(snapshotDesc, () => {
...@@ -141,14 +151,12 @@ describe('SubscriptionTable component', () => { ...@@ -141,14 +151,12 @@ describe('SubscriptionTable component', () => {
'given plan with state: isFreePlan=$isFreePlan and feature flag saasManualRenewButton=$featureFlag', 'given plan with state: isFreePlan=$isFreePlan and feature flag saasManualRenewButton=$featureFlag',
({ planName, planCode, isFreePlan, featureFlag, testDescription, expectedBehavior }) => { ({ planName, planCode, isFreePlan, featureFlag, testDescription, expectedBehavior }) => {
beforeEach(() => { beforeEach(() => {
factory({ createComponent(
{
propsData: { namespaceName: TEST_NAMESPACE_NAME }, propsData: { namespaceName: TEST_NAMESPACE_NAME },
provide: {
glFeatures: {
saasManualRenewButton: featureFlag,
},
}, },
}); { saasManualRenewButton: featureFlag },
);
Object.assign(store.state, { Object.assign(store.state, {
isLoadingSubscription: false, isLoadingSubscription: false,
...@@ -166,4 +174,38 @@ describe('SubscriptionTable component', () => { ...@@ -166,4 +174,38 @@ describe('SubscriptionTable component', () => {
}); });
}, },
); );
describe.each`
planCode | featureFlag | expected | testDescription
${'silver'} | ${true} | ${true} | ${'renders the button'}
${'silver'} | ${false} | ${false} | ${'does not render the button'}
${null} | ${true} | ${false} | ${'does not render the button'}
${null} | ${false} | ${false} | ${'does not render the button'}
`(
'Add seats button – given plan with state: planCode = $planCode and saasAddSeatsButton = $featureFlag',
({ planCode, featureFlag, expected, testDescription }) => {
beforeEach(() => {
createComponent(
{
propsData: { namespaceName: TEST_NAMESPACE_NAME },
},
{
saasAddSeatsButton: featureFlag,
},
);
Object.assign(store.state, {
isLoadingSubscription: false,
plan: {
code: planCode,
upgradable: true,
},
});
});
it(testDescription, () => {
expect(findAddSeatsButton().exists()).toBe(expected);
});
},
);
}); });
...@@ -26501,6 +26501,9 @@ msgstr "" ...@@ -26501,6 +26501,9 @@ msgstr ""
msgid "Subscription successfully deleted." msgid "Subscription successfully deleted."
msgstr "" msgstr ""
msgid "SubscriptionTable|Add seats"
msgstr ""
msgid "SubscriptionTable|An error occurred while loading billable members list" msgid "SubscriptionTable|An error occurred while loading billable members list"
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