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';
import { TABLE_TYPE_DEFAULT, TABLE_TYPE_FREE, TABLE_TYPE_TRIAL } from 'ee/billings/constants';
import { s__ } from '~/locale';
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 {
name: 'SubscriptionTable',
......@@ -13,7 +15,7 @@ export default {
SubscriptionTableRow,
GlLoadingIcon,
},
mixins: [glFeatureFlagsMixin()],
mixins: [glFeaturesFlagMixin()],
props: {
namespaceName: {
type: String,
......@@ -35,6 +37,11 @@ export default {
default: '',
},
},
inject: {
addSeatsHref: {
default: '',
},
},
computed: {
...mapState(['isLoadingSubscription', 'hasErrorSubscription', 'plan', 'tables', 'endpoint']),
...mapGetters(['isFreePlan']),
......@@ -44,43 +51,45 @@ export default {
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() {
if (!this.isFreePlan && !this.plan.upgradable) {
return null;
}
return {
text: s__('SubscriptionTable|Upgrade'),
href:
!this.isFreePlan && this.planUpgradeHref ? this.planUpgradeHref : this.customerPortalUrl,
};
return this.canUpgrade
? createButtonProps(s__('SubscriptionTable|Upgrade'), this.upgradeButtonHref)
: null;
},
upgradeButtonHref() {
return this.canUpgradeEEPlan ? this.planUpgradeHref : this.customerPortalUrl;
},
renewButton() {
if (!this.glFeatures.saasManualRenewButton) {
return null;
}
if (this.isFreePlan) {
return null;
}
return {
text: s__('SubscriptionTable|Renew'),
href: this.planRenewHref,
};
return this.canRenew
? createButtonProps(s__('SubscriptionTable|Renew'), this.planRenewHref)
: null;
},
manageButton() {
if (this.isFreePlan) {
return null;
}
return {
text: s__('SubscriptionTable|Manage'),
href: this.customerPortalUrl,
};
return !this.isFreePlan
? createButtonProps(s__('SubscriptionTable|Manage'), this.customerPortalUrl)
: null;
},
buttons() {
return [this.upgradeButton, this.renewButton, this.manageButton].filter(Boolean);
return [this.upgradeButton, this.addSeatsButton, this.renewButton, this.manageButton].filter(
Boolean,
);
},
visibleRows() {
let tableKey = TABLE_TYPE_DEFAULT;
......@@ -119,7 +128,8 @@ export default {
target="_blank"
rel="noopener noreferrer"
class="btn btn-inverted-secondary"
:class="{ 'ml-2': index !== 0 }"
:class="{ 'gl-ml-3': index !== 0 }"
:data-testid="button.testId"
>{{ button.text }}</a
>
</div>
......
......@@ -15,6 +15,7 @@ export default (containerId = 'js-billing-plans') => {
const {
namespaceId,
namespaceName,
addSeatsHref,
planUpgradeHref,
planRenewHref,
customerPortalUrl,
......@@ -27,6 +28,7 @@ export default (containerId = 'js-billing-plans') => {
provide: {
namespaceId,
namespaceName,
addSeatsHref,
planUpgradeHref,
planRenewHref,
customerPortalUrl,
......
......@@ -5,9 +5,9 @@ require 'spec_helper'
RSpec.describe 'Groups > Billing', :js do
include StubRequests
let!(:user) { create(:user) }
let!(:group) { create(:group) }
let!(:bronze_plan) { create(:bronze_plan) }
let_it_be(:user) { create(:user) }
let_it_be(:group) { create(:group) }
let_it_be(:bronze_plan) { create(:bronze_plan) }
def formatted_date(date)
date.strftime("%B %-d, %Y")
......@@ -19,6 +19,7 @@ RSpec.describe 'Groups > Billing', :js do
before do
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')))
allow(Gitlab).to receive(:com?).and_return(true)
......@@ -50,21 +51,40 @@ RSpec.describe 'Groups > Billing', :js do
context 'with a paid plan' do
let(:plan) { 'bronze' }
let!(:subscription) do
let_it_be(:subscription) do
create(:gitlab_subscription, namespace: group, hosted_plan: bronze_plan, seats: 15)
end
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 =
"#{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")
within subscription_table do
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("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
......@@ -86,4 +106,37 @@ RSpec.describe 'Groups > Billing', :js do
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
......@@ -7,6 +7,7 @@ import * as types from 'ee/billings/subscriptions/store/mutation_types';
import { mockDataSubscription } from 'ee_jest/billings/mock_data';
import { TEST_HOST } from 'helpers/test_constants';
import Vuex from 'vuex';
import { extendedWrapper } from '../../../../../../spec/frontend/helpers/vue_test_utils_helper';
const TEST_NAMESPACE_NAME = 'GitLab.com';
const CUSTOMER_PORTAL_URL = 'https://customers.gitlab.com/subscriptions';
......@@ -18,28 +19,41 @@ describe('SubscriptionTable component', () => {
let store;
let wrapper;
const findAddSeatsButton = () => wrapper.findByTestId('add-seats');
const findButtonProps = () =>
wrapper.findAll('a').wrappers.map(x => ({ text: x.text(), href: x.attributes('href') }));
const findRenewButton = () => findButtonProps().filter(({ text }) => text === 'Renew');
const factory = (options = {}) => {
const createComponent = (
options = {},
{ saasManualRenewButton = false, saasAddSeatsButton = false } = {},
) => {
store = new Vuex.Store(initialStore());
jest.spyOn(store, 'dispatch').mockImplementation();
wrapper = shallowMount(SubscriptionTable, {
...options,
wrapper = extendedWrapper(
shallowMount(SubscriptionTable, {
store,
localVue,
});
provide: {
glFeatures: {
saasManualRenewButton,
saasAddSeatsButton,
},
},
...options,
}),
);
};
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
describe('when created', () => {
beforeEach(() => {
factory({
createComponent({
propsData: {
namespaceName: TEST_NAMESPACE_NAME,
planUpgradeHref: '/url/',
......@@ -49,8 +63,6 @@ describe('SubscriptionTable component', () => {
});
Object.assign(store.state, { isLoadingSubscription: true });
return wrapper.vm.$nextTick();
});
it('shows loading icon', () => {
......@@ -68,7 +80,7 @@ describe('SubscriptionTable component', () => {
describe('with success', () => {
beforeEach(() => {
factory({ propsData: { namespaceName: TEST_NAMESPACE_NAME } });
createComponent({ propsData: { namespaceName: TEST_NAMESPACE_NAME } });
store.state.isLoadingSubscription = false;
store.commit(`${types.RECEIVE_SUBSCRIPTION_SUCCESS}`, mockDataSubscription.gold);
......@@ -103,7 +115,7 @@ describe('SubscriptionTable component', () => {
const planUpgradeHref = `${TEST_HOST}/plan/upgrade/${planName}`;
const planRenewHref = `${TEST_HOST}/plan/renew`;
factory({
createComponent({
propsData: {
namespaceName: TEST_NAMESPACE_NAME,
customerPortalUrl: CUSTOMER_PORTAL_URL,
......@@ -121,8 +133,6 @@ describe('SubscriptionTable component', () => {
upgradable,
},
});
return wrapper.vm.$nextTick();
});
it(snapshotDesc, () => {
......@@ -141,14 +151,12 @@ describe('SubscriptionTable component', () => {
'given plan with state: isFreePlan=$isFreePlan and feature flag saasManualRenewButton=$featureFlag',
({ planName, planCode, isFreePlan, featureFlag, testDescription, expectedBehavior }) => {
beforeEach(() => {
factory({
createComponent(
{
propsData: { namespaceName: TEST_NAMESPACE_NAME },
provide: {
glFeatures: {
saasManualRenewButton: featureFlag,
},
},
});
{ saasManualRenewButton: featureFlag },
);
Object.assign(store.state, {
isLoadingSubscription: false,
......@@ -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 ""
msgid "Subscription successfully deleted."
msgstr ""
msgid "SubscriptionTable|Add seats"
msgstr ""
msgid "SubscriptionTable|An error occurred while loading billable members list"
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