Disables jupyter install button while ingress is not installed

Includes juptyer hostname in the post request
Adds tests
This commit is contained in:
Filipa Lacerda 2018-05-24 21:00:21 +01:00 committed by Dmitriy Zaporozhets
parent b3cf153082
commit e1d4deb2d2
10 changed files with 212 additions and 72 deletions

View File

@ -211,11 +211,12 @@ export default class Clusters {
}
}
installApplication(appId) {
installApplication(data) {
const appId = data.id;
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId)
this.service.installApplication(appId, data.params)
.then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
})

View File

@ -52,6 +52,16 @@
type: String,
required: false,
},
disableInstallButton: {
type: Boolean,
required: false,
default: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
},
computed: {
rowJsClass() {
@ -67,7 +77,7 @@
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but
// we already made a request to install and are just waiting for the real-time
// to sync up.
return (this.status !== APPLICATION_INSTALLABLE
return this.disableInstallButton || (this.status !== APPLICATION_INSTALLABLE
&& this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS;
@ -109,7 +119,10 @@
},
methods: {
installClicked() {
eventHub.$emit('installApplication', this.id);
eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
},
},
};

View File

@ -37,11 +37,6 @@ export default {
default: '',
},
},
data() {
return {
jupyterSuggestHostnameValue: '',
};
},
computed: {
generalApplicationDescription() {
return sprintf(
@ -132,14 +127,6 @@ export default {
jupyterHostname() {
return this.applications.jupyter.hostname;
},
jupyterSuggestHostname() {
return `jupyter.${this.applications.ingress.externalIp}.xip.io`;
},
},
watch: {
jupyterSuggestHostname() {
this.jupyterSuggestHostnameValue = this.jupyterSuggestHostname;
},
},
};
</script>
@ -305,6 +292,8 @@ export default {
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:disable-install-button="!ingressInstalled"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
>
<div slot="description">
<p>
@ -314,45 +303,23 @@ export default {
notebooks to a class of students, a corporate data science group,
or a scientific research group.`) }}
</p>
<template v-if="jupyterInstalled">
<div class="form-group">
<label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<div
v-if="jupyterHostname"
class="input-group"
>
<input
type="text"
id="jupyter-hostname"
class="form-control js-hostname"
:value="jupyterHostname"
readonly
/>
<span class="input-group-btn">
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class="js-clipboard-btn"
/>
</span>
</div>
</div>
</template>
<template v-else-if="ingressInstalled">
<template v-if="ingressInstalled">
<div class="form-group">
<label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<div class="input-group">
<input
type="text"
id="jupyter-hostname"
class="form-control js-hostname"
v-model="jupyterSuggestHostnameValue"
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
/>
<span class="input-group-btn">
<span
class="input-group-btn"
>
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
@ -361,7 +328,7 @@ export default {
</span>
</div>
</div>
<p>
<p v-if="ingressInstalled">
{{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
If you do so, point hostname to Ingress IP Address from above.`) }}
<a

View File

@ -1,5 +1,4 @@
import axios from '../../lib/utils/axios_utils';
import { JUPYTER } from '../constants';
export default class ClusterService {
constructor(options = {}) {
@ -17,14 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint);
}
installApplication(appId) {
const data = {};
if (appId === JUPYTER) {
data.hostname = document.getElementById('jupyter-hostname').value;
}
return axios.post(this.appInstallEndpointMap[appId], data);
installApplication(appId, params) {
return axios.post(this.appInstallEndpointMap[appId], params);
}
static updateCluster(endpoint, data) {

View File

@ -92,7 +92,7 @@ export default class ClusterStore {
if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname = serverAppEntry.hostname;
this.state.applications.jupyter.hostname = serverAppEntry.hostname || this.state.applications.ingress.externalIp ? `jupyter.${this.state.applications.ingress.externalIp}.xip.io` : '';
}
});
}

View File

@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
cluster.installApplication('helm');
cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm');
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
getSetTimeoutPromise()
.then(() => {
@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null);
cluster.installApplication('ingress');
cluster.installApplication({ id: 'ingress' });
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress');
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
getSetTimeoutPromise()
.then(() => {
@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null);
cluster.installApplication('runner');
cluster.installApplication({ id: 'runner' });
expect(cluster.store.state.applications.runner.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner');
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
getSetTimeoutPromise()
.then(() => {
@ -260,11 +260,29 @@ describe('Clusters', () => {
.catch(done.fail);
});
it('tries to install jupyter', (done) => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(null);
cluster.installApplication({ id: 'jupyter', params: { hostname: cluster.store.state.applications.jupyter.hostname } });
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('jupyter', { hostname: cluster.store.state.applications.jupyter.hostname });
getSetTimeoutPromise()
.then(() => {
expect(cluster.store.state.applications.jupyter.requestStatus).toEqual(REQUEST_SUCCESS);
expect(cluster.store.state.applications.jupyter.requestReason).toEqual(null);
})
.then(done)
.catch(done.fail);
});
it('sets error request status when the request fails', (done) => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null);
cluster.installApplication('helm');
cluster.installApplication({ id: 'helm' });
expect(cluster.store.state.applications.helm.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null);

View File

@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton.click();
expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', DEFAULT_APPLICATION_STATE.id);
expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: {},
});
});
it('clicking install button when installApplicationRequestParams are provided emits event', () => {
spyOn(eventHub, '$emit');
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_INSTALLABLE,
installApplicationRequestParams: { hostname: 'jupyter' },
});
const installButton = vm.$el.querySelector('.js-cluster-application-install-button');
installButton.click();
expect(eventHub.$emit).toHaveBeenCalledWith('installApplication', {
id: DEFAULT_APPLICATION_STATE.id,
params: { hostname: 'jupyter' },
});
});
it('clicking disabled install button emits nothing', () => {
@ -191,6 +211,16 @@ describe('Application Row', () => {
expect(eventHub.$emit).not.toHaveBeenCalled();
});
it('is disabled when disableInstallButton prop is provided', () => {
vm = mountComponent(ApplicationRow, {
...DEFAULT_APPLICATION_STATE,
status: APPLICATION_INSTALLING,
disableInstallButton: true,
});
expect(vm.installButtonDisabled).toEqual(true);
});
});
describe('Error block', () => {

View File

@ -22,6 +22,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
},
});
});
@ -41,6 +42,10 @@ describe('Applications', () => {
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
});
it('renders a row for Jupyter', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-jupyter')).not.toBe(null);
});
});
describe('Ingress application', () => {
@ -57,12 +62,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('0.0.0.0');
expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
@ -81,12 +85,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
},
});
expect(
vm.$el.querySelector('.js-ip-address').value,
).toEqual('?');
expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
});
@ -101,6 +104,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
},
});
@ -108,5 +112,66 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
});
});
describe('Jupyter application', () => {
describe('with ingress installed & jupyter not installed', () => {
it('renders hostname active input', () => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller', status: 'installed' },
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
},
});
expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual(null);
});
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller', status: 'installed' },
ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
},
});
expect(vm.$el.querySelector('.js-hostname').getAttribute('readonly')).toEqual('readonly');
});
});
});
describe('without ingress installed', () => {
beforeEach(() => {
vm = mountComponent(Applications, {
applications: {
helm: { title: 'Helm Tiller' },
ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
},
});
});
it('does not render input', () => {
expect(vm.$el.querySelector('.js-hostname')).toBe(null);
});
it('renders disabled install button', () => {
expect(
vm.$el
.querySelector(
'.js-cluster-application-row-jupyter .js-cluster-application-install-button',
)
.getAttribute('disabled'),
).toEqual('disabled');
});
});
});
});
});

View File

@ -1,4 +1,5 @@
import {
APPLICATION_INSTALLED,
APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING,
APPLICATION_ERROR,
@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
}, {
name: 'jupyter',
status: APPLICATION_INSTALLING,
status_reason: 'Cannot connect',
}],
},
},
'/gitlab-org/gitlab-shell/clusters/2/status.json': {
data: {
status: 'errored',
status_reason: 'Failed to request to CloudPlatform.',
applications: [{
name: 'helm',
status: APPLICATION_INSTALLED,
status_reason: null,
}, {
name: 'ingress',
status: APPLICATION_INSTALLED,
status_reason: 'Cannot connect',
external_ip: '1.1.1.1',
}, {
name: 'runner',
status: APPLICATION_INSTALLING,
status_reason: null,
},
{
name: 'prometheus',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
}, {
name: 'jupyter',
status: APPLICATION_INSTALLABLE,
status_reason: 'Cannot connect',
}],
},
},
@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
},
};

View File

@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus: null,
requestReason: null,
},
jupyter: {
title: 'JupyterHub',
status: mockResponseData.applications[4].status,
statusReason: mockResponseData.applications[4].status_reason,
requestStatus: null,
requestReason: null,
hostname: '',
},
},
});
});
it('sets default hostname for jupyter when ingress has a ip address', () => {
const mockResponseData = CLUSTERS_MOCK_DATA.GET['/gitlab-org/gitlab-shell/clusters/2/status.json'].data;
store.updateStateFromServer(mockResponseData);
expect(
store.state.applications.jupyter.hostname,
).toEqual(`jupyter.${store.state.applications.ingress.externalIp}.xip.io`);
});
});
});