Commit 0f36d2ab authored by Stan Hu's avatar Stan Hu

Merge branch 'cloud-license-subscription-view' into 'master'

Cloud License Activation Form View

See merge request gitlab-org/gitlab!53952
parents 52b80b0f bac99949
<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