Commit e1d4deb2 authored by Filipa Lacerda's avatar Filipa Lacerda Committed by Dmitriy Zaporozhets

Disables jupyter install button while ingress is not installed

Includes juptyer hostname in the post request
Adds tests
parent b3cf1530
...@@ -211,11 +211,12 @@ export default class Clusters { ...@@ -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, 'requestStatus', REQUEST_LOADING);
this.store.updateAppProperty(appId, 'requestReason', null); this.store.updateAppProperty(appId, 'requestReason', null);
this.service.installApplication(appId) this.service.installApplication(appId, data.params)
.then(() => { .then(() => {
this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS);
}) })
......
...@@ -52,6 +52,16 @@ ...@@ -52,6 +52,16 @@
type: String, type: String,
required: false, required: false,
}, },
disableInstallButton: {
type: Boolean,
required: false,
default: false,
},
installApplicationRequestParams: {
type: Object,
required: false,
default: () => ({}),
},
}, },
computed: { computed: {
rowJsClass() { rowJsClass() {
...@@ -67,7 +77,7 @@ ...@@ -67,7 +77,7 @@
// Avoid the potential for the real-time data to say APPLICATION_INSTALLABLE but // 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 // we already made a request to install and are just waiting for the real-time
// to sync up. // to sync up.
return (this.status !== APPLICATION_INSTALLABLE return this.disableInstallButton || (this.status !== APPLICATION_INSTALLABLE
&& this.status !== APPLICATION_ERROR) || && this.status !== APPLICATION_ERROR) ||
this.requestStatus === REQUEST_LOADING || this.requestStatus === REQUEST_LOADING ||
this.requestStatus === REQUEST_SUCCESS; this.requestStatus === REQUEST_SUCCESS;
...@@ -109,7 +119,10 @@ ...@@ -109,7 +119,10 @@
}, },
methods: { methods: {
installClicked() { installClicked() {
eventHub.$emit('installApplication', this.id); eventHub.$emit('installApplication', {
id: this.id,
params: this.installApplicationRequestParams,
});
}, },
}, },
}; };
......
...@@ -37,11 +37,6 @@ export default { ...@@ -37,11 +37,6 @@ export default {
default: '', default: '',
}, },
}, },
data() {
return {
jupyterSuggestHostnameValue: '',
};
},
computed: { computed: {
generalApplicationDescription() { generalApplicationDescription() {
return sprintf( return sprintf(
...@@ -132,14 +127,6 @@ export default { ...@@ -132,14 +127,6 @@ export default {
jupyterHostname() { jupyterHostname() {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
jupyterSuggestHostname() {
return `jupyter.${this.applications.ingress.externalIp}.xip.io`;
},
},
watch: {
jupyterSuggestHostname() {
this.jupyterSuggestHostnameValue = this.jupyterSuggestHostname;
},
}, },
}; };
</script> </script>
...@@ -305,6 +292,8 @@ export default { ...@@ -305,6 +292,8 @@ export default {
:status-reason="applications.jupyter.statusReason" :status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus" :request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason" :request-reason="applications.jupyter.requestReason"
:disable-install-button="!ingressInstalled"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
> >
<div slot="description"> <div slot="description">
<p> <p>
...@@ -314,45 +303,23 @@ export default { ...@@ -314,45 +303,23 @@ export default {
notebooks to a class of students, a corporate data science group, notebooks to a class of students, a corporate data science group,
or a scientific research group.`) }} or a scientific research group.`) }}
</p> </p>
<template v-if="jupyterInstalled">
<div class="form-group"> <template v-if="ingressInstalled">
<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">
<div class="form-group"> <div class="form-group">
<label for="jupyter-hostname"> <label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }} {{ s__('ClusterIntegration|Jupyter Hostname') }}
</label> </label>
<div class="input-group"> <div class="input-group">
<input <input
type="text" type="text"
id="jupyter-hostname"
class="form-control js-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 <clipboard-button
:text="jupyterHostname" :text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')" :title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
...@@ -361,7 +328,7 @@ export default { ...@@ -361,7 +328,7 @@ export default {
</span> </span>
</div> </div>
</div> </div>
<p> <p v-if="ingressInstalled">
{{ 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 <a
......
import axios from '../../lib/utils/axios_utils'; import axios from '../../lib/utils/axios_utils';
import { JUPYTER } from '../constants';
export default class ClusterService { export default class ClusterService {
constructor(options = {}) { constructor(options = {}) {
...@@ -17,14 +16,8 @@ export default class ClusterService { ...@@ -17,14 +16,8 @@ export default class ClusterService {
return axios.get(this.options.endpoint); return axios.get(this.options.endpoint);
} }
installApplication(appId) { installApplication(appId, params) {
const data = {}; return axios.post(this.appInstallEndpointMap[appId], params);
if (appId === JUPYTER) {
data.hostname = document.getElementById('jupyter-hostname').value;
}
return axios.post(this.appInstallEndpointMap[appId], data);
} }
static updateCluster(endpoint, data) { static updateCluster(endpoint, data) {
......
...@@ -92,7 +92,7 @@ export default class ClusterStore { ...@@ -92,7 +92,7 @@ export default class ClusterStore {
if (appId === INGRESS) { if (appId === INGRESS) {
this.state.applications.ingress.externalIp = serverAppEntry.external_ip; this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
} else if (appId === JUPYTER) { } 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` : '';
} }
}); });
} }
......
...@@ -207,11 +207,11 @@ describe('Clusters', () => { ...@@ -207,11 +207,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); 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.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('helm'); expect(cluster.service.installApplication).toHaveBeenCalledWith('helm', undefined);
getSetTimeoutPromise() getSetTimeoutPromise()
.then(() => { .then(() => {
...@@ -226,11 +226,11 @@ describe('Clusters', () => { ...@@ -226,11 +226,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.ingress.requestStatus).toEqual(null); 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.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.ingress.requestReason).toEqual(null); expect(cluster.store.state.applications.ingress.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress'); expect(cluster.service.installApplication).toHaveBeenCalledWith('ingress', undefined);
getSetTimeoutPromise() getSetTimeoutPromise()
.then(() => { .then(() => {
...@@ -245,11 +245,11 @@ describe('Clusters', () => { ...@@ -245,11 +245,11 @@ describe('Clusters', () => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve()); spyOn(cluster.service, 'installApplication').and.returnValue(Promise.resolve());
expect(cluster.store.state.applications.runner.requestStatus).toEqual(null); 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.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.runner.requestReason).toEqual(null); expect(cluster.store.state.applications.runner.requestReason).toEqual(null);
expect(cluster.service.installApplication).toHaveBeenCalledWith('runner'); expect(cluster.service.installApplication).toHaveBeenCalledWith('runner', undefined);
getSetTimeoutPromise() getSetTimeoutPromise()
.then(() => { .then(() => {
...@@ -260,11 +260,29 @@ describe('Clusters', () => { ...@@ -260,11 +260,29 @@ describe('Clusters', () => {
.catch(done.fail); .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) => { it('sets error request status when the request fails', (done) => {
spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR'))); spyOn(cluster.service, 'installApplication').and.returnValue(Promise.reject(new Error('STUBBED ERROR')));
expect(cluster.store.state.applications.helm.requestStatus).toEqual(null); 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.requestStatus).toEqual(REQUEST_LOADING);
expect(cluster.store.state.applications.helm.requestReason).toEqual(null); expect(cluster.store.state.applications.helm.requestReason).toEqual(null);
......
...@@ -174,7 +174,27 @@ describe('Application Row', () => { ...@@ -174,7 +174,27 @@ describe('Application Row', () => {
installButton.click(); 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', () => { it('clicking disabled install button emits nothing', () => {
...@@ -191,6 +211,16 @@ describe('Application Row', () => { ...@@ -191,6 +211,16 @@ describe('Application Row', () => {
expect(eventHub.$emit).not.toHaveBeenCalled(); 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', () => { describe('Error block', () => {
......
...@@ -22,6 +22,7 @@ describe('Applications', () => { ...@@ -22,6 +22,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' }, ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' }, runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' }, prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub' },
}, },
}); });
}); });
...@@ -41,6 +42,10 @@ describe('Applications', () => { ...@@ -41,6 +42,10 @@ describe('Applications', () => {
it('renders a row for GitLab Runner', () => { it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined(); 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', () => { describe('Ingress application', () => {
...@@ -57,12 +62,11 @@ describe('Applications', () => { ...@@ -57,12 +62,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' }, helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' }, runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' }, prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
}, },
}); });
expect( expect(vm.$el.querySelector('.js-ip-address').value).toEqual('0.0.0.0');
vm.$el.querySelector('.js-ip-address').value,
).toEqual('0.0.0.0');
expect( expect(
vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'), vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
...@@ -81,12 +85,11 @@ describe('Applications', () => { ...@@ -81,12 +85,11 @@ describe('Applications', () => {
helm: { title: 'Helm Tiller' }, helm: { title: 'Helm Tiller' },
runner: { title: 'GitLab Runner' }, runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' }, prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
}, },
}); });
expect( expect(vm.$el.querySelector('.js-ip-address').value).toEqual('?');
vm.$el.querySelector('.js-ip-address').value,
).toEqual('?');
expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null); expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
}); });
...@@ -101,6 +104,7 @@ describe('Applications', () => { ...@@ -101,6 +104,7 @@ describe('Applications', () => {
ingress: { title: 'Ingress' }, ingress: { title: 'Ingress' },
runner: { title: 'GitLab Runner' }, runner: { title: 'GitLab Runner' },
prometheus: { title: 'Prometheus' }, prometheus: { title: 'Prometheus' },
jupyter: { title: 'JupyterHub', hostname: '' },
}, },
}); });
...@@ -108,5 +112,66 @@ describe('Applications', () => { ...@@ -108,5 +112,66 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-ip-address')).toBe(null); 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');
});
});
});
}); });
}); });
import { import {
APPLICATION_INSTALLED,
APPLICATION_INSTALLABLE, APPLICATION_INSTALLABLE,
APPLICATION_INSTALLING, APPLICATION_INSTALLING,
APPLICATION_ERROR, APPLICATION_ERROR,
...@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = { ...@@ -28,6 +29,39 @@ const CLUSTERS_MOCK_DATA = {
name: 'prometheus', name: 'prometheus',
status: APPLICATION_ERROR, status: APPLICATION_ERROR,
status_reason: 'Cannot connect', 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 = { ...@@ -37,6 +71,7 @@ const CLUSTERS_MOCK_DATA = {
'/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/ingress': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/runner': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/runner': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { }, '/gitlab-org/gitlab-shell/clusters/1/applications/prometheus': { },
'/gitlab-org/gitlab-shell/clusters/1/applications/jupyter': { },
}, },
}; };
......
...@@ -91,8 +91,26 @@ describe('Clusters Store', () => { ...@@ -91,8 +91,26 @@ describe('Clusters Store', () => {
requestStatus: null, requestStatus: null,
requestReason: 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`);
});
}); });
}); });
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