Commit bac99949 authored by Jake Burden's avatar Jake Burden Committed by Stan Hu

Cloud License Subscription

Add Activation Form View
Add controller for cloud license
Add FE graphql integration
Add FE tests for Activation Form View
Add Jest helpers for events
Add tests for controller
Add helper for navbar item
Disable input field while request is processing
Add namespace to translations
parent 2aab7082
<script>
import CloudLicenseSubscriptionActivationForm from './subscription_activation_form.vue';
export default {
name: 'CloudLicenseApp',
components: {
CloudLicenseSubscriptionActivationForm,
},
props: {
subscription: {
required: false,
type: Object,
default: null,
},
},
data() {
return {
subscriptionData: this.subscription,
};
},
};
</script>
<template>
<div class="gl-display-flex gl-justify-content-center gl-flex-direction-column">
<h3 class="gl-mb-7 gl-mt-6 gl-text-center">
{{ s__('CloudLicense|This instance is currently using the Core plan.') }}
</h3>
<cloud-license-subscription-activation-form v-if="!subscriptionData" />
</div>
</template>
<script>
import { GlButton, GlForm, GlFormGroup, GlFormInput } from '@gitlab/ui';
import activateSubscriptionMutation from 'ee/pages/admin/cloud_licenses/graphql/mutations/activate_subscription.mutation.graphql';
export const SUBSCRIPTION_ACTIVATION_EVENT = 'subscription-activation';
export default {
name: 'CloudLicenseSubscriptionActivationForm',
components: {
GlButton,
GlForm,
GlFormGroup,
GlFormInput,
},
data() {
return {
activationCode: null,
isLoading: false,
};
},
methods: {
submit() {
this.isLoading = true;
this.$apollo
.mutate({
mutation: activateSubscriptionMutation,
variables: {
gitlabSubscriptionActivateInput: {
activationCode: this.activationCode,
},
},
})
.then(
({
data: {
gitlabSubscriptionActivate: { errors },
},
}) => {
if (errors.length) {
throw new Error();
}
this.$emit(SUBSCRIPTION_ACTIVATION_EVENT, this.activationCode);
},
)
.catch(() => {
this.$emit(SUBSCRIPTION_ACTIVATION_EVENT, null);
})
.finally(() => {
this.isLoading = false;
});
},
},
};
</script>
<template>
<gl-form @submit.stop.prevent="submit">
<gl-form-group>
<div class="gl-display-flex gl-flex-wrap gl-justify-content-center">
<label class="gl-text-center gl-w-full" for="activation-code-group">
{{ s__('CloudLicense|Paste your activation code below') }}
</label>
<gl-form-input
id="activation-code-group"
v-model="activationCode"
:disabled="isLoading"
:placeholder="s__('CloudLicense|Paste your activation code')"
class="gl-mr-3"
required
size="xl"
/>
<gl-button
:disabled="isLoading"
category="primary"
class="gl-align-self-end"
data-testid="activate-button"
type="submit"
variant="confirm"
>
{{ s__('CloudLicense|Activate') }}
</gl-button>
</div>
</gl-form-group>
</gl-form>
</template>
mutation($gitlabSubscriptionActivateInput: GitlabSubscriptionActivateInput!) {
gitlabSubscriptionActivate(input: $gitlabSubscriptionActivateInput) {
clientMutationId
errors
}
}
import initShowCloudLicense from './mount_cloud_licenses';
initShowCloudLicense();
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import CloudLicenseShowApp from '../components/app.vue';
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('js-show-cloud-license-page');
return new Vue({
el,
apolloProvider,
render: (h) => h(CloudLicenseShowApp),
});
};
# frozen_string_literal: true
class Admin::CloudLicensesController < Admin::ApplicationController
respond_to :html
feature_category :license
before_action :require_cloud_license_enabled
def show; end
private
def require_cloud_license_enabled
redirect_to admin_license_path unless cloud_license_enabled?
end
def cloud_license_enabled?
Gitlab::CurrentSettings.cloud_license_enabled?
end
end
# frozen_string_literal: true
module Admin
module NavbarHelper
def navbar_item_name
cloud_license_enabled? ? _('Cloud License') : _('License')
end
def navbar_item_path
cloud_license_enabled? ? admin_cloud_license_path : admin_license_path
end
private
def cloud_license_enabled?
Gitlab::CurrentSettings.cloud_license_enabled?
end
end
end
- page_title _('Cloud License')
#js-show-cloud-license-page
= nav_link(controller: :licenses) do = nav_link(controller: controller) do
= link_to admin_license_path, class: "qa-link-license-menu" do = link_to navbar_item_path, class: "qa-link-license-menu" do
.nav-icon-container .nav-icon-container
= sprite_icon('license') = sprite_icon('license')
%span.nav-item-name %span.nav-item-name
= _('License') = navbar_item_name
%ul.sidebar-sub-level-items.is-fly-out-only %ul.sidebar-sub-level-items.is-fly-out-only
= nav_link(controller: :licenses, html_options: { class: "fly-out-top-item" } ) do = nav_link(controller: controller, html_options: { class: "fly-out-top-item" } ) do
= link_to admin_license_path do = link_to navbar_item_path do
%strong.fly-out-top-item-name %strong.fly-out-top-item-name
= _('License') = navbar_item_name
...@@ -32,6 +32,8 @@ namespace :admin do ...@@ -32,6 +32,8 @@ namespace :admin do
resource :usage_export, controller: 'licenses/usage_exports', only: [:show] resource :usage_export, controller: 'licenses/usage_exports', only: [:show]
end end
resource :cloud_license, only: [:show]
# using `only: []` to keep duplicate routes from being created # using `only: []` to keep duplicate routes from being created
resource :application_settings, only: [] do resource :application_settings, only: [] do
get :seat_link_payload get :seat_link_payload
......
import { shallowMount } from '@vue/test-utils';
import CloudLicenseApp from 'ee/pages/admin/cloud_licenses/components/app.vue';
import CloudLicenseSubscriptionActivationForm from 'ee/pages/admin/cloud_licenses/components/subscription_activation_form.vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
describe('CloudLicenseApp', () => {
let wrapper;
const findActivateSubscriptionForm = () =>
wrapper.findComponent(CloudLicenseSubscriptionActivationForm);
const createComponent = (props = {}) => {
wrapper = extendedWrapper(
shallowMount(CloudLicenseApp, {
propsData: {
...props,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('Subscription Activation Form', () => {
beforeEach(() => createComponent());
it('presents a form', () => {
expect(findActivateSubscriptionForm().exists()).toBe(true);
});
});
});
import { GlForm, GlFormInput } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
import CloudLicenseSubscriptionActivationForm, {
SUBSCRIPTION_ACTIVATION_EVENT,
} from 'ee/pages/admin/cloud_licenses/components/subscription_activation_form.vue';
import activateSubscriptionMutation from 'ee/pages/admin/cloud_licenses/graphql/mutations/activate_subscription.mutation.graphql';
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { preventDefault, stopPropagation } from '../../test_helpers';
import { activateLicenseMutationResponse } from '../mock_data';
const localVue = createLocalVue();
localVue.use(VueApollo);
describe('CloudLicenseApp', () => {
let wrapper;
const fakeActivationCode = 'gEg959hDCkvM2d4Der5RyktT';
const createMockApolloProvider = (resolverMock) => {
localVue.use(VueApollo);
return createMockApollo([[activateSubscriptionMutation, resolverMock]]);
};
const findActivateButton = () => wrapper.findByTestId('activate-button');
const findActivationCodeInput = () => wrapper.findComponent(GlFormInput);
const findActivateSubscriptionForm = () => wrapper.findComponent(GlForm);
const GlFormInputStub = stubComponent(GlFormInput, {
template: `<input />`,
});
const createFakeEvent = () => ({
preventDefault,
stopPropagation,
});
const createComponentWithApollo = (props = {}, resolverMock) => {
wrapper = extendedWrapper(
shallowMount(CloudLicenseSubscriptionActivationForm, {
localVue,
apolloProvider: createMockApolloProvider(resolverMock),
propsData: {
...props,
},
stubs: {
GlFormInput: GlFormInputStub,
},
}),
);
};
afterEach(() => {
wrapper.destroy();
});
describe('Subscription Activation Form', () => {
beforeEach(() => createComponentWithApollo());
it('presents a form', () => {
expect(findActivateSubscriptionForm().exists()).toBe(true);
});
it('has an input', () => {
expect(findActivationCodeInput().exists()).toBe(true);
});
it('has an `Activate` button', () => {
expect(findActivateButton().text()).toBe('Activate');
expect(findActivateButton().props('disabled')).toBe(false);
});
});
describe('Activate the subscription', () => {
describe('when submitting the form', () => {
const mutationMock = jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS);
beforeEach(() => {
createComponentWithApollo({}, mutationMock);
findActivationCodeInput().vm.$emit('input', fakeActivationCode);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
});
it('prevents default submit', () => {
expect(preventDefault).toHaveBeenCalled();
});
it('calls mutate with the correct variables', () => {
expect(mutationMock).toHaveBeenCalledWith({
gitlabSubscriptionActivateInput: {
activationCode: fakeActivationCode,
},
});
});
});
describe('when the mutation is successful', () => {
beforeEach(() => {
createComponentWithApollo(
{},
jest.fn().mockResolvedValue(activateLicenseMutationResponse.SUCCESS),
);
findActivationCodeInput().vm.$emit('input', fakeActivationCode);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
});
it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_EVENT)).toEqual([[fakeActivationCode]]);
});
});
describe('when the mutation is not successful but looks like it is', () => {
beforeEach(() => {
createComponentWithApollo(
{},
jest.fn().mockResolvedValue(activateLicenseMutationResponse.FAILURE_IN_DISGUISE),
);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
});
it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_EVENT)).toBeUndefined();
});
it.todo('deals with failures in a meaningful way');
});
describe('when the mutation is not successful', () => {
beforeEach(() => {
createComponentWithApollo(
{},
jest.fn().mockRejectedValue(activateLicenseMutationResponse.FAILURE),
);
findActivateSubscriptionForm().vm.$emit('submit', createFakeEvent());
});
it('emits a successful event', () => {
expect(wrapper.emitted(SUBSCRIPTION_ACTIVATION_EVENT)).toBeUndefined();
});
it.todo('deals with failures in a meaningful way');
});
});
});
export const activateLicenseMutationResponse = {
FAILURE: [
{
errors: [
{
message:
'Variable $gitlabSubscriptionActivateInput of type GitlabSubscriptionActivateInput! was provided invalid value',
locations: [
{
line: 1,
column: 11,
},
],
extensions: {
value: null,
problems: [
{
path: [],
explanation: 'Expected value to not be null',
},
],
},
},
],
},
],
FAILURE_IN_DISGUISE: {
data: {
gitlabSubscriptionActivate: {
clientMutationId: null,
errors: ["undefined method `[]' for nil:NilClass"],
__typename: 'GitlabSubscriptionActivatePayload',
},
},
},
SUCCESS: {
data: {
gitlabSubscriptionActivate: {
clientMutationId: null,
errors: [],
},
},
},
};
export const preventDefault = jest.fn();
export const stopPropagation = jest.fn();
# frozen_string_literal: true
require "spec_helper"
RSpec.describe Admin::NavbarHelper do
let_it_be(:current_user) { build(:user) }
describe 'when cloud license is enabled' do
before do
stub_application_setting(cloud_license_enabled: true)
end
it 'returns the correct navbar item name' do
expect(helper.navbar_item_name).to eq('Cloud License')
end
it 'returns the correct navbar item path' do
expect(helper.navbar_item_path).to eq(admin_cloud_license_path)
end
end
describe 'when cloud license is not enabled' do
before do
stub_application_setting(cloud_license_enabled: false)
end
it 'returns the correct navbar item name' do
expect(helper.navbar_item_name).to eq('License')
end
it 'returns the correct navbar item path' do
expect(helper.navbar_item_path).to eq(admin_license_path)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Admin::CloudLicensesController, :cloud_licenses do
include AdminModeHelper
describe 'GET /cloud_licenses' do
context 'when the user is not admin' do
let_it_be(:user) { create(:user) }
it 'responds with 404' do
sign_in(user)
send_request
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when the user an admin' do
let_it_be(:admin) { create(:admin) }
before do
login_as(admin)
enable_admin_mode!(admin)
end
context 'when the application setting is not active' do
before do
stub_application_setting(cloud_license_enabled: false)
end
it 'redirects to admin license path when the setting is not active' do
send_request
expect(response).to redirect_to admin_license_path
end
end
context 'when the application setting is active' do
before do
stub_application_setting(cloud_license_enabled: true)
end
it 'renders the Activation Form' do
send_request
expect(response).to render_template(:show)
expect(response.body).to include('js-show-cloud-license-pag')
end
end
end
end
def send_request
get admin_cloud_license_path
end
end
...@@ -6277,6 +6277,21 @@ msgstr "" ...@@ -6277,6 +6277,21 @@ msgstr ""
msgid "Closes this %{quick_action_target}." msgid "Closes this %{quick_action_target}."
msgstr "" msgstr ""
msgid "Cloud License"
msgstr ""
msgid "CloudLicense|Activate"
msgstr ""
msgid "CloudLicense|Paste your activation code"
msgstr ""
msgid "CloudLicense|Paste your activation code below"
msgstr ""
msgid "CloudLicense|This instance is currently using the Core plan."
msgstr ""
msgid "Cluster" msgid "Cluster"
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