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 @@
/* eslint-disable vue/require-default-prop */
import { s__, sprintf } from '../../locale';
import eventHub from '../event_hub';
import identicon from '../../vue_shared/components/identicon.vue';
import loadingButton from '../../vue_shared/components/loading_button.vue';
import {
APPLICATION_STATUS,
......@@ -13,6 +14,7 @@
export default {
components: {
loadingButton,
identicon,
},
props: {
id: {
......@@ -31,6 +33,16 @@
type: String,
required: false,
},
logoUrl: {
type: String,
required: false,
default: null,
},
disabled: {
type: Boolean,
required: false,
default: false,
},
status: {
type: String,
required: false,
......@@ -60,6 +72,18 @@
isKnownStatus() {
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() {
return `js-cluster-application-row-${this.id}`;
},
......@@ -128,37 +152,81 @@
<template>
<div
:class="rowJsClass"
class="gl-responsive-table-row gl-responsive-table-row-col-span"
:class="[
rowJsClass,
isInstalled && 'cluster-application-installed',
disabled && 'cluster-application-disabled'
]"
class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
>
<div
class="gl-responsive-table-row-layout"
role="row"
>
<a
v-if="titleLink"
:href="titleLink"
target="blank"
rel="noopener noreferrer"
<div
class="table-section append-right-8 section-align-top"
role="gridcell"
class="table-section section-15 section-align-top js-cluster-application-title"
>
{{ title }}
</a>
<span
v-else
class="table-section section-15 section-align-top js-cluster-application-title"
>
{{ title }}
</span>
<img
v-if="hasLogo"
:src="logoUrl"
:alt="`${title} logo`"
class="cluster-application-logo avatar s40"
/>
<identicon
v-else
:entity-id="identiconId"
:entity-name="title"
size-class="s40"
/>
</div>
<div
class="table-section section-wrap"
class="table-section cluster-application-description section-wrap"
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>
<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
:class="{ 'section-20': showManageButton, 'section-15': !showManageButton }"
:class="{ 'section-25': showManageButton, 'section-15': !showManageButton }"
class="table-section table-button-footer section-align-top"
role="gridcell"
>
......@@ -168,6 +236,7 @@
>
<a
:href="manageLink"
:class="{ disabled: disabled }"
class="btn"
>
{{ manageButtonLabel }}
......@@ -176,7 +245,7 @@
<div class="btn-group table-action-buttons">
<loading-button
:loading="installButtonLoading"
:disabled="installButtonDisabled"
:disabled="disabled || installButtonDisabled"
:label="installButtonLabel"
class="js-cluster-application-install-button"
@click="installClicked"
......@@ -184,35 +253,5 @@
</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>
</template>
<script>
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 applicationRow from './application_row.vue';
import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
......@@ -37,21 +46,21 @@ export default {
default: '',
},
},
data: () => ({
elasticsearchLogo,
gitlabLogo,
helmLogo,
jeagerLogo,
jupyterhubLogo,
kubernetesLogo,
meltanoLogo,
prometheusLogo,
}),
computed: {
generalApplicationDescription() {
return sprintf(
_.escape(
s__(
`ClusterIntegration|Install applications on your Kubernetes cluster.
Read more about %{helpLink}`,
),
),
{
helpLink: `<a href="${this.helpPath}">
${_.escape(s__('ClusterIntegration|installing applications'))}
</a>`,
},
false,
helmInstalled() {
return (
this.applications.helm.status === APPLICATION_STATUS.INSTALLED ||
this.applications.helm.status === APPLICATION_STATUS.UPDATED
);
},
ingressId() {
......@@ -128,224 +137,240 @@ export default {
return this.applications.jupyter.hostname;
},
},
created() {
this.helmInstallIllustration = helmInstallIllustration;
},
};
</script>
<template>
<section
id="cluster-applications"
class="settings no-animate expanded"
>
<div class="settings-header">
<h4>
{{ s__('ClusterIntegration|Applications') }}
</h4>
<p
class="append-bottom-0"
v-html="generalApplicationDescription"
>
</p>
</div>
<section id="cluster-applications">
<h4>
{{ s__('ClusterIntegration|Applications') }}
</h4>
<p class="append-bottom-0">
{{ s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.
Helm Tiller is required to install any of the following applications.`) }}
<a :href="helpPath">
{{ __('More information') }}
</a>
</p>
<div class="settings-content">
<div class="append-bottom-20">
<application-row
id="helm"
:title="applications.helm.title"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
title-link="https://docs.helm.sh/"
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<application-row
:id="ingressId"
:title="applications.ingress.title"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
<div class="cluster-application-list prepend-top-10">
<application-row
id="helm"
:logo-url="helmLogo"
:title="applications.helm.title"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
class="rounded-top"
title-link="https://docs.helm.sh/"
>
<div slot="description">
{{ s__(`ClusterIntegration|Helm streamlines installing
and managing Kubernetes applications.
Tiller runs inside of your Kubernetes Cluster,
and manages releases of your charts.`) }}
</div>
</application-row>
<div
v-show="!helmInstalled"
class="cluster-application-warning"
>
<div
class="svg-container"
v-html="helmInstallIllustration"
>
<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>
</div>
{{ s__(`ClusterIntegration|You must first install Helm Tiller before
installing the applications below`) }}
</div>
<application-row
: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">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
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>
<template v-if="ingressInstalled">
<div class="form-group">
<label for="ingress-ip-address">
{{ s__('ClusterIntegration|Ingress IP Address') }}
</label>
<div
v-if="ingressExternalIp"
class="input-group"
>
<input
v-else
id="ingress-ip-address"
:value="ingressExternalIp"
type="text"
class="form-control js-ip-address"
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>
<input
v-else
type="text"
class="form-control js-ip-address"
readonly
value="?"
/>
</div>
<p
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address 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
v-if="!ingressExternalIp"
class="settings-message js-no-ip-message"
>
{{ s__(`ClusterIntegration|The IP address is in
the process of being assigned. Please check your Kubernetes
cluster or Quotas on Google Kubernetes Engine if it takes a long time.`) }}
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<a
:href="ingressHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
<p>
{{ s__(`ClusterIntegration|Point a wildcard DNS to this
generated IP address in order to access
your application after it has been deployed.`) }}
<a
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</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/"
>
</template>
<div
slot="description"
v-html="prometheusDescription"
v-html="ingressDescription"
>
</div>
</application-row>
<application-row
id="runner"
:title="applications.runner.title"
:status="applications.runner.status"
:status-reason="applications.runner.statusReason"
:request-status="applications.runner.requestStatus"
:request-reason="applications.runner.requestReason"
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"
: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>
</application-row>
<application-row
id="prometheus"
:logo-url="prometheusLogo"
: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"
:disabled="!helmInstalled"
title-link="https://prometheus.io/docs/introduction/overview/"
>
<div
slot="description"
v-html="prometheusDescription"
>
<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>
</div>
</application-row>
<application-row
id="runner"
:logo-url="gitlabLogo"
:title="applications.runner.title"
:status="applications.runner.status"
: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">
<div class="form-group">
<label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<template v-if="ingressExternalIp">
<div class="form-group">
<label for="jupyter-hostname">
{{ s__('ClusterIntegration|Jupyter Hostname') }}
</label>
<div class="input-group">
<input
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
type="text"
class="form-control js-hostname"
<div class="input-group">
<input
v-model="applications.jupyter.hostname"
:readonly="jupyterInstalled"
type="text"
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
class="input-group-btn"
>
<clipboard-button
:text="jupyterHostname"
:title="s__('ClusterIntegration|Copy Jupyter Hostname to clipboard')"
class="js-clipboard-btn"
/>
</span>
</div>
</span>
</div>
<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
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
</div>
</application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
-->
</div>
</div>
<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
:href="ingressDnsHelpPath"
target="_blank"
rel="noopener noreferrer"
>
{{ __('More information') }}
</a>
</p>
</template>
</div>
</application-row>
</div>
</section>
</template>
......@@ -4,9 +4,60 @@
}
}
.cluster-applications-table {
// Wait for the Vue to kick-in and render the applications block
min-height: 628px;
.cluster-application-row {
background: $gray-lighter;
&.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 {
......
---
title: Improve install flow of Kubernetes cluster apps
merge_request: 21567
author:
type: changed
......@@ -1353,6 +1353,9 @@ msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
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."
msgstr ""
......@@ -1443,9 +1446,6 @@ msgstr ""
msgid "ClusterIntegration|Install"
msgstr ""
msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}"
msgstr ""
msgid "ClusterIntegration|Installed"
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."
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}"
msgstr ""
......@@ -1671,9 +1674,6 @@ msgstr ""
msgid "ClusterIntegration|help page"
msgstr ""
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements"
msgstr ""
......
......@@ -86,7 +86,6 @@ describe 'User Cluster', :js do
context 'when user disables the cluster' do
before do
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' }
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