diff options
author | Eric Eastwood <contact@ericeastwood.com> | 2017-10-24 07:56:39 +0300 |
---|---|---|
committer | Eric Eastwood <contact@ericeastwood.com> | 2017-11-03 11:28:57 -0500 |
commit | 797e758beb882d67f32b87db6453cad41c0b931f (patch) | |
tree | 7e3a35aa71b0d484f727a29f88f16f3b3d0ffa29 /app | |
parent | e6616e0468deaf1e37ddddc9332cc3e677567410 (diff) | |
download | gitlab-ce-797e758beb882d67f32b87db6453cad41c0b931f.tar.gz |
Add applications section to GKE clusters page
Diffstat (limited to 'app')
-rw-r--r-- | app/assets/javascripts/clusters.js | 123 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/clusters_bundle.js | 216 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/components/application_row.vue | 168 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/components/applications.vue | 105 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/constants.js | 10 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/event_hub.js | 3 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/services/clusters_service.js | 24 | ||||
-rw-r--r-- | app/assets/javascripts/clusters/stores/clusters_store.js | 68 | ||||
-rw-r--r-- | app/assets/javascripts/dispatcher.js | 9 | ||||
-rw-r--r-- | app/assets/javascripts/vue_shared/components/loading_button.vue | 7 | ||||
-rw-r--r-- | app/assets/stylesheets/framework/buttons.scss | 1 | ||||
-rw-r--r-- | app/assets/stylesheets/pages/clusters.scss | 5 | ||||
-rw-r--r-- | app/views/projects/clusters/show.html.haml | 19 |
13 files changed, 627 insertions, 131 deletions
diff --git a/app/assets/javascripts/clusters.js b/app/assets/javascripts/clusters.js deleted file mode 100644 index c9fef94efea..00000000000 --- a/app/assets/javascripts/clusters.js +++ /dev/null @@ -1,123 +0,0 @@ -/* globals Flash */ -import Visibility from 'visibilityjs'; -import axios from 'axios'; -import setAxiosCsrfToken from './lib/utils/axios_utils'; -import Poll from './lib/utils/poll'; -import { s__ } from './locale'; -import initSettingsPanels from './settings_panels'; -import Flash from './flash'; - -/** - * Cluster page has 2 separate parts: - * Toggle button - * - * - Polling status while creating or scheduled - * -- Update status area with the response result - */ - -class ClusterService { - constructor(options = {}) { - this.options = options; - setAxiosCsrfToken(); - } - fetchData() { - return axios.get(this.options.endpoint); - } -} - -export default class Clusters { - constructor() { - initSettingsPanels(); - - const dataset = document.querySelector('.js-edit-cluster-form').dataset; - - this.state = { - statusPath: dataset.statusPath, - clusterStatus: dataset.clusterStatus, - clusterStatusReason: dataset.clusterStatusReason, - toggleStatus: dataset.toggleStatus, - }; - - this.service = new ClusterService({ endpoint: this.state.statusPath }); - this.toggleButton = document.querySelector('.js-toggle-cluster'); - this.toggleInput = document.querySelector('.js-toggle-input'); - this.errorContainer = document.querySelector('.js-cluster-error'); - this.successContainer = document.querySelector('.js-cluster-success'); - this.creatingContainer = document.querySelector('.js-cluster-creating'); - this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); - - this.toggleButton.addEventListener('click', this.toggle.bind(this)); - - if (this.state.clusterStatus !== 'created') { - this.updateContainer(this.state.clusterStatus, this.state.clusterStatusReason); - } - - if (this.state.statusPath) { - this.initPolling(); - } - } - - toggle() { - this.toggleButton.classList.toggle('checked'); - this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); - } - - initPolling() { - this.poll = new Poll({ - resource: this.service, - method: 'fetchData', - successCallback: data => this.handleSuccess(data), - errorCallback: () => Clusters.handleError(), - }); - - if (!Visibility.hidden()) { - this.poll.makeRequest(); - } else { - this.service.fetchData() - .then(data => this.handleSuccess(data)) - .catch(() => Clusters.handleError()); - } - - Visibility.change(() => { - if (!Visibility.hidden()) { - this.poll.restart(); - } else { - this.poll.stop(); - } - }); - } - - static handleError() { - Flash(s__('ClusterIntegration|Something went wrong on our end.')); - } - - handleSuccess(data) { - const { status, status_reason } = data.data; - this.updateContainer(status, status_reason); - } - - hideAll() { - this.errorContainer.classList.add('hidden'); - this.successContainer.classList.add('hidden'); - this.creatingContainer.classList.add('hidden'); - } - - updateContainer(status, error) { - this.hideAll(); - switch (status) { - case 'created': - this.successContainer.classList.remove('hidden'); - break; - case 'errored': - this.errorContainer.classList.remove('hidden'); - this.errorReasonContainer.textContent = error; - break; - case 'scheduled': - case 'creating': - this.creatingContainer.classList.remove('hidden'); - break; - default: - this.hideAll(); - } - } -} diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js new file mode 100644 index 00000000000..053f11cc3c4 --- /dev/null +++ b/app/assets/javascripts/clusters/clusters_bundle.js @@ -0,0 +1,216 @@ +import Visibility from 'visibilityjs'; +import Vue from 'vue'; +import { s__, sprintf } from '../locale'; +import Flash from '../flash'; +import Poll from '../lib/utils/poll'; +import initSettingsPanels from '../settings_panels'; +import eventHub from './event_hub'; +import { + APPLICATION_INSTALLED, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, +} from './constants'; +import ClustersService from './services/clusters_service'; +import ClustersStore from './stores/clusters_store'; +import applications from './components/applications.vue'; + +/** + * Cluster page has 2 separate parts: + * Toggle button and applications section + * + * - Polling status while creating or scheduled + * - Update status area with the response result + */ + +export default class Clusters { + constructor() { + const { + statusPath, + installHelmPath, + installIngressPath, + installRunnerPath, + clusterStatus, + clusterStatusReason, + helpPath, + } = document.querySelector('.js-edit-cluster-form').dataset; + + this.store = new ClustersStore(); + this.store.setHelpPath(helpPath); + this.store.updateStatus(clusterStatus); + this.store.updateStatusReason(clusterStatusReason); + this.service = new ClustersService({ + endpoint: statusPath, + installHelmEndpoint: installHelmPath, + installIngresEndpoint: installIngressPath, + installRunnerEndpoint: installRunnerPath, + }); + + this.toggle = this.toggle.bind(this); + this.installApplication = this.installApplication.bind(this); + + this.toggleButton = document.querySelector('.js-toggle-cluster'); + this.toggleInput = document.querySelector('.js-toggle-input'); + this.errorContainer = document.querySelector('.js-cluster-error'); + this.successContainer = document.querySelector('.js-cluster-success'); + this.creatingContainer = document.querySelector('.js-cluster-creating'); + this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason'); + this.successApplicationContainer = document.querySelector('.js-cluster-application-notice'); + + initSettingsPanels(); + this.initApplications(); + + if (this.store.state.status !== 'created') { + this.updateContainer(this.store.state.status, this.store.state.statusReason); + } + + this.addListeners(); + if (statusPath) { + this.initPolling(); + } + } + + initApplications() { + const store = this.store; + const el = document.querySelector('#js-cluster-applications'); + + this.applications = new Vue({ + el, + components: { + applications, + }, + data() { + return { + state: store.state, + }; + }, + render(createElement) { + return createElement('applications', { + props: { + applications: this.state.applications, + helpPath: this.state.helpPath, + }, + }); + }, + }); + } + + addListeners() { + this.toggleButton.addEventListener('click', this.toggle); + eventHub.$on('installApplication', this.installApplication); + } + + removeListeners() { + this.toggleButton.removeEventListener('click', this.toggle); + eventHub.$off('installApplication', this.installApplication); + } + + initPolling() { + this.poll = new Poll({ + resource: this.service, + method: 'fetchData', + successCallback: data => this.handleSuccess(data), + errorCallback: () => Clusters.handleError(), + }); + + if (!Visibility.hidden()) { + this.poll.makeRequest(); + } else { + this.service.fetchData() + .then(data => this.handleSuccess(data)) + .catch(() => Clusters.handleError()); + } + + Visibility.change(() => { + if (!Visibility.hidden() && !this.destroyed) { + this.poll.restart(); + } else { + this.poll.stop(); + } + }); + } + + static handleError() { + Flash(s__('ClusterIntegration|Something went wrong on our end.')); + } + + handleSuccess(data) { + const prevApplicationMap = Object.assign({}, this.store.state.applications); + this.store.updateStateFromServer(data.data); + this.checkForNewInstalls(prevApplicationMap, this.store.state.applications); + this.updateContainer(this.store.state.status, this.store.state.statusReason); + } + + toggle() { + this.toggleButton.classList.toggle('checked'); + this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); + } + + hideAll() { + this.errorContainer.classList.add('hidden'); + this.successContainer.classList.add('hidden'); + this.creatingContainer.classList.add('hidden'); + } + + checkForNewInstalls(prevApplicationMap, newApplicationMap) { + const appTitles = Object.keys(newApplicationMap) + .filter(appId => newApplicationMap[appId].status === APPLICATION_INSTALLED && + prevApplicationMap[appId].status !== APPLICATION_INSTALLED && + prevApplicationMap[appId].status !== null) + .map(appId => newApplicationMap[appId].title); + + if (appTitles.length > 0) { + this.successApplicationContainer.textContent = sprintf(s__('ClusterIntegration|%{appList} was successfully installed on your cluster'), { + appList: appTitles.join(', '), + }); + this.successApplicationContainer.classList.remove('hidden'); + } else { + this.successApplicationContainer.classList.add('hidden'); + } + } + + updateContainer(status, error) { + this.hideAll(); + switch (status) { + case 'created': + this.successContainer.classList.remove('hidden'); + break; + case 'errored': + this.errorContainer.classList.remove('hidden'); + this.errorReasonContainer.textContent = error; + break; + case 'scheduled': + case 'creating': + this.creatingContainer.classList.remove('hidden'); + break; + default: + this.hideAll(); + } + } + + installApplication(appId) { + this.store.updateAppProperty(appId, 'requestStatus', REQUEST_LOADING); + this.store.updateAppProperty(appId, 'requestReason', null); + + this.service.installApplication(appId) + .then(() => { + this.store.updateAppProperty(appId, 'requestStatus', REQUEST_SUCCESS); + }) + .catch(() => { + this.store.updateAppProperty(appId, 'requestStatus', REQUEST_FAILURE); + this.store.updateAppProperty(appId, 'requestReason', s__('ClusterIntegration|Request to begin installing failed')); + }); + } + + destroy() { + this.destroyed = true; + + this.removeListeners(); + + if (this.poll) { + this.poll.stop(); + } + + this.applications.$destroy(); + } +} diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue new file mode 100644 index 00000000000..f8d53fcc4b7 --- /dev/null +++ b/app/assets/javascripts/clusters/components/application_row.vue @@ -0,0 +1,168 @@ +<script> +import { s__ } from '../../locale'; +import eventHub from '../event_hub'; +import loadingButton from '../../vue_shared/components/loading_button.vue'; +import { + APPLICATION_INSTALLABLE, + APPLICATION_INSTALLING, + APPLICATION_INSTALLED, + APPLICATION_ERROR, + REQUEST_LOADING, + REQUEST_SUCCESS, + REQUEST_FAILURE, +} from '../constants'; + +export default { + props: { + id: { + type: String, + required: true, + }, + title: { + type: String, + required: true, + }, + titleLink: { + type: String, + required: false, + }, + description: { + type: String, + required: true, + }, + status: { + type: String, + required: false, + }, + statusReason: { + type: String, + required: false, + }, + requestStatus: { + type: String, + required: false, + }, + requestReason: { + type: String, + required: false, + }, + }, + components: { + loadingButton, + }, + computed: { + rowJsClass() { + return `js-cluster-application-row-${this.id}`; + }, + titleElementType() { + return this.titleLink ? 'a' : 'span'; + }, + installButtonLoading() { + return !this.status || + this.status === APPLICATION_INSTALLING || + this.requestStatus === REQUEST_LOADING; + }, + installButtonDisabled() { + // 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 || + this.requestStatus === REQUEST_LOADING || + this.requestStatus === REQUEST_SUCCESS; + }, + installButtonLabel() { + let label; + if (this.status === APPLICATION_INSTALLABLE || this.status === APPLICATION_ERROR) { + label = s__('ClusterIntegration|Install'); + } else if (this.status === APPLICATION_INSTALLING) { + label = s__('ClusterIntegration|Installing'); + } else if (this.status === APPLICATION_INSTALLED) { + label = s__('ClusterIntegration|Installed'); + } + + return label; + }, + hasError() { + return this.status === APPLICATION_ERROR || this.requestStatus === REQUEST_FAILURE; + }, + }, + methods: { + installClicked() { + eventHub.$emit('installApplication', this.id); + }, + }, +}; +</script> + +<template> + <div + class="gl-responsive-table-row gl-responsive-table-row-col-span" + :class="rowJsClass" + > + <div + class="gl-responsive-table-row-layout" + role="row" + > + <component + :is="titleElementType" + :href="titleLink" + target="blank" + rel="noopener noreferrer" + role="gridcell" + class="table-section section-15 section-align-top js-cluster-application-title" + > + {{ title }} + </component> + <div + class="table-section section-wrap" + role="gridcell" + > + <div v-html="description"></div> + </div> + <div + class="table-section table-button-footer section-15 section-align-top" + role="gridcell" + > + <div class="btn-group table-action-buttons"> + <loading-button + class="js-cluster-application-install-button" + :loading="installButtonLoading" + :disabled="installButtonDisabled" + :label="installButtonLabel" + @click="installClicked" + /> + </div> + </div> + </div> + <div + v-if="hasError" + class="gl-responsive-table-row-layout" + role="row" + > + <div + class="alert alert-danger alert-block append-bottom-0 table-section section-100" + role="gridcell" + > + <div> + <p class="js-cluster-application-general-error-message"> + Something went wrong while installing {{ title }} + </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> diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue new file mode 100644 index 00000000000..865fa4f702e --- /dev/null +++ b/app/assets/javascripts/clusters/components/applications.vue @@ -0,0 +1,105 @@ +<script> +import _ from 'underscore'; +import { s__, sprintf } from '../../locale'; +import applicationRow from './application_row.vue'; + +export default { + props: { + applications: { + type: Object, + required: false, + default: () => ({}), + }, + helpPath: { + type: String, + required: false, + }, + }, + components: { + applicationRow, + }, + computed: { + generalApplicationDescription() { + return sprintf( + _.escape(s__('ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}')), { + helpLink: `<a href="${this.helpPath}"> + ${_.escape(s__('ClusterIntegration|installing applications'))} + </a>`, + }, + false, + ); + }, + helmTillerDescription() { + return _.escape(s__( + `ClusterIntegration|Helm streamlines installing and managing Kubernets applications. + Tiller runs inside of your Kubernetes Cluster, and manages + releases of your charts.`, + )); + }, + ingressDescription() { + const descriptionParagraph = _.escape(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.`, + )); + + const extraCostParagraph = sprintf( + _.escape(s__('ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}')), { + boldNotice: `<strong>${_.escape(s__('ClusterIntegration|Note:'))}</strong>`, + pricingLink: `<a href="https://cloud.google.com/compute/pricing#lb" target="blank" rel="noopener noreferrer"> + ${_.escape(s__('ClusterIntegration|GKE pricing'))} + </a>`, + }, + false, + ); + + return ` + <p> + ${descriptionParagraph} + </p> + <p class="append-bottom-0"> + ${extraCostParagraph} + </p> + `; + }, + gitlabRunnerDescription() { + return _.escape(s__( + `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs + and send the results back to GitLab.`, + )); + }, + }, +}; +</script> + +<template> + <section class="settings no-animate expanded"> + <div class="settings-header"> + <h4> + {{ s__('ClusterIntegration|Applications') }} + </h4> + <p + class="append-bottom-0" + v-html="generalApplicationDescription" + > + </p> + </div> + + <div class="settings-content"> + <div class="form_group append-bottom-20"> + <application-row + id="helm" + :title="applications.helm.title" + title-link="https://docs.helm.sh/" + :description="helmTillerDescription" + :status="applications.helm.status" + :status-reason="applications.helm.statusReason" + :request-status="applications.helm.requestStatus" + :request-reason="applications.helm.requestReason" + /> + <!-- NOTE: Don't forget to update `clusters.scss` min-height for this block and uncomment `application_spec` tests --> + <!-- Add Ingress row, all other plumbing is complete --> + <!-- Add GitLab Runner row, all other plumbing is complete --> + </div> + </div> + </section> +</template> diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js new file mode 100644 index 00000000000..3f202435716 --- /dev/null +++ b/app/assets/javascripts/clusters/constants.js @@ -0,0 +1,10 @@ +// These need to match what is returned from the server +export const APPLICATION_INSTALLABLE = 'installable'; +export const APPLICATION_INSTALLING = 'installing'; +export const APPLICATION_INSTALLED = 'installed'; +export const APPLICATION_ERROR = 'error'; + +// These are only used client-side +export const REQUEST_LOADING = 'request-loading'; +export const REQUEST_SUCCESS = 'request-success'; +export const REQUEST_FAILURE = 'request-failure'; diff --git a/app/assets/javascripts/clusters/event_hub.js b/app/assets/javascripts/clusters/event_hub.js new file mode 100644 index 00000000000..0948c2e5352 --- /dev/null +++ b/app/assets/javascripts/clusters/event_hub.js @@ -0,0 +1,3 @@ +import Vue from 'vue'; + +export default new Vue(); diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js new file mode 100644 index 00000000000..0ac8e68187d --- /dev/null +++ b/app/assets/javascripts/clusters/services/clusters_service.js @@ -0,0 +1,24 @@ +import axios from 'axios'; +import setAxiosCsrfToken from '../../lib/utils/axios_utils'; + +export default class ClusterService { + constructor(options = {}) { + setAxiosCsrfToken(); + + this.options = options; + this.appInstallEndpointMap = { + helm: this.options.installHelmEndpoint, + ingress: this.options.installIngressEndpoint, + runner: this.options.installRunnerEndpoint, + }; + } + + fetchData() { + return axios.get(this.options.endpoint); + } + + installApplication(appId) { + const endpoint = this.appInstallEndpointMap[appId]; + return axios.post(endpoint); + } +} diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js new file mode 100644 index 00000000000..e731cdc3042 --- /dev/null +++ b/app/assets/javascripts/clusters/stores/clusters_store.js @@ -0,0 +1,68 @@ +import { s__ } from '../../locale'; + +export default class ClusterStore { + constructor() { + this.state = { + helpPath: null, + status: null, + statusReason: null, + applications: { + helm: { + title: s__('ClusterIntegration|Helm Tiller'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + }, + ingress: { + title: s__('ClusterIntegration|Ingress'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + }, + runner: { + title: s__('ClusterIntegration|GitLab Runner'), + status: null, + statusReason: null, + requestStatus: null, + requestReason: null, + }, + }, + }; + } + + setHelpPath(helpPath) { + this.state.helpPath = helpPath; + } + + updateStatus(status) { + this.state.status = status; + } + + updateStatusReason(reason) { + this.state.statusReason = reason; + } + + updateAppProperty(appId, prop, value) { + this.state.applications[appId][prop] = value; + } + + updateStateFromServer(serverState = {}) { + this.state.status = serverState.status; + this.state.statusReason = serverState.status_reason; + serverState.applications.forEach((serverAppEntry) => { + const { + name: appId, + status, + status_reason: statusReason, + } = serverAppEntry; + + this.state.applications[appId] = { + ...(this.state.applications[appId] || {}), + status, + statusReason, + }; + }); + } +} diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js index 760fb0cdf67..44606989395 100644 --- a/app/assets/javascripts/dispatcher.js +++ b/app/assets/javascripts/dispatcher.js @@ -1,4 +1,5 @@ /* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, wrap-iife, no-shadow, consistent-return, one-var, one-var-declaration-per-line, camelcase, default-case, no-new, quotes, no-duplicate-case, no-case-declarations, no-fallthrough, max-len */ +import { s__ } from './locale'; /* global ProjectSelect */ import IssuableIndex from './issuable_index'; /* global Milestone */ @@ -32,6 +33,7 @@ import Labels from './labels'; import LabelManager from './label_manager'; /* global Sidebar */ +import Flash from './flash'; import CommitsList from './commits'; import Issue from './issue'; import BindInOut from './behaviors/bind_in_out'; @@ -543,9 +545,12 @@ import Diff from './diff'; new DueDateSelectors(); break; case 'projects:clusters:show': - import(/* webpackChunkName: "clusters" */ './clusters') + import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') .then(cluster => new cluster.default()) // eslint-disable-line new-cap - .catch(() => {}); + .catch((err) => { + Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript')); + throw err; + }); break; } switch (path[0]) { diff --git a/app/assets/javascripts/vue_shared/components/loading_button.vue b/app/assets/javascripts/vue_shared/components/loading_button.vue index 6670b554faf..0cc2653761c 100644 --- a/app/assets/javascripts/vue_shared/components/loading_button.vue +++ b/app/assets/javascripts/vue_shared/components/loading_button.vue @@ -26,6 +26,11 @@ export default { required: false, default: false, }, + disabled: { + type: Boolean, + required: false, + default: false, + }, label: { type: String, required: false, @@ -47,7 +52,7 @@ export default { class="btn btn-align-content" @click="onClick" type="button" - :disabled="loading" + :disabled="loading || disabled" > <transition name="fade"> <loading-icon diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss index 00a0e9cef67..16373b14ff7 100644 --- a/app/assets/stylesheets/framework/buttons.scss +++ b/app/assets/stylesheets/framework/buttons.scss @@ -294,6 +294,7 @@ .btn-align-content { display: flex; + justify-content: center; align-items: center; } diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss index 5c91579c69c..b9bfae9356c 100644 --- a/app/assets/stylesheets/pages/clusters.scss +++ b/app/assets/stylesheets/pages/clusters.scss @@ -3,3 +3,8 @@ background-color: $white-light; } } + +.cluster-applications-table { + // Wait for the Vue to kick-in and render the applications block + min-height: 179px; +} diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml index bd55136562a..de1f9fc5d32 100644 --- a/app/views/projects/clusters/show.html.haml +++ b/app/views/projects/clusters/show.html.haml @@ -4,15 +4,22 @@ - expanded = Rails.env.test? -- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) && @cluster.on_creation? +- status_path = status_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster.id, format: :json) if can?(current_user, :admin_cluster, @cluster) .edit-cluster-form.js-edit-cluster-form{ data: { status_path: status_path, + install_helm_path: 'TODO', + install_ingress_path: 'TODO', + install_runner_path: 'TODO', toggle_status: @cluster.enabled? ? 'true': 'false', cluster_status: @cluster.status_name, - cluster_status_reason: @cluster.status_reason } } + cluster_status_reason: @cluster.status_reason, + help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications') } } - %section.settings + + .hidden.js-cluster-application-notice.alert.alert-info.alert-block.append-bottom-10{ role: 'alert' } + + %section.settings.no-animate.expanded %h4= s_('ClusterIntegration|Enable cluster integration') - .settings-content.expanded + .settings-content .hidden.js-cluster-error.alert.alert-danger.alert-block.append-bottom-10{ role: 'alert' } = s_('ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine') @@ -49,6 +56,8 @@ .form-group = field.submit _('Save'), class: 'btn btn-success' + .cluster-applications-table#js-cluster-applications + %section.settings#js-cluster-details .settings-header %h4= s_('ClusterIntegration|Cluster details') @@ -59,7 +68,7 @@ .settings-content.no-animate{ class: ('expanded' if expanded) } .form_group.append-bottom-20 - %label.append-bottom-10{ for: 'cluter-name' } + %label.append-bottom-10{ for: 'cluster-name' } = s_('ClusterIntegration|Cluster name') .input-group %input.form-control.cluster-name{ value: @cluster.name, disabled: true } |