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
This commit is contained in:
Enrique Alcantara 2019-05-29 14:56:35 -04:00
parent 6418ffacfa
commit 60c58c7570
5 changed files with 365 additions and 205 deletions

View File

@ -353,8 +353,10 @@ export default class Clusters {
saveKnativeDomain(data) { saveKnativeDomain(data) {
const appId = data.id; const appId = data.id;
this.store.updateAppProperty(appId, 'status', APPLICATION_STATUS.UPDATING); this.store.updateApplication(appId);
this.service.updateApplication(appId, data.params); this.service.updateApplication(appId, data.params).catch(() => {
this.store.notifyUpdateFailure(appId);
});
} }
setKnativeHostname(data) { setKnativeHostname(data) {

View File

@ -15,6 +15,7 @@ import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue'; import applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.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 { CLUSTER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
import LoadingButton from '~/vue_shared/components/loading_button.vue'; import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/clusters/event_hub'; import eventHub from '~/clusters/event_hub';
@ -25,6 +26,7 @@ export default {
clipboardButton, clipboardButton,
LoadingButton, LoadingButton,
GlLoadingIcon, GlLoadingIcon,
KnativeDomainEditor,
}, },
props: { props: {
type: { type: {
@ -154,64 +156,21 @@ export default {
knative() { knative() {
return this.applications.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: `<strong><a href="https://cloud.google.com/compute/pricing#lb"
target="_blank" rel="noopener noreferrer">
${_.escape(s__('ClusterIntegration|pricing'))}</a></strong>`,
},
false,
);
},
canUpdateKnativeEndpoint() {
return this.knativeExternalEndpoint && !this.knativeUpgradeFailed && !this.knativeUpgrading;
},
knativeHostname: {
get() {
return this.knative.hostname;
},
set(hostname) {
eventHub.$emit('setKnativeHostname', {
id: 'knative',
hostname,
});
},
},
}, },
created() { created() {
this.helmInstallIllustration = helmInstallIllustration; this.helmInstallIllustration = helmInstallIllustration;
}, },
methods: { methods: {
saveKnativeDomain() { saveKnativeDomain(hostname) {
eventHub.$emit('saveKnativeDomain', { eventHub.$emit('saveKnativeDomain', {
id: 'knative', 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 generated endpoint in order to access
your application after it has been deployed.`) your application after it has been deployed.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
__('More information') {{ __('More information') }}
}}</a> </a>
</p> </p>
</div> </div>
@ -330,9 +289,9 @@ export default {
the process of being assigned. Please check your Kubernetes the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
__('More information') {{ __('More information') }}
}}</a> </a>
</p> </p>
</template> </template>
<template v-if="!ingressInstalled"> <template v-if="!ingressInstalled">
@ -361,9 +320,9 @@ export default {
<div slot="description"> <div slot="description">
<p v-html="certManagerDescription"></p> <p v-html="certManagerDescription"></p>
<div class="form-group"> <div class="form-group">
<label for="cert-manager-issuer-email">{{ <label for="cert-manager-issuer-email">
s__('ClusterIntegration|Issuer Email') {{ s__('ClusterIntegration|Issuer Email') }}
}}</label> </label>
<div class="input-group"> <div class="input-group">
<input <input
v-model="applications.cert_manager.email" v-model="applications.cert_manager.email"
@ -491,9 +450,9 @@ export default {
s__(`ClusterIntegration|Replace this with your own hostname if you want. s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`) If you do so, point hostname to Ingress IP Address from above.`)
}} }}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{ <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
__('More information') {{ __('More information') }}
}}</a> </a>
</p> </p>
</div> </div>
</template> </template>
@ -514,6 +473,7 @@ export default {
:uninstallable="applications.knative.uninstallable" :uninstallable="applications.knative.uninstallable"
:uninstall-successful="applications.knative.uninstallSuccessful" :uninstall-successful="applications.knative.uninstallSuccessful"
:uninstall-failed="applications.knative.uninstallFailed" :uninstall-failed="applications.knative.uninstallFailed"
:updateable="false"
:disabled="!helmInstalled" :disabled="!helmInstalled"
v-bind="applications.knative" v-bind="applications.knative"
title-link="https://github.com/knative/docs" title-link="https://github.com/knative/docs"
@ -525,9 +485,9 @@ export default {
s__(`ClusterIntegration|You must have an RBAC-enabled cluster s__(`ClusterIntegration|You must have an RBAC-enabled cluster
to install Knative.`) to install Knative.`)
}} }}
<a :href="helpPath" target="_blank" rel="noopener noreferrer">{{ <a :href="helpPath" target="_blank" rel="noopener noreferrer">
__('More information') {{ __('More information') }}
}}</a> </a>
</p> </p>
<br /> <br />
</span> </span>
@ -540,83 +500,13 @@ export default {
}} }}
</p> </p>
<div class="row"> <knative-domain-editor
<template v-if="knativeInstalled || (helmInstalled && rbac)"> v-if="knative.installed || (helmInstalled && rbac)"
<div :knative="knative"
:class="{ 'col-md-6': knativeInstalled, 'col-12': helmInstalled && rbac }" :ingress-dns-help-path="ingressDnsHelpPath"
class="form-group col-sm-12 mb-0" @save="saveKnativeDomain"
> @set="setKnativeHostname"
<label for="knative-domainname"> />
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/>
</div>
</template>
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
<strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
id="knative-endpoint"
:value="knativeExternalEndpoint"
type="text"
class="form-control js-knative-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
class="input-group-text js-knative-endpoint-clipboard-btn"
/>
</span>
</div>
<div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
/>
</div>
</div>
<p class="form-text text-muted col-12">
{{
s__(
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
__('More information')
}}</a>
</p>
<p
v-if="!knativeExternalEndpoint"
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
>
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
<button
v-if="canUpdateKnativeEndpoint"
class="btn btn-success js-knative-save-domain-button mt-3 ml-3"
@click="saveKnativeDomain"
>
{{ s__('ClusterIntegration|Save changes') }}
</button>
</template>
</div>
</div> </div>
</application-row> </application-row>
</div> </div>

View File

@ -0,0 +1,150 @@
<script>
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { s__ } from '~/locale';
import { APPLICATION_STATUS } from '~/clusters/constants';
const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
export default {
components: {
LoadingButton,
ClipboardButton,
GlLoadingIcon,
},
props: {
knative: {
type: Object,
required: true,
},
ingressDnsHelpPath: {
type: String,
default: '',
},
},
computed: {
saveButtonDisabled() {
return [UNINSTALLING, UPDATING].includes(this.knative.status);
},
saving() {
return [UPDATING].includes(this.knative.status);
},
saveButtonLabel() {
return this.saving ? this.__('Saving') : this.__('Save changes');
},
knativeInstalled() {
return this.knative.installed;
},
knativeExternalEndpoint() {
return this.knative.externalIp || this.knative.externalHostname;
},
knativeUpdateSuccessful() {
return this.knative.updateSuccessful;
},
knativeHostname: {
get() {
return this.knative.hostname;
},
set(hostname) {
this.$emit('set', hostname);
},
},
},
watch: {
knativeUpdateSuccessful(updateSuccessful) {
if (updateSuccessful) {
this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
}
},
},
};
</script>
<template>
<div class="row">
<div
v-if="knative.updateFailed"
class="bs-callout bs-callout-danger cluster-application-banner col-12 mt-2 mb-2 js-cluster-knative-domain-name-failure-message"
>
{{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
</div>
<template>
<div
:class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
class="form-group col-sm-12 mb-0"
>
<label for="knative-domainname">
<strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
</label>
<input
id="knative-domainname"
v-model="knativeHostname"
type="text"
class="form-control js-knative-domainname"
/>
</div>
</template>
<template v-if="knativeInstalled">
<div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
<label for="knative-endpoint">
<strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
</label>
<div v-if="knativeExternalEndpoint" class="input-group">
<input
id="knative-endpoint"
:value="knativeExternalEndpoint"
type="text"
class="form-control js-knative-endpoint"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="knativeExternalEndpoint"
:title="s__('ClusterIntegration|Copy Knative Endpoint to clipboard')"
class="input-group-text js-knative-endpoint-clipboard-btn"
/>
</span>
</div>
<div v-else class="input-group">
<input type="text" class="form-control js-endpoint" readonly />
<gl-loading-icon
class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
/>
</div>
</div>
<p class="form-text text-muted col-12">
{{
s__(
`ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
)
}}
<a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">
{{ __('More information') }}
</a>
</p>
<p
v-if="!knativeExternalEndpoint"
class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
>
{{
s__(`ClusterIntegration|The endpoint is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
}}
</p>
<loading-button
class="btn-success js-knative-save-domain-button mt-3 ml-3"
:loading="saving"
:disabled="saveButtonDisabled"
:label="saveButtonLabel"
@click="$emit('save', knativeHostname)"
/>
</template>
</div>
</template>

View File

@ -1,9 +1,11 @@
import Vue from 'vue'; import Vue from 'vue';
import applications from '~/clusters/components/applications.vue'; import applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE } from '~/clusters/constants'; import { CLUSTER_TYPE } from '~/clusters/constants';
import eventHub from '~/clusters/event_hub';
import mountComponent from 'helpers/vue_mount_component_helper'; import mountComponent from 'helpers/vue_mount_component_helper';
import { APPLICATIONS_MOCK_STATE } from '../services/mock_data'; 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', () => { describe('Applications', () => {
let vm; let vm;
@ -277,73 +279,48 @@ describe('Applications', () => {
}); });
describe('Knative application', () => { describe('Knative application', () => {
describe('when installed', () => { const propsData = {
describe('with ip address', () => { applications: {
const props = { ...APPLICATIONS_MOCK_STATE,
applications: { knative: {
...APPLICATIONS_MOCK_STATE, title: 'Knative',
knative: { hostname: 'example.com',
title: 'Knative', status: 'installed',
hostname: 'example.com', externalIp: '1.1.1.1',
status: 'installed', installed: true,
externalIp: '1.1.1.1', },
}, },
}, };
}; const newHostname = 'newhostname.com';
it('renders ip address with a clipboard button', () => { let wrapper;
vm = mountComponent(Applications, props); 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( knativeDomainEditor = wrapper.find(KnativeDomainEditor);
vm.$el });
.querySelector('.js-knative-endpoint-clipboard-btn')
.getAttribute('data-clipboard-text'),
).toEqual('1.1.1.1');
});
it('renders domain & allows editing', () => { afterEach(() => {
expect(vm.$el.querySelector('.js-knative-domainname').value).toEqual('example.com'); wrapper.destroy();
expect(vm.$el.querySelector('.js-knative-domainname').getAttribute('readonly')).toBe( });
null,
);
});
it('renders an update/save Knative domain button', () => { it('emits saveKnativeDomain event when knative domain editor emits save event', () => {
expect(vm.$el.querySelector('.js-knative-save-domain-button')).not.toBe(null); knativeDomainEditor.vm.$emit('save', newHostname);
});
it('emits event when clicking Save changes button', () => { expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
jest.spyOn(eventHub, '$emit'); id: 'knative',
vm = mountComponent(Applications, props); params: { hostname: newHostname },
const saveButton = vm.$el.querySelector('.js-knative-save-domain-button');
saveButton.click();
expect(eventHub.$emit).toHaveBeenCalledWith('saveKnativeDomain', {
id: 'knative',
params: { hostname: 'example.com' },
});
});
}); });
});
describe('without ip address', () => { it('emits setKnativeHostname event when knative domain editor emits change event', () => {
it('renders an input text with a loading icon and an alert text', () => { wrapper.find(KnativeDomainEditor).vm.$emit('set', newHostname);
vm = mountComponent(Applications, {
applications: {
...APPLICATIONS_MOCK_STATE,
knative: {
title: 'Knative',
hostname: 'example.com',
status: 'installed',
},
},
});
expect(vm.$el.querySelector('.js-knative-ip-loading-icon')).not.toBe(null); expect(eventHub.$emit).toHaveBeenCalledWith('setKnativeHostname', {
expect(vm.$el.querySelector('.js-no-knative-endpoint-message')).not.toBe(null); id: 'knative',
}); hostname: newHostname,
}); });
}); });
}); });

View File

@ -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');
});
});
});