From 60c58c757039bea7b141605bdb3b947eb10d0068 Mon Sep 17 00:00:00 2001 From: Enrique Alcantara Date: Wed, 29 May 2019 14:56:35 -0400 Subject: [PATCH] Extract knative domain editor into a component The new component also implements several improvements in the knative domain editor workflow: - Display a loading spinner when saving changes in the domain name - Display success toast message indicating changes were saved successfully. - Display error message in the contraty occurs --- .../javascripts/clusters/clusters_bundle.js | 6 +- .../clusters/components/applications.vue | 176 ++++-------------- .../components/knative_domain_editor.vue | 150 +++++++++++++++ .../clusters/components/applications_spec.js | 97 ++++------ .../components/knative_domain_editor_spec.js | 141 ++++++++++++++ 5 files changed, 365 insertions(+), 205 deletions(-) create mode 100644 app/assets/javascripts/clusters/components/knative_domain_editor.vue create mode 100644 spec/frontend/clusters/components/knative_domain_editor_spec.js diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js index 70af333a0dd..bc2e71b99f2 100644 --- a/app/assets/javascripts/clusters/clusters_bundle.js +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -353,8 +353,10 @@ export default class Clusters { saveKnativeDomain(data) { const appId = data.id; - this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); - this.service.updateApplication(appId, data.params); + this.store.updateApplication(appId); + this.service.updateApplication(appId, data.params).catch(() => { + this.store.notifyUpdateFailure(appId); + }); } setKnativeHostname(data) { diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue index 73760da9b98..2d129245d37 100644 --- a/app/assets/javascripts/clusters/components/applications.vue +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png'; import { s__, sprintf } from '../../locale'; import applicationRow from './application_row.vue'; import clipboardButton from '../../vue_shared/components/clipboard_button.vue'; +import KnativeDomainEditor from './knative_domain_editor.vue'; import { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants'; import LoadingButton from '~/vue_shared/components/loading_button.vue'; import eventHub from '~/clusters/event_hub'; @@ -25,6 +26,7 @@ export default { clipboardButton, LoadingButton, GlLoadingIcon, + KnativeDomainEditor, }, props: { type: { @@ -154,64 +156,21 @@ export default { knative() { return this.applications.knative; }, - knativeInstalled() { - return ( - this.knative.status === APPLICATION_STATUS.INSTALLED || - this.knativeUpgrading || - this.knativeUpgradeFailed || - this.knative.status === APPLICATION_STATUS.UPDATED - ); - }, - knativeUpgrading() { - return ( - this.knative.status === APPLICATION_STATUS.UPDATING || - this.knative.status === APPLICATION_STATUS.SCHEDULED - ); - }, - knativeUpgradeFailed() { - return this.knative.status === APPLICATION_STATUS.UPDATE_ERRORED; - }, - knativeExternalEndpoint() { - return this.knative.externalIp || this.knative.externalHostname; - }, - knativeDescription() { - return sprintf( - _.escape( - s__( - `ClusterIntegration|Installing Knative may incur additional costs. Learn more about %{pricingLink}.`, - ), - ), - { - pricingLink: ` - ${_.escape(s__('ClusterIntegration|pricing'))}`, - }, - false, - ); - }, - canUpdateKnativeEndpoint() { - return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading; - }, - knativeHostname: { - get() { - return this.knative.hostname; - }, - set(hostname) { - eventHub.$emit('setKnativeHostname', { - id: 'knative', - hostname, - }); - }, - }, }, created() { this.helmInstallIllustration = helmInstallIllustration; }, methods: { - saveKnativeDomain() { + saveKnativeDomain(hostname) { eventHub.$emit('saveKnativeDomain', { id: 'knative', - params: { hostname: this.knative.hostname }, + params: { hostname }, + }); + }, + setKnativeHostname(hostname) { + eventHub.$emit('setKnativeHostname', { + id: 'knative', + hostname, }); }, }, @@ -318,9 +277,9 @@ export default { generated endpoint in order to access your application after it has been deployed.`) }} - {{ - __('More information') - }} + + {{ __('More information') }} +

@@ -330,9 +289,9 @@ export default { the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }} - {{ - __('More information') - }} + + {{ __('More information') }} +

@@ -514,6 +473,7 @@ export default { :uninstallable="applications.knative.uninstallable" :uninstall-successful="applications.knative.uninstallSuccessful" :uninstall-failed="applications.knative.uninstallFailed" + :updateable="false" :disabled="!helmInstalled" v-bind="applications.knative" title-link="https://github.com/knative/docs" @@ -525,9 +485,9 @@ export default { s__(`ClusterIntegration|You must have an RBAC-enabled cluster to install Knative.`) }} - {{ - __('More information') - }} + + {{ __('More information') }} +


@@ -540,83 +500,13 @@ export default { }}

-
- - -
+ diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue new file mode 100644 index 00000000000..480228619a5 --- /dev/null +++ b/app/assets/javascripts/clusters/components/knative_domain_editor.vue @@ -0,0 +1,150 @@ + + + diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js index 8bcf02f0a34..221ebb143be 100644 --- a/spec/frontend/clusters/components/applications_spec.js +++ b/spec/frontend/clusters/components/applications_spec.js @@ -1,9 +1,11 @@ import Vue from 'vue'; import applications from '~/clusters/components/applications.vue'; import { CLUSTER_TYPE } from '~/clusters/constants'; -import eventHub from '~/clusters/event_hub'; import mountComponent from 'helpers/vue_mount_component_helper'; import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; +import eventHub from '~/clusters/event_hub'; +import { shallowMount } from '@vue/test-utils'; +import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue'; describe('Applications', () => { let vm; @@ -277,73 +279,48 @@ describe('Applications', () => { }); describe('Knative application', () => { - describe('when installed', () => { - describe('with ip address', () => { - const props = { - applications: { - ...APPLICATIONS_MOCK_STATE, - knative: { - title: 'Knative', - hostname: 'example.com', - status: 'installed', - externalIp: '1.1.1.1', - }, - }, - }; - it('renders ip address with a clipboard button', () => { - vm = mountComponent(Applications, props); + const propsData = { + applications: { + ...APPLICATIONS_MOCK_STATE, + knative: { + title: 'Knative', + hostname: 'example.com', + status: 'installed', + externalIp: '1.1.1.1', + installed: true, + }, + }, + }; + const newHostname = 'newhostname.com'; + let wrapper; + let knativeDomainEditor; - expect(vm.$el.querySelector('.js-knative-endpoint').value).toEqual('1.1.1.1'); + beforeEach(() => { + wrapper = shallowMount(Applications, { propsData }); + jest.spyOn(eventHub, '$emit'); - expect( - vm.$el - .querySelector('.js-knative-endpoint-clipboard-btn') - .getAttribute('data-clipboard-text'), - ).toEqual('1.1.1.1'); - }); + knativeDomainEditor = wrapper.find(KnativeDomainEditor); + }); - it('renders domain & allows editing', () => { - expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com'); - expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe( - null, - ); - }); + afterEach(() => { + wrapper.destroy(); + }); - it('renders an update/save Knative domain button', () => { - expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null); - }); + it('emits saveKnativeDomain event when knative domain editor emits save event', () => { + knativeDomainEditor.vm.$emit('save', newHostname); - it('emits event when clicking Save changes button', () => { - jest.spyOn(eventHub, '$emit'); - vm = mountComponent(Applications, props); - - const saveButton = vm.$el.querySelector('.js-knative-save-domain-button'); - - saveButton.click(); - - expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { - id: 'knative', - params: { hostname: 'example.com' }, - }); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', { + id: 'knative', + params: { hostname: newHostname }, }); + }); - describe('without ip address', () => { - it('renders an input text with a loading icon and an alert text', () => { - vm = mountComponent(Applications, { - applications: { - ...APPLICATIONS_MOCK_STATE, - knative: { - title: 'Knative', - hostname: 'example.com', - status: 'installed', - }, - }, - }); + it('emits setKnativeHostname event when knative domain editor emits change event', () => { + wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname); - expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null); - expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null); - }); + expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', { + id: 'knative', + hostname: newHostname, }); }); }); diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js new file mode 100644 index 00000000000..242b5701f8b --- /dev/null +++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js @@ -0,0 +1,141 @@ +import { shallowMount } from '@vue/test-utils'; +import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue'; +import LoadingButton from '~/vue_shared/components/loading_button.vue'; +import { APPLICATION_STATUS } from '~/clusters/constants'; + +const { UPDATING } = APPLICATION_STATUS; + +describe('KnativeDomainEditor', () => { + let wrapper; + let knative; + + const createComponent = (props = {}) => { + wrapper = shallowMount(KnativeDomainEditor, { + propsData: { ...props }, + }); + }; + + beforeEach(() => { + knative = { + title: 'Knative', + hostname: 'example.com', + installed: true, + }; + }); + + afterEach(() => { + wrapper.destroy(); + }); + + describe('knative has an assigned IP address', () => { + beforeEach(() => { + knative.externalIp = '1.1.1.1'; + createComponent({ knative }); + }); + + it('renders ip address with a clipboard button', () => { + expect(wrapper.find('.js-knative-endpoint').exists()).toBe(true); + expect(wrapper.find('.js-knative-endpoint').element.value).toEqual(knative.externalIp); + }); + + it('displays ip address clipboard button', () => { + expect(wrapper.find('.js-knative-endpoint-clipboard-btn').attributes('text')).toEqual( + knative.externalIp, + ); + }); + + it('renders domain & allows editing', () => { + const domainNameInput = wrapper.find('.js-knative-domainname'); + + expect(domainNameInput.element.value).toEqual(knative.hostname); + expect(domainNameInput.attributes('readonly')).toBeFalsy(); + }); + + it('renders an update/save Knative domain button', () => { + expect(wrapper.find('.js-knative-save-domain-button').exists()).toBe(true); + }); + }); + + describe('knative without ip address', () => { + beforeEach(() => { + knative.externalIp = null; + createComponent({ knative }); + }); + + it('renders an input text with a loading icon', () => { + expect(wrapper.find('.js-knative-ip-loading-icon').exists()).toBe(true); + }); + + it('renders message indicating there is not IP address assigned', () => { + expect(wrapper.find('.js-no-knative-endpoint-message').exists()).toBe(true); + }); + }); + + describe('clicking save changes button', () => { + beforeEach(() => { + createComponent({ knative }); + }); + + it('triggers save event and pass current knative hostname', () => { + wrapper.find(LoadingButton).vm.$emit('click'); + expect(wrapper.emitted('save')[0]).toEqual([knative.hostname]); + }); + }); + + describe('when knative domain name was saved successfully', () => { + beforeEach(() => { + createComponent({ knative }); + }); + + it('displays toast indicating a successful update', () => { + wrapper.vm.$toast = { show: jest.fn() }; + wrapper.setProps({ knative: Object.assign({ updateSuccessful: true }, knative) }); + + return wrapper.vm.$nextTick(() => { + expect(wrapper.vm.$toast.show).toHaveBeenCalledWith( + 'Knative domain name was updated successfully.', + ); + }); + }); + }); + + describe('when knative domain name input changes', () => { + it('emits "set" event with updated domain name', () => { + const newHostname = 'newhostname.com'; + + wrapper.setData({ knativeHostname: newHostname }); + + expect(wrapper.emitted('set')[0]).toEqual([newHostname]); + }); + }); + + describe('when updating knative domain name failed', () => { + beforeEach(() => { + createComponent({ knative }); + }); + + it('displays an error banner indicating the operation failure', () => { + wrapper.setProps({ knative: { updateFailed: true, ...knative } }); + + expect(wrapper.find('.js-cluster-knative-domain-name-failure-message').exists()).toBe(true); + }); + }); + + describe(`when knative status is ${UPDATING}`, () => { + beforeEach(() => { + createComponent({ knative: { status: UPDATING, ...knative } }); + }); + + it('renders loading spinner in save button', () => { + expect(wrapper.find(LoadingButton).props('loading')).toBe(true); + }); + + it('renders disabled save button', () => { + expect(wrapper.find(LoadingButton).props('disabled')).toBe(true); + }); + + it('renders save button with "Saving" label', () => { + expect(wrapper.find(LoadingButton).props('label')).toBe('Saving'); + }); + }); +});