Commit 354f0bcc authored by Clement Ho's avatar Clement Ho

Merge branch '50111-improve-design-of-cluster-apps-to-handle-larger-quantity' into 'master'

Improve cluster apps installation flow

Closes #50111

See merge request gitlab-org/gitlab-ce!21567
parents 187b8e74 583cc364
...@@ -2,6 +2,7 @@ ...@@ -2,6 +2,7 @@
/* eslint-disable vue/require-default-prop */ /* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale'; import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub'; import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue'; import loadingButton from '../../vue_shared/components/loading_button.vue';
import { import {
APPLICATION_STATUS, APPLICATION_STATUS,
...@@ -13,6 +14,7 @@ ...@@ -13,6 +14,7 @@
export default { export default {
components: { components: {
loadingButton, loadingButton,
identicon,
}, },
props: { props: {
id: { id: {
...@@ -31,6 +33,16 @@ ...@@ -31,6 +33,16 @@
type: String, type: String,
required: false, required: false,
}, },
logoUrl: {
type: String,
required: false,
default: null,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
status: { status: {
type: String, type: String,
required: false, required: false,
...@@ -60,6 +72,18 @@ ...@@ -60,6 +72,18 @@
isKnownStatus() { isKnownStatus() {
return Object.values(APPLICATION_STATUS).includes(this.status); return Object.values(APPLICATION_STATUS).includes(this.status);
}, },
isInstalled() {
return (
this.status === APPLICATION_STATUS.INSTALLED || this.status === APPLICATION_STATUS.UPDATED
);
},
hasLogo() {
return !!this.logoUrl;
},
identiconId() {
// generate a deterministic integer id for the identicon background
return this.id.charCodeAt(0);
},
rowJsClass() { rowJsClass() {
return `js-cluster-application-row-${this.id}`; return `js-cluster-application-row-${this.id}`;
}, },
...@@ -128,37 +152,81 @@ ...@@ -128,37 +152,81 @@
<template> <template>
<div <div
:class="rowJsClass" :class="[
class="gl-responsive-table-row gl-responsive-table-row-col-span" rowJsClass,
isInstalled && 'cluster-application-installed',
disabled && 'cluster-application-disabled'
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
> >
<div <div
class="gl-responsive-table-row-layout" class="gl-responsive-table-row-layout"
role="row" role="row"
> >
<a <div
v-if="titleLink" class="table-section append-right-8 section-align-top"
:href="titleLink"
target="blank"
rel="noopener noreferrer"
role="gridcell" role="gridcell"
class="table-section section-15 section-align-top js-cluster-application-title"
> >
{{ title }} <img
</a> v-if="hasLogo"
<span :src="logoUrl"
v-else :alt="`${title} logo`"
class="table-section section-15 section-align-top js-cluster-application-title" class="cluster-application-logo avatar s40"
> />
{{ title }} <identicon
</span> v-else
:entity-id="identiconId"
:entity-name="title"
size-class="s40"
/>
</div>
<div <div
class="table-section section-wrap" class="table-section cluster-application-description section-wrap"
role="gridcell" role="gridcell"
> >
<strong>
<a
v-if="titleLink"
:href="titleLink"
target="blank"
rel="noopener noreferrer"
class="js-cluster-application-title"
>
{{ title }}
</a>
<span
v-else
class="js-cluster-application-title"
>
{{ title }}
</span>
</strong>
<slot name="description"></slot> <slot name="description"></slot>
<div
v-if="hasError || isUnknownStatus"
class="cluster-application-error text-danger prepend-top-10"
>
<p class="js-cluster-application-general-error-message append-bottom-0">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li
v-if="statusReason"
class="js-cluster-application-status-error-message"
>
{{ statusReason }}
</li>
<li
v-if="requestReason"
class="js-cluster-application-request-error-message"
>
{{ requestReason }}
</li>
</ul>
</div>
</div> </div>
<div <div
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }" :class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
class="table-section table-button-footer section-align-top" class="table-section table-button-footer section-align-top"
role="gridcell" role="gridcell"
> >
...@@ -168,6 +236,7 @@ ...@@ -168,6 +236,7 @@
> >
<a <a
:href="manageLink" :href="manageLink"
:class="{ disabled: disabled }"
class="btn" class="btn"
> >
{{ manageButtonLabel }} {{ manageButtonLabel }}
...@@ -176,7 +245,7 @@ ...@@ -176,7 +245,7 @@
<div class="btn-group table-action-buttons"> <div class="btn-group table-action-buttons">
<loading-button <loading-button
:loading="installButtonLoading" :loading="installButtonLoading"
:disabled="installButtonDisabled" :disabled="disabled || installButtonDisabled"
:label="installButtonLabel" :label="installButtonLabel"
class="js-cluster-application-install-button" class="js-cluster-application-install-button"
@click="installClicked" @click="installClicked"
...@@ -184,35 +253,5 @@ ...@@ -184,35 +253,5 @@
</div> </div>
</div> </div>
</div> </div>
<div
v-if="hasError || isUnknownStatus"
class="gl-responsive-table-row-layout"
role="row"
>
<div
class="alert alert-danger alert-block append-bottom-0 clusters-error-alert"
role="gridcell"
>
<div>
<p class="js-cluster-application-general-error-message">
{{ generalErrorDescription }}
</p>
<ul v-if="statusReason || requestReason">
<li
v-if="statusReason"
class="js-cluster-application-status-error-message"
>
{{ statusReason }}
</li>
<li
v-if="requestReason"
class="js-cluster-application-request-error-message"
>
{{ requestReason }}
</li>
</ul>
</div>
</div>
</div>
</div> </div>
</template> </template>
<script> <script>
import _ from 'underscore'; import _ from 'underscore';
import helmInstallIllustration from '@gitlab-org/gitlab-svgs/illustrations/kubernetes-installation.svg';
import elasticsearchLogo from 'images/cluster_app_logos/elasticsearch.png';
import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
import helmLogo from 'images/cluster_app_logos/helm.png';
import jeagerLogo from 'images/cluster_app_logos/jeager.png';
import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
import meltanoLogo from 'images/cluster_app_logos/meltano.png';
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';
...@@ -37,21 +46,21 @@ export default { ...@@ -37,21 +46,21 @@ export default {
default: '', default: '',
}, },
}, },
data: () => ({
elasticsearchLogo,
gitlabLogo,
helmLogo,
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
meltanoLogo,
prometheusLogo,
}),
computed: { computed: {
generalApplicationDescription() { helmInstalled() {
return sprintf( return (
_.escape( this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
s__( this.applications.helm.status === APPLICATION_STATUS.UPDATED
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
); );
}, },
ingressId() { ingressId() {
...@@ -128,224 +137,240 @@ export default { ...@@ -128,224 +137,240 @@ export default {
return this.applications.jupyter.hostname; return this.applications.jupyter.hostname;
}, },
}, },
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
}; };
</script> </script>
<template> <template>
<section <section id="cluster-applications">
id="cluster-applications" <h4>
class="settings no-animate expanded" {{ s__('ClusterIntegration|Applications') }}
> </h4>
<div class="settings-header"> <p class="append-bottom-0">
<h4> {{ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
{{ s__('ClusterIntegration|Applications') }} Helm Tiller is required to install any of the following applications.`) }}
</h4> <a :href="helpPath">
<p {{ __('More information') }}
class="append-bottom-0" </a>
v-html="generalApplicationDescription" </p>
>
</p>
</div>
<div class="settings-content"> <div class="cluster-application-list prepend-top-10">
<div class="append-bottom-20"> <application-row
<application-row id="helm"
id="helm" :logo-url="helmLogo"
:title="applications.helm.title" :title="applications.helm.title"
:status="applications.helm.status" :status="applications.helm.status"
:status-reason="applications.helm.statusReason" :status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus" :request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason" :request-reason="applications.helm.requestReason"
title-link="https://docs.helm.sh/" class="rounded-top"
> title-link="https://docs.helm.sh/"
<div slot="description"> >
{{ s__(`ClusterIntegration|Helm streamlines installing <div slot="description">
and managing Kubernetes applications. {{ s__(`ClusterIntegration|Helm streamlines installing
Tiller runs inside of your Kubernetes Cluster, and managing Kubernetes applications.
and manages releases of your charts.`) }} Tiller runs inside of your Kubernetes Cluster,
</div> and manages releases of your charts.`) }}
</application-row> </div>
<application-row </application-row>
:id="ingressId" <div
:title="applications.ingress.title" v-show="!helmInstalled"
:status="applications.ingress.status" class="cluster-application-warning"
:status-reason="applications.ingress.statusReason" >
:request-status="applications.ingress.requestStatus" <div
:request-reason="applications.ingress.requestReason" class="svg-container"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/" v-html="helmInstallIllustration"
> >
<div slot="description"> </div>
<p> {{ s__(`ClusterIntegration|You must first install Helm Tiller before
{{ s__(`ClusterIntegration|Ingress gives you a way to route installing the applications below`) }}
requests to services based on the request host or path, </div>
centralizing a number of services into a single entrypoint.`) }} <application-row
</p> :id="ingressId"
:logo-url="kubernetesLogo"
:title="applications.ingress.title"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
:disabled="!helmInstalled"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|Ingress gives you a way to route
requests to services based on the request host or path,
centralizing a number of services into a single entrypoint.`) }}
</p>
<template v-if="ingressInstalled"> <template v-if="ingressInstalled">
<div class="form-group"> <div class="form-group">
<label for="ingress-ip-address"> <label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }} {{ s__('ClusterIntegration|Ingress IP Address') }}
</label> </label>
<div <div
v-if="ingressExternalIp" v-if="ingressExternalIp"
class="input-group" class="input-group"
> >
<input
id="ingress-ip-address"
:value="ingressExternalIp"
type="text"
class="form-control js-ip-address"
readonly
/>
<span class="input-group-append">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
class="input-group-text js-clipboard-btn"
/>
</span>
</div>
<input <input
v-else id="ingress-ip-address"
:value="ingressExternalIp"
type="text" type="text"
class="form-control js-ip-address" class="form-control js-ip-address"
readonly readonly
value="?"
/> />
<span class="input-group-append">
<clipboard-button
:text="ingressExternalIp"
:title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
class="input-group-text js-clipboard-btn"
/>
</span>
</div> </div>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p <p
v-if="!ingressExternalIp" v-if="!ingressExternalIp"
class="settings-message js-no-ip-message" class="settings-message js-no-ip-message"
> >
{{ s__(`ClusterIntegration|The IP address is in {{ s__(`ClusterIntegration|The IP address is in
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 <a
:href="ingressHelpPath" :href="ingressHelpPath"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
<p> <p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this {{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access generated IP address in order to access
your application after it has been deployed.`) }} your application after it has been deployed.`) }}
<a <a
:href="ingressDnsHelpPath" :href="ingressDnsHelpPath"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
> >
{{ __('More information') }} {{ __('More information') }}
</a> </a>
</p> </p>
</template> </template>
<div
v-else
v-html="ingressDescription"
>
</div>
</div>
</application-row>
<application-row
id="prometheus"
:title="applications.prometheus.title"
:manage-link="managePrometheusPath"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
title-link="https://prometheus.io/docs/introduction/overview/"
>
<div <div
slot="description" v-html="ingressDescription"
v-html="prometheusDescription"
> >
</div> </div>
</application-row> </div>
<application-row </application-row>
id="runner" <application-row
:title="applications.runner.title" id="prometheus"
:status="applications.runner.status" :logo-url="prometheusLogo"
:status-reason="applications.runner.statusReason" :title="applications.prometheus.title"
:request-status="applications.runner.requestStatus" :manage-link="managePrometheusPath"
:request-reason="applications.runner.requestReason" :status="applications.prometheus.status"
title-link="https://docs.gitlab.com/runner/" :status-reason="applications.prometheus.statusReason"
> :request-status="applications.prometheus.requestStatus"
<div slot="description"> :request-reason="applications.prometheus.requestReason"
{{ s__(`ClusterIntegration|GitLab Runner connects to this :disabled="!helmInstalled"
project's repository and executes CI/CD jobs, title-link="https://prometheus.io/docs/introduction/overview/"
pushing results back and deploying, >
applications to production.`) }} <div
</div> slot="description"
</application-row> v-html="prometheusDescription"
<application-row
id="jupyter"
:title="applications.jupyter.title"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
> >
<div slot="description"> </div>
<p> </application-row>
{{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns, <application-row
manages, and proxies multiple instances of the single-user id="runner"
Jupyter notebook server. JupyterHub can be used to serve :logo-url="gitlabLogo"
notebooks to a class of students, a corporate data science group, :title="applications.runner.title"
or a scientific research group.`) }} :status="applications.runner.status"
</p> :status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
:disabled="!helmInstalled"
title-link="https://docs.gitlab.com/runner/"
>
<div slot="description">
{{ s__(`ClusterIntegration|GitLab Runner connects to this
project's repository and executes CI/CD jobs,
pushing results back and deploying,
applications to production.`) }}
</div>
</application-row>
<application-row
id="jupyter"
:logo-url="jupyterhubLogo"
:title="applications.jupyter.title"
:status="applications.jupyter.status"
:status-reason="applications.jupyter.statusReason"
:request-status="applications.jupyter.requestStatus"
:request-reason="applications.jupyter.requestReason"
:install-application-request-params="{ hostname: applications.jupyter.hostname }"
:disabled="!helmInstalled"
class="hide-bottom-border rounded-bottom"
title-link="https://jupyterhub.readthedocs.io/en/stable/"
>
<div slot="description">
<p>
{{ s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
manages, and proxies multiple instances of the single-user
Jupyter notebook server. JupyterHub can be used to serve
notebooks to a class of students, a corporate data science group,
or a scientific research group.`) }}
</p>
<template v-if="ingressExternalIp"> <template v-if="ingressExternalIp">
<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
v-model="applications.jupyter.hostname" v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled" :readonly="jupyterInstalled"
type="text" type="text"
class="form-control js-hostname" class="form-control js-hostname"
/>
<span
class="input-group-btn"
>
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class="js-clipboard-btn"
/> />
<span </span>
class="input-group-btn"
>
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class="js-clipboard-btn"
/>
</span>
</div>
</div> </div>
<p v-if="ingressInstalled"> </div>
{{ s__(`ClusterIntegration|Replace this with your own hostname if you want. <p v-if="ingressInstalled">
If you do so, point hostname to Ingress IP Address from above.`) }} {{ s__(`ClusterIntegration|Replace this with your own hostname if you want.
<a If you do so, point hostname to Ingress IP Address from above.`) }}
:href="ingressDnsHelpPath" <a
target="_blank" :href="ingressDnsHelpPath"
rel="noopener noreferrer" target="_blank"
> rel="noopener noreferrer"
{{ __('More information') }} >
</a> {{ __('More information') }}
</p> </a>
</template> </p>
</div> </template>
</application-row> </div>
<!-- </application-row>
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
</div>
</div> </div>
</section> </section>
</template> </template>
...@@ -4,9 +4,60 @@ ...@@ -4,9 +4,60 @@
} }
} }
.cluster-applications-table { .cluster-application-row {
// Wait for the Vue to kick-in and render the applications block background: $gray-lighter;
min-height: 628px;
&.cluster-application-installed {
background: none;
}
.settings-message {
padding: $gl-vert-padding $gl-padding-8;
}
}
@media (min-width: map-get($grid-breakpoints, md)) {
.cluster-application-list {
border: 1px solid $border-color;
border-radius: $border-radius-default;
}
.cluster-application-row {
border-bottom: 1px solid $border-color;
padding: $gl-padding;
}
}
.cluster-application-logo {
border: 3px solid $white-light;
box-shadow: 0 0 0 1px $gray-normal;
&.avatar:hover {
border-color: $white-light;
}
}
.cluster-application-warning {
font-weight: bold;
text-align: center;
padding: $gl-padding;
border-bottom: 1px solid $white-normal;
.svg-container {
display: inline-block;
vertical-align: middle;
margin-right: $gl-padding-8;
width: 40px;
height: 40px;
}
}
.cluster-application-description {
flex: 1;
}
.cluster-application-disabled {
opacity: 0.5;
} }
.clusters-dropdown-menu { .clusters-dropdown-menu {
......
---
title: Improve install flow of Kubernetes cluster apps
merge_request: 21567
author:
type: changed
...@@ -1353,6 +1353,9 @@ msgstr "" ...@@ -1353,6 +1353,9 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)" msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr "" msgstr ""
msgid "ClusterIntegration|Choose which applications to install on your Kubernetes cluster. Helm Tiller is required to install any of the following applications."
msgstr ""
msgid "ClusterIntegration|Choose which of your environments will use this cluster." msgid "ClusterIntegration|Choose which of your environments will use this cluster."
msgstr "" msgstr ""
...@@ -1443,9 +1446,6 @@ msgstr "" ...@@ -1443,9 +1446,6 @@ msgstr ""
msgid "ClusterIntegration|Install" msgid "ClusterIntegration|Install"
msgstr "" msgstr ""
msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}"
msgstr ""
msgid "ClusterIntegration|Installed" msgid "ClusterIntegration|Installed"
msgstr "" msgstr ""
...@@ -1653,6 +1653,9 @@ msgstr "" ...@@ -1653,6 +1653,9 @@ msgstr ""
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "" msgstr ""
msgid "ClusterIntegration|You must first install Helm Tiller before installing the applications below"
msgstr ""
msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}" msgid "ClusterIntegration|Your account must have %{link_to_kubernetes_engine}"
msgstr "" msgstr ""
...@@ -1671,9 +1674,6 @@ msgstr "" ...@@ -1671,9 +1674,6 @@ msgstr ""
msgid "ClusterIntegration|help page" msgid "ClusterIntegration|help page"
msgstr "" msgstr ""
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements" msgid "ClusterIntegration|meets the requirements"
msgstr "" msgstr ""
......
...@@ -86,7 +86,6 @@ describe 'User Cluster', :js do ...@@ -86,7 +86,6 @@ describe 'User Cluster', :js do
context 'when user disables the cluster' do context 'when user disables the cluster' do
before do before do
page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click page.find(:css, '.js-cluster-enable-toggle-area .js-project-feature-toggle').click
fill_in 'cluster_name', with: 'dev-cluster'
page.within('#cluster-integration') { click_button 'Save changes' } page.within('#cluster-integration') { click_button 'Save changes' }
end end
......
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