Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters/clusters_index.js57
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js4
-rw-r--r--app/assets/javascripts/dispatcher.js10
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue2
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/toggle_button.vue77
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/toggle.scss138
-rw-r--r--app/assets/stylesheets/pages/clusters.scss10
-rw-r--r--app/assets/stylesheets/pages/projects.scss87
-rw-r--r--app/controllers/projects/clusters_controller.rb42
-rw-r--r--app/finders/clusters_finder.rb29
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/models/clusters/cluster.rb4
-rw-r--r--app/services/clusters/create_service.rb6
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/projects/clusters/_cluster.html.haml22
-rw-r--r--app/views/projects/clusters/_empty_state.html.haml12
-rw-r--r--app/views/projects/clusters/_tabs.html.haml18
-rw-r--r--app/views/projects/clusters/index.html.haml24
-rw-r--r--app/views/projects/clusters/show.html.haml3
-rw-r--r--locale/gitlab.pot351
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb319
-rw-r--r--spec/factories/clusters/clusters.rb4
-rw-r--r--spec/features/projects/clusters_spec.rb248
-rw-r--r--spec/features/projects/features_visibility_spec.rb6
-rw-r--r--spec/finders/clusters_finder_spec.rb31
-rw-r--r--spec/helpers/clusters_helper_spec.rb45
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js2
-rw-r--r--spec/javascripts/clusters/clusters_index_spec.js58
-rw-r--r--spec/javascripts/fixtures/clusters.rb15
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js91
-rw-r--r--spec/models/clusters/cluster_spec.rb22
-rw-r--r--spec/services/clusters/create_service_spec.rb88
36 files changed, 1378 insertions, 512 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index cdb5c430aa9..2cfd6179a25 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -150,8 +150,8 @@ export default class Clusters {
}
toggle() {
- this.toggleButton.classList.toggle('checked');
- this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString());
+ this.toggleButton.classList.toggle('is-checked');
+ this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
}
showToken() {
diff --git a/app/assets/javascripts/clusters/clusters_index.js b/app/assets/javascripts/clusters/clusters_index.js
new file mode 100644
index 00000000000..3fd188a8770
--- /dev/null
+++ b/app/assets/javascripts/clusters/clusters_index.js
@@ -0,0 +1,57 @@
+import Flash from '../flash';
+import { s__ } from '../locale';
+import ClustersService from './services/clusters_service';
+/**
+ * Toggles loading and disabled classes.
+ * @param {HTMLElement} button
+ */
+const toggleLoadingButton = (button) => {
+ if (button.getAttribute('disabled')) {
+ button.removeAttribute('disabled');
+ } else {
+ button.setAttribute('disabled', true);
+ }
+
+ button.classList.toggle('is-loading');
+};
+
+/**
+ * Toggles checked class for the given button
+ * @param {HTMLElement} button
+ */
+const toggleValue = (button) => {
+ button.classList.toggle('is-checked');
+};
+
+/**
+ * Handles toggle buttons in the cluster's table.
+ *
+ * When the user clicks the toggle button for each cluster, it:
+ * - toggles the button
+ * - shows a loding and disabled state
+ * - Makes a put request to the given endpoint
+ * Once we receive the response, either:
+ * 1) Show updated status in case of successfull response
+ * 2) Show initial status in case of failed response
+ */
+export default function setClusterTableToggles() {
+ document.querySelectorAll('.js-toggle-cluster-list')
+ .forEach(button => button.addEventListener('click', (e) => {
+ const toggleButton = e.currentTarget;
+ const value = toggleButton.classList.contains('checked').toString();
+ const endpoint = toggleButton.getAttribute('data-endpoint');
+
+ toggleValue(toggleButton);
+ toggleLoadingButton(toggleButton);
+
+ ClustersService.updateCluster(endpoint, { cluster: { enabled: value } })
+ .then(() => {
+ toggleLoadingButton(toggleButton);
+ })
+ .catch(() => {
+ toggleLoadingButton(toggleButton);
+ toggleValue(toggleButton);
+ Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ });
+ }));
+}
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index ce14c9a9945..755c2981c2e 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) {
return axios.post(this.appInstallEndpointMap[appId]);
}
+
+ static updateCluster(endpoint, data) {
+ return axios.put(endpoint, data);
+ }
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index a21c92f24d6..7793140e608 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -558,7 +558,15 @@ import ProjectVariables from './project_variables';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => {
- Flash(s__('ClusterIntegration|Problem setting up the cluster JavaScript'));
+ Flash(s__('ClusterIntegration|Problem setting up the cluster'));
+ throw err;
+ });
+ break;
+ case 'projects:clusters:index':
+ import(/* webpackChunkName: "clusters_index" */ './clusters/clusters_index')
+ .then(clusterIndex => clusterIndex.default())
+ .catch((err) => {
+ Flash(s__('ClusterIntegration|Problem setting up the clusters list'));
throw err;
});
break;
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
index 80c5d39f736..8fce4c63872 100644
--- a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
@@ -1,5 +1,5 @@
<script>
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default {
props: {
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
deleted file mode 100644
index 2403c60186a..00000000000
--- a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-export default {
- props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- value: {
- type: Boolean,
- required: true,
- },
- disabledInput: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
-
- model: {
- prop: 'value',
- event: 'change',
- },
-
- methods: {
- toggleFeature() {
- if (!this.disabledInput) this.$emit('change', !this.value);
- },
- },
-};
-</script>
-
-<template>
- <label class="toggle-wrapper">
- <input
- v-if="name"
- type="hidden"
- :name="name"
- :value="value"
- />
- <button
- type="button"
- aria-label="Toggle"
- class="project-feature-toggle"
- data-enabled-text="Enabled"
- data-disabled-text="Disabled"
- :class="{ checked: value, disabled: disabledInput }"
- @click="toggleFeature"
- />
- </label>
-</template>
diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
index 326d9105666..639429baf26 100644
--- a/app/assets/javascripts/projects/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
@@ -1,6 +1,6 @@
<script>
import projectFeatureSetting from './project_feature_setting.vue';
-import projectFeatureToggle from './project_feature_toggle.vue';
+import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
import projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external';
diff --git a/app/assets/javascripts/vue_shared/components/toggle_button.vue b/app/assets/javascripts/vue_shared/components/toggle_button.vue
new file mode 100644
index 00000000000..ddc9ddbc3a3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/toggle_button.vue
@@ -0,0 +1,77 @@
+<script>
+ import loadingIcon from './loading_icon.vue';
+
+ export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ enabledText: {
+ type: String,
+ required: false,
+ default: 'Enabled',
+ },
+ disabledText: {
+ type: String,
+ required: false,
+ default: 'Disabled',
+ },
+ },
+
+ components: {
+ loadingIcon,
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature() {
+ if (!this.disabledInput) this.$emit('change', !this.value);
+ },
+ },
+ };
+</script>
+
+<template>
+ <label class="toggle-wrapper">
+ <input
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <button
+ type="button"
+ aria-label="Toggle"
+ class="project-feature-toggle"
+ :data-enabled-text="enabledText"
+ :data-disabled-text="disabledText"
+ :class="{
+ 'is-checked': value,
+ 'is-disabled': disabledInput,
+ 'is-loading': isLoading
+ }"
+ @click="toggleFeature"
+ >
+ <loadingIcon class="loading-icon" />
+ </button>
+ </label>
+</template>
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 66212be1b8f..43b16d3cf7d 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -44,6 +44,7 @@
@import "framework/tabs";
@import "framework/timeline";
@import "framework/tooltips";
+@import "framework/toggle";
@import "framework/typography";
@import "framework/zen";
@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/toggle.scss b/app/assets/stylesheets/framework/toggle.scss
new file mode 100644
index 00000000000..71765da3908
--- /dev/null
+++ b/app/assets/stylesheets/framework/toggle.scss
@@ -0,0 +1,138 @@
+/**
+* Toggle button
+*
+* @usage
+* ### Active and Inactive text should be provided as data attributes:
+* <button type="button" class="project-feature-toggle" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Checked should have `is-checked` class
+* <button type="button" class="project-feature-toggle is-checked" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Disabled should have `is-disabled` class
+* <button type="button" class="project-feature-toggle is-disabled" data-enabled-text="Enabled" data-disabled-text="Disabled" disabled="true">
+* <i class="fa fa-spinner fa-spin loading-icon hidden"></i>
+* </button>
+
+* ### Loading should have `is-loading` and an icon with `loading-icon` class
+* <button type="button" class="project-feature-toggle is-loading" data-enabled-text="Enabled" data-disabled-text="Disabled">
+* <i class="fa fa-spinner fa-spin loading-icon"></i>
+* </button>
+*/
+.project-feature-toggle {
+ position: relative;
+ border: 0;
+ outline: 0;
+ display: block;
+ width: 100px;
+ height: 24px;
+ cursor: pointer;
+ user-select: none;
+ background: $feature-toggle-color-disabled;
+ border-radius: 12px;
+ padding: 3px;
+ transition: all .4s ease;
+
+ &::selection,
+ &::before::selection,
+ &::after::selection {
+ background: none;
+ }
+
+ &::before {
+ color: $feature-toggle-text-color;
+ font-size: 12px;
+ line-height: 24px;
+ position: absolute;
+ top: 0;
+ left: 25px;
+ right: 5px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: animate-disabled .2s ease-in;
+ content: attr(data-disabled-text);
+ }
+
+ &::after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 22px;
+ height: 18px;
+ left: 0;
+ border-radius: 9px;
+ background: $feature-toggle-color;
+ transition: all .2s ease;
+ }
+
+ .loading-icon {
+ display: none;
+ font-size: 12px;
+ color: $white-light;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+
+ }
+
+ &.is-loading {
+ &::before {
+ display: none;
+ }
+
+ .loading-icon {
+ display: block;
+
+ &::before {
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: translate(-50%, -50%);
+ }
+ }
+ }
+
+ &.is-checked {
+ background: $feature-toggle-color-enabled;
+
+ &::before {
+ left: 5px;
+ right: 25px;
+ animation: animate-enabled .2s ease-in;
+ content: attr(data-enabled-text);
+ }
+
+ &::after {
+ left: calc(100% - 22px);
+ }
+ }
+
+ &.is-disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ width: 50px;
+
+ &::before,
+ &.is-checked::before {
+ display: none;
+ }
+ }
+
+ @keyframes animate-enabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ @keyframes animate-disabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index 83e211d6086..b5ac6db04ad 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -14,3 +14,13 @@
}
@include new-style-dropdown('.clusters-dropdown ');
+
+.clusters-container {
+ .nav-bar-right {
+ padding: $gl-padding-top $gl-padding;
+ }
+
+ .empty-state .svg-content img {
+ width: 145px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 2dc0c288a6d..2856af37f8d 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -126,93 +126,6 @@
}
}
-.project-feature-toggle {
- position: relative;
- border: 0;
- outline: 0;
- display: block;
- width: 100px;
- height: 24px;
- cursor: pointer;
- user-select: none;
- background: $feature-toggle-color-disabled;
- border-radius: 12px;
- padding: 3px;
- transition: all .4s ease;
-
- &::selection,
- &::before::selection,
- &::after::selection {
- background: none;
- }
-
- &::before {
- color: $feature-toggle-text-color;
- font-size: 12px;
- line-height: 24px;
- position: absolute;
- top: 0;
- left: 25px;
- right: 5px;
- text-align: center;
- overflow: hidden;
- text-overflow: ellipsis;
- animation: animate-disabled .2s ease-in;
- content: attr(data-disabled-text);
- }
-
- &::after {
- position: relative;
- display: block;
- content: "";
- width: 22px;
- height: 18px;
- left: 0;
- border-radius: 9px;
- background: $feature-toggle-color;
- transition: all .2s ease;
- }
-
- &.checked {
- background: $feature-toggle-color-enabled;
-
- &::before {
- left: 5px;
- right: 25px;
- animation: animate-enabled .2s ease-in;
- content: attr(data-enabled-text);
- }
-
- &::after {
- left: calc(100% - 22px);
- }
- }
-
- &.disabled {
- opacity: 0.4;
- cursor: not-allowed;
- }
-
- @media (max-width: $screen-xs-min) {
- width: 50px;
-
- &::before,
- &.checked::before {
- display: none;
- }
- }
-
- @keyframes animate-enabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-
- @keyframes animate-disabled {
- 0%, 35% { opacity: 0; }
- 100% { opacity: 1; }
- }
-}
-
.project-home-panel,
.group-home-panel {
padding-top: 24px;
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 639874e6231..ae22243c0ee 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -8,11 +8,12 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000
def index
- if project.cluster
- redirect_to project_cluster_path(project, project.cluster)
- else
- redirect_to new_project_cluster_path(project)
- end
+ @scope = params[:scope] || 'all'
+ clusters = ClustersFinder.new(project, current_user, @scope).execute
+ @clusters = clusters.page(params[:page])
+ @active_count = project.clusters.enabled.count
+ @inactive_count = project.clusters.disabled.count
+ @all_count = @active_count + @inactive_count
end
def new
@@ -39,10 +40,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster)
if cluster.valid?
- flash[:notice] = "Cluster was successfully updated."
- redirect_to project_cluster_path(project, project.cluster)
+ respond_to do |format|
+ format.json do
+ head :no_content
+ end
+ format.html do
+ flash[:notice] = "Cluster was successfully updated."
+ redirect_to project_cluster_path(project, project.cluster)
+ end
+ end
else
- render :show
+ respond_to do |format|
+ format.json { head :bad_request }
+ format.html { render :show }
+ end
end
end
@@ -59,7 +70,20 @@ class Projects::ClustersController < Projects::ApplicationController
private
def cluster
- @cluster ||= project.clusters.find(params[:id]).present(current_user: current_user) || render_404
+ @cluster ||= project.clusters.find_by(id: params[:id])&.present(current_user: current_user) || render_404
+ end
+
+ def create_params
+ params.require(:cluster).permit(
+ :enabled,
+ :name,
+ :provider_type,
+ provider_gcp_attributes: [
+ :gcp_project_id,
+ :zone,
+ :num_nodes,
+ :machine_type
+ ])
end
def update_params
diff --git a/app/finders/clusters_finder.rb b/app/finders/clusters_finder.rb
new file mode 100644
index 00000000000..c13f98257bf
--- /dev/null
+++ b/app/finders/clusters_finder.rb
@@ -0,0 +1,29 @@
+class ClustersFinder
+ def initialize(project, user, scope)
+ @project = project
+ @user = user
+ @scope = scope || :active
+ end
+
+ def execute
+ clusters = project.clusters
+ filter_by_scope(clusters)
+ end
+
+ private
+
+ attr_reader :project, :user, :scope
+
+ def filter_by_scope(clusters)
+ case scope.to_sym
+ when :all
+ clusters
+ when :inactive
+ clusters.disabled
+ when :active
+ clusters.enabled
+ else
+ raise "Invalid scope #{scope}"
+ end
+ end
+end
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
new file mode 100644
index 00000000000..f8281c893fa
--- /dev/null
+++ b/app/helpers/clusters_helper.rb
@@ -0,0 +1,5 @@
+module ClustersHelper
+ def can_toggle_cluster?(cluster)
+ can?(current_user, :update_cluster, cluster) && cluster.created?
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 441cbcb701d..c97a6fdc78a 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -55,6 +55,10 @@ module Clusters
end
end
+ def created?
+ status_name == :created
+ end
+
def applications
[
application_helm || build_application_helm,
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 1d407739b21..a47ce5a8887 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -5,6 +5,8 @@ module Clusters
def execute(access_token)
@access_token = access_token
+ raise Exception.new('Instance does not support multiple clusters') unless can_create_cluster?
+
create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end
@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project])
end
+
+ def can_create_cluster?
+ return project.clusters.empty?
+ end
end
end
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 8b2d2a5c74d..53a9162b703 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -187,7 +187,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span
- Cluster
+ Clusters
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do
diff --git a/app/views/projects/clusters/_cluster.html.haml b/app/views/projects/clusters/_cluster.html.haml
new file mode 100644
index 00000000000..ba36828d407
--- /dev/null
+++ b/app/views/projects/clusters/_cluster.html.haml
@@ -0,0 +1,22 @@
+.gl-responsive-table-row
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Cluster")
+ .table-mobile-content
+ = link_to cluster.name, namespace_project_cluster_path(@project.namespace, @project, cluster)
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Environment pattern")
+ .table-mobile-content= cluster.environment_scope
+ .table-section.section-30
+ .table-mobile-header{ role: "rowheader" }= s_("ClusterIntegration|Project namespace")
+ .table-mobile-content= cluster.platform_kubernetes&.namespace
+ .table-section.section-10
+ .table-mobile-header{ role: "rowheader" }
+ .table-mobile-content
+ %button{ type: "button",
+ class: "js-toggle-cluster-list project-feature-toggle #{'is-checked' if cluster.enabled?} #{'is-disabled' if !can_toggle_cluster?(cluster)}",
+ "aria-label": s_("ClusterIntegration|Toggle Cluster"),
+ disabled: !can_toggle_cluster?(cluster),
+ data: { "enabled-text": s_("ClusterIntegration|Active"),
+ "disabled-text": s_("ClusterIntegration|Inactive"),
+ endpoint: namespace_project_cluster_path(@project.namespace, @project, cluster, format: :json) } }
+ = icon("spinner spin", class: "loading-icon")
diff --git a/app/views/projects/clusters/_empty_state.html.haml b/app/views/projects/clusters/_empty_state.html.haml
new file mode 100644
index 00000000000..e629cc58b06
--- /dev/null
+++ b/app/views/projects/clusters/_empty_state.html.haml
@@ -0,0 +1,12 @@
+.row.empty-state
+ .col-xs-12
+ .svg-content= image_tag 'illustrations/clusters_empty.svg'
+ .col-xs-12.text-center
+ .text-content
+ %h4= s_('ClusterIntegration|Integrate cluster automation')
+ - link_to_help_page = link_to(s_('ClusterIntegration|Learn more about Clusters'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ %p= s_('ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}').html_safe % { link_to_help_page: link_to_help_page}
+
+ %p
+ = link_to s_('ClusterIntegration|Add cluster'), new_project_cluster_path(@project), class: 'btn btn-success'
+
diff --git a/app/views/projects/clusters/_tabs.html.haml b/app/views/projects/clusters/_tabs.html.haml
new file mode 100644
index 00000000000..955a9940727
--- /dev/null
+++ b/app/views/projects/clusters/_tabs.html.haml
@@ -0,0 +1,18 @@
+.top-area.scrolling-tabs-container.inner-page-scroll-tabs
+ .fade-left= icon("angle-left")
+ .fade-right= icon("angle-right")
+ %ul.nav-links.scrolling-tabs
+ %li{ class: ('active' if @scope == 'active') }>
+ = link_to project_clusters_path(@project, scope: :active), class: "js-active-tab" do
+ = s_("ClusterIntegration|Active")
+ %span.badge= @active_count
+ %li{ class: ('active' if @scope == 'inactive') }>
+ = link_to project_clusters_path(@project, scope: :inactive), class: "js-inactive-tab" do
+ = s_("ClusterIntegration|Inactive")
+ %span.badge= @inactive_count
+ %li{ class: ('active' if @scope.nil? || @scope == 'all') }>
+ = link_to project_clusters_path(@project), class: "js-all-tab" do
+ = s_("ClusterIntegration|All")
+ %span.badge= @all_count
+ .pull-right.nav-bar-right
+ = link_to s_("ClusterIntegration|Add cluster"), new_project_cluster_path(@project), class: "btn btn-success disabled has-tooltip js-add-cluster", title: s_("ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate")
diff --git a/app/views/projects/clusters/index.html.haml b/app/views/projects/clusters/index.html.haml
new file mode 100644
index 00000000000..4dd956971ae
--- /dev/null
+++ b/app/views/projects/clusters/index.html.haml
@@ -0,0 +1,24 @@
+- breadcrumb_title "Clusters"
+- page_title "Clusters"
+
+.clusters-container
+ - if !@clusters.empty?
+ = render "tabs"
+ .ci-table.js-clusters-list
+ .gl-responsive-table-row.table-row-header{ role: "row" }
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Cluster")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Environment pattern")
+ .table-section.section-30{ role: "rowheader" }
+ = s_("ClusterIntegration|Project namespace")
+ .table-section.section-10{ role: "rowheader" }
+ - @clusters.each do |cluster|
+ = render "cluster", cluster: cluster
+ = paginate @clusters, theme: "gitlab"
+ - elsif @scope == 'all'
+ = render "empty_state"
+ - else
+ = render "tabs"
+ .prepend-top-20.text-center
+ = s_("ClusterIntegration|There are no clusters to show")
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index d23efe4d9aa..c177e675b3c 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -1,5 +1,6 @@
- @content_class = "limit-container-width" unless fluid_layout
-- breadcrumb_title "Cluster"
+- add_to_breadcrumbs "Clusters", project_clusters_path(@project)
+- breadcrumb_title @cluster.id
- page_title _("Cluster")
- expanded = Rails.env.test?
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 32afb7b06e4..740701a71da 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-10-22 16:40+0300\n"
-"PO-Revision-Date: 2017-10-22 16:40+0300\n"
+"POT-Creation-Date: 2017-12-02 15:22+0000\n"
+"PO-Revision-Date: 2017-12-02 15:22+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -36,6 +36,11 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr ""
+msgid "%{count} participant"
+msgid_plural "%{count} participants"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr ""
@@ -56,6 +61,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)."
msgstr ""
+msgid "+ %{moreCount} more"
+msgstr ""
+
+msgid "- show less"
+msgstr ""
+
msgid "1 pipeline"
msgid_plural "%d pipelines"
msgstr[0] ""
@@ -115,6 +126,12 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "An error occurred when toggling the notification subscription"
+msgstr ""
+
+msgid "An error occurred while fetching sidebar data"
+msgstr ""
+
msgid "An error occurred. Please try again."
msgstr ""
@@ -184,6 +201,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr ""
+msgid "Available"
+msgstr ""
+
msgid "Branch"
msgid_plural "Branches"
msgstr[0] ""
@@ -399,7 +419,34 @@ msgstr ""
msgid "Cluster"
msgstr ""
-msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgid "ClusterIntegration|%{appList} was successfully installed on your cluster"
+msgstr ""
+
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which incur additional costs. See %{pricingLink}"
+msgstr ""
+
+msgid "ClusterIntegration|A cluster has been set up on this project through the Kubernetes integration page"
+msgstr ""
+
+msgid "ClusterIntegration|Active"
+msgstr ""
+
+msgid "ClusterIntegration|Add an existing cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Add cluster"
+msgstr ""
+
+msgid "ClusterIntegration|All"
+msgstr ""
+
+msgid "ClusterIntegration|Applications"
+msgstr ""
+
+msgid "ClusterIntegration|Choose how to set up cluster integration"
+msgstr ""
+
+msgid "ClusterIntegration|Cluster"
msgstr ""
msgid "ClusterIntegration|Cluster details"
@@ -420,24 +467,48 @@ msgstr ""
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
msgstr ""
+msgid "ClusterIntegration|Cluster management"
+msgstr ""
+
msgid "ClusterIntegration|Cluster name"
msgstr ""
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
msgstr ""
+msgid "ClusterIntegration|Clusters allow you to use review apps, deploy your applications, run your pipelines, and much more in an easy way. %{link_to_help_page}"
+msgstr ""
+
msgid "ClusterIntegration|Copy cluster name"
msgstr ""
+msgid "ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab"
+msgstr ""
+
msgid "ClusterIntegration|Create cluster"
msgstr ""
msgid "ClusterIntegration|Create new cluster on Google Container Engine"
msgstr ""
+msgid "ClusterIntegration|Create on GKE"
+msgstr ""
+
msgid "ClusterIntegration|Enable cluster integration"
msgstr ""
+msgid "ClusterIntegration|Enter the details for an existing Kubernetes cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Environment pattern"
+msgstr ""
+
+msgid "ClusterIntegration|GKE pricing"
+msgstr ""
+
+msgid "ClusterIntegration|GitLab Runner"
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr ""
@@ -447,9 +518,36 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project"
msgstr ""
+msgid "ClusterIntegration|Helm Tiller"
+msgstr ""
+
+msgid "ClusterIntegration|Inactive"
+msgstr ""
+
+msgid "ClusterIntegration|Ingress"
+msgstr ""
+
+msgid "ClusterIntegration|Install"
+msgstr ""
+
+msgid "ClusterIntegration|Install applications on your cluster. Read more about %{helpLink}"
+msgstr ""
+
+msgid "ClusterIntegration|Installed"
+msgstr ""
+
+msgid "ClusterIntegration|Installing"
+msgstr ""
+
+msgid "ClusterIntegration|Integrate cluster automation"
+msgstr ""
+
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
+msgid "ClusterIntegration|Learn more about Clusters"
+msgstr ""
+
msgid "ClusterIntegration|Machine type"
msgstr ""
@@ -459,16 +557,31 @@ msgstr ""
msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
msgstr ""
+msgid "ClusterIntegration|Manage Kubernetes integration"
+msgstr ""
+
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr ""
+msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
+msgstr ""
+
+msgid "ClusterIntegration|Note:"
+msgstr ""
+
msgid "ClusterIntegration|Number of nodes"
msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
-msgid "ClusterIntegration|Project namespace (optional, unique)"
+msgid "ClusterIntegration|Problem setting up the cluster"
+msgstr ""
+
+msgid "ClusterIntegration|Problem setting up the clusters list"
+msgstr ""
+
+msgid "ClusterIntegration|Project namespace"
msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
@@ -483,6 +596,9 @@ msgstr ""
msgid "ClusterIntegration|Removing cluster integration will remove the cluster configuration you have added to this project. It will not delete your cluster on Google Container Engine."
msgstr ""
+msgid "ClusterIntegration|Request to begin installing failed"
+msgstr ""
+
msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr ""
@@ -501,6 +617,15 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr ""
+msgid "ClusterIntegration|Something went wrong while installing %{title}"
+msgstr ""
+
+msgid "ClusterIntegration|There are no clusters to show"
+msgstr ""
+
+msgid "ClusterIntegration|This account must have permissions to create a cluster in the %{link_to_container_project} specified below"
+msgstr ""
+
msgid "ClusterIntegration|Toggle Cluster"
msgstr ""
@@ -522,6 +647,9 @@ msgstr ""
msgid "ClusterIntegration|help page"
msgstr ""
+msgid "ClusterIntegration|installing applications"
+msgstr ""
+
msgid "ClusterIntegration|meets the requirements"
msgstr ""
@@ -536,6 +664,11 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit %d file"
+msgid_plural "Commit %d files"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
msgstr ""
@@ -617,6 +750,15 @@ msgstr ""
msgid "Contributors"
msgstr ""
+msgid "ContributorsPage|Building repository graph."
+msgstr ""
+
+msgid "ContributorsPage|Commits to %{branch_name}, excluding merge commits. Limited to 6,000 commits."
+msgstr ""
+
+msgid "ContributorsPage|Please wait a moment, this page will automatically refresh when ready."
+msgstr ""
+
msgid "Copy URL to clipboard"
msgstr ""
@@ -635,12 +777,21 @@ msgstr ""
msgid "Create empty bare repository"
msgstr ""
+msgid "Create file"
+msgstr ""
+
msgid "Create merge request"
msgstr ""
msgid "Create new branch"
msgstr ""
+msgid "Create new directory"
+msgstr ""
+
+msgid "Create new file"
+msgstr ""
+
msgid "Create new..."
msgstr ""
@@ -766,6 +917,60 @@ msgstr ""
msgid "Emails"
msgstr ""
+msgid "Environments|An error occurred while fetching the environments."
+msgstr ""
+
+msgid "Environments|An error occurred while making the request."
+msgstr ""
+
+msgid "Environments|Commit"
+msgstr ""
+
+msgid "Environments|Deployment"
+msgstr ""
+
+msgid "Environments|Environment"
+msgstr ""
+
+msgid "Environments|Environments"
+msgstr ""
+
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr ""
+
+msgid "Environments|Job"
+msgstr ""
+
+msgid "Environments|New environment"
+msgstr ""
+
+msgid "Environments|No deployments yet"
+msgstr ""
+
+msgid "Environments|Open"
+msgstr ""
+
+msgid "Environments|Re-deploy"
+msgstr ""
+
+msgid "Environments|Read more about environments"
+msgstr ""
+
+msgid "Environments|Rollback"
+msgstr ""
+
+msgid "Environments|Show all"
+msgstr ""
+
+msgid "Environments|Updated"
+msgstr ""
+
+msgid "Environments|You don't have any environments right now."
+msgstr ""
+
+msgid "Error occurred when toggling the notification subscription"
+msgstr ""
+
msgid "EventFilterBy|Filter by all"
msgstr ""
@@ -805,6 +1010,9 @@ msgstr ""
msgid "Failed to remove the pipeline schedule"
msgstr ""
+msgid "File name"
+msgstr ""
+
msgid "Files"
msgstr ""
@@ -1131,6 +1339,12 @@ msgstr ""
msgid "No schedules"
msgstr ""
+msgid "No time spent"
+msgstr ""
+
+msgid "None"
+msgstr ""
+
msgid "Not available"
msgstr ""
@@ -1194,6 +1408,12 @@ msgstr ""
msgid "Notifications"
msgstr ""
+msgid "Number of access attempts"
+msgstr ""
+
+msgid "Number of failures before backing off"
+msgstr ""
+
msgid "OfSearchInADropdown|Filter"
msgstr ""
@@ -1455,6 +1675,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr ""
+msgid "PrometheusService|By default, Prometheus listens on ‘http://localhost:9090’. It’s not recommended to change the default address and port as this might affect or conflict with other services running on the GitLab server."
+msgstr ""
+
+msgid "PrometheusService|Finding and configuring metrics..."
+msgstr ""
+
+msgid "PrometheusService|Metrics"
+msgstr ""
+
+msgid "PrometheusService|Metrics are automatically configured and monitored based on a library of metrics from popular exporters."
+msgstr ""
+
+msgid "PrometheusService|Missing environment variable"
+msgstr ""
+
+msgid "PrometheusService|Monitored"
+msgstr ""
+
+msgid "PrometheusService|More information"
+msgstr ""
+
+msgid "PrometheusService|No metrics are being monitored. To start monitoring, deploy to an environment."
+msgstr ""
+
+msgid "PrometheusService|Prometheus API Base URL, like http://prometheus.example.com/"
+msgstr ""
+
+msgid "PrometheusService|Prometheus monitoring"
+msgstr ""
+
+msgid "PrometheusService|View environments"
+msgstr ""
+
msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr ""
@@ -1721,9 +1974,15 @@ msgstr ""
msgid "Start the Runner!"
msgstr ""
+msgid "Stopped"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
+msgid "Subscribe"
+msgstr ""
+
msgid "Switch branch/tag"
msgstr ""
@@ -1738,12 +1997,84 @@ msgstr[1] ""
msgid "Tags"
msgstr ""
+msgid "TagsPage|Browse commits"
+msgstr ""
+
+msgid "TagsPage|Browse files"
+msgstr ""
+
+msgid "TagsPage|Can't find HEAD commit for this tag"
+msgstr ""
+
+msgid "TagsPage|Cancel"
+msgstr ""
+
+msgid "TagsPage|Create tag"
+msgstr ""
+
+msgid "TagsPage|Delete tag"
+msgstr ""
+
+msgid "TagsPage|Deleting the %{tag_name} tag cannot be undone. Are you sure?"
+msgstr ""
+
+msgid "TagsPage|Edit release notes"
+msgstr ""
+
+msgid "TagsPage|Existing branch name, tag, or commit SHA"
+msgstr ""
+
+msgid "TagsPage|Filter by tag name"
+msgstr ""
+
+msgid "TagsPage|New Tag"
+msgstr ""
+
+msgid "TagsPage|New tag"
+msgstr ""
+
+msgid "TagsPage|Optionally, add a message to the tag."
+msgstr ""
+
+msgid "TagsPage|Optionally, add release notes to the tag. They will be stored in the GitLab database and displayed on the tags page."
+msgstr ""
+
+msgid "TagsPage|Release notes"
+msgstr ""
+
+msgid "TagsPage|Repository has no tags yet."
+msgstr ""
+
+msgid "TagsPage|Sort by"
+msgstr ""
+
+msgid "TagsPage|Tags"
+msgstr ""
+
+msgid "TagsPage|Tags give the ability to mark specific points in history as being important"
+msgstr ""
+
+msgid "TagsPage|This tag has no release notes."
+msgstr ""
+
+msgid "TagsPage|Use git tag command to add a new one:"
+msgstr ""
+
+msgid "TagsPage|Write your release notes or drag files here..."
+msgstr ""
+
+msgid "TagsPage|protected"
+msgstr ""
+
msgid "Target Branch"
msgstr ""
msgid "Team"
msgstr ""
+msgid "The circuitbreaker backoff threshold should be lower than the failure count threshold"
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
@@ -1756,6 +2087,12 @@ msgstr ""
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
+msgid "The number of attempts GitLab will make to access a storage."
+msgstr ""
+
+msgid "The number of failures after which GitLab will start temporarily disabling access to a storage shard on a host"
+msgstr ""
+
msgid "The number of failures of after which GitLab will completely prevent access to the storage. The number of failures can be reset in the admin interface: %{link_to_health_page} or using the %{api_documentation_link}."
msgstr ""
@@ -1976,6 +2313,9 @@ msgstr ""
msgid "Total Time"
msgstr ""
+msgid "Total issue time spent"
+msgstr ""
+
msgid "Total test time for all commits/merges"
msgstr ""
@@ -1988,6 +2328,9 @@ msgstr ""
msgid "Unstar"
msgstr ""
+msgid "Unsubscribe"
+msgstr ""
+
msgid "Upload New File"
msgstr ""
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 0612e0b022f..e69de29bb2d 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -1,319 +0,0 @@
-require 'spec_helper'
-
-describe Projects::ClustersController do
- include AccessMatchersForController
- include GoogleApi::CloudPlatformHelpers
-
- set(:project) { create(:project) }
-
- describe 'GET index' do
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when project has a cluster' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it { expect(go).to redirect_to(namespace_project_cluster_path(project.namespace, project, project.cluster)) }
- end
-
- context 'when project does not have a cluster' do
- it { expect(go).to redirect_to(new_namespace_project_cluster_path(project.namespace, project)) }
- end
- end
-
- describe 'security' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :index, namespace_id: project.namespace.to_param, project_id: project
- end
- end
-
- describe 'GET status' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- it "responds with matching schema" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to match_response_schema('cluster_status')
- end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :status, namespace_id: project.namespace,
- project_id: project,
- id: cluster,
- format: :json
- end
- end
-
- describe 'GET show' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- it "renders view" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(assigns(:cluster)).to eq(cluster)
- end
- end
-
- describe 'security' do
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- get :show, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
- end
-
- describe 'PUT update' do
- context 'Managed' do
- let(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when changing parameters' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
- }
- }
- end
-
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(namespace_project_cluster_path(project.namespace, project, project.cluster))
- expect(flash[:notice]).to eq('Cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- end
-
- it "does not change cluster name" do
- go
-
- expect(cluster.name).to eq('test-cluster')
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it "rejects changes" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- expect(cluster.enabled).to be_truthy
- end
- end
- end
- end
-
- context 'User' do
- let(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'when changing parameters' do
- let(:params) do
- {
- cluster: {
- enabled: false,
- name: 'my-new-cluster-name',
- platform_kubernetes_attributes: {
- namespace: 'my-namespace'
- }
- }
- }
- end
-
- it "updates and redirects back to show page" do
- go
-
- cluster.reload
- expect(response).to redirect_to(namespace_project_cluster_path(project.namespace, project, project.cluster))
- expect(flash[:notice]).to eq('Cluster was successfully updated.')
- expect(cluster.enabled).to be_falsey
- expect(cluster.name).to eq('my-new-cluster-name')
- expect(cluster.platform_kubernetes.namespace).to eq('my-namespace')
- end
-
- context 'when cluster is being created' do
- let(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it "rejects changes" do
- go
-
- expect(response).to have_gitlab_http_status(:ok)
- expect(response).to render_template(:show)
- expect(cluster.enabled).to be_truthy
- end
- end
- end
- end
-
- describe 'security' do
- set(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- let(:params) do
- { cluster: { enabled: false } }
- end
-
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- put :update, params.merge(namespace_id: project.namespace,
- project_id: project,
- id: cluster)
- end
- end
-
- describe 'DELETE destroy' do
- describe 'functionality' do
- let(:user) { create(:user) }
-
- before do
- project.add_master(user)
- sign_in(user)
- end
-
- context 'GCP' do
- context 'when cluster is created' do
- let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
-
- expect(response).to redirect_to(namespace_project_clusters_path(project.namespace, project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
- end
- end
-
- context 'when cluster is being created' do
- let!(:cluster) { create(:cluster, :providing_by_gcp, projects: [project]) }
-
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(-1)
-
- expect(response).to redirect_to(namespace_project_clusters_path(project.namespace, project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
- end
- end
- end
-
- context 'User' do
- context 'when provider is user' do
- let!(:cluster) { create(:cluster, :provided_by_user, projects: [project]) }
-
- it "destroys and redirects back to clusters list" do
- expect { go }
- .to change { Clusters::Cluster.count }.by(-1)
- .and change { Clusters::Platforms::Kubernetes.count }.by(-1)
- .and change { Clusters::Providers::Gcp.count }.by(0)
-
- expect(response).to redirect_to(namespace_project_clusters_path(project.namespace, project))
- expect(flash[:notice]).to eq('Cluster integration was successfully removed.')
- end
- end
- end
- end
-
- describe 'security' do
- set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
-
- it { expect { go }.to be_allowed_for(:admin) }
- it { expect { go }.to be_allowed_for(:owner).of(project) }
- it { expect { go }.to be_allowed_for(:master).of(project) }
- it { expect { go }.to be_denied_for(:developer).of(project) }
- it { expect { go }.to be_denied_for(:reporter).of(project) }
- it { expect { go }.to be_denied_for(:guest).of(project) }
- it { expect { go }.to be_denied_for(:user) }
- it { expect { go }.to be_denied_for(:external) }
- end
-
- def go
- delete :destroy, namespace_id: project.namespace,
- project_id: project,
- id: cluster
- end
- end
-end
diff --git a/spec/factories/clusters/clusters.rb b/spec/factories/clusters/clusters.rb
index 81866845a20..9e73a19e856 100644
--- a/spec/factories/clusters/clusters.rb
+++ b/spec/factories/clusters/clusters.rb
@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating]
end
+
+ trait :disabled do
+ enabled false
+ end
end
end
diff --git a/spec/features/projects/clusters_spec.rb b/spec/features/projects/clusters_spec.rb
index 4243c4fd266..ad515649202 100644
--- a/spec/features/projects/clusters_spec.rb
+++ b/spec/features/projects/clusters_spec.rb
@@ -11,10 +11,256 @@ feature 'Clusters', :js do
gitlab_sign_in(user)
end
+<<<<<<< HEAD
context 'when user does not have a cluster and visits cluster index page' do
+=======
+ context 'when user has signed in Google' do
+ before do
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:token_in_session).and_return('token')
+ allow_any_instance_of(Projects::ClustersController)
+ .to receive(:expires_at_in_session).and_return(1.hour.since.to_i.to_s)
+ end
+
+ context 'when user does not have a cluster and visits cluster index page' do
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'sees empty state' do
+ expect(page).to have_link('Add cluster')
+ expect(page).to have_selector('.empty-state')
+ end
+
+ context 'when user opens create on gke page' do
+ before do
+ click_link 'Add cluster'
+ click_link 'Create on GKE'
+ end
+
+ context 'when user filled form with valid parameters' do
+ before do
+ double.tap do |dbl|
+ allow(dbl).to receive(:status).and_return('RUNNING')
+ allow(dbl).to receive(:self_link)
+ .and_return('projects/gcp-project-12345/zones/us-central1-a/operations/ope-123')
+ allow_any_instance_of(GoogleApi::CloudPlatform::Client)
+ .to receive(:projects_zones_clusters_create).and_return(dbl)
+ end
+
+ allow(WaitForClusterCreationWorker).to receive(:perform_in).and_return(nil)
+
+ fill_in 'cluster_provider_gcp_attributes_gcp_project_id', with: 'gcp-project-123'
+ fill_in 'cluster_name', with: 'dev-cluster'
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a cluster details page and creation status' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ end
+
+ Clusters::Cluster.last.provider.make_created!
+
+ expect(page).to have_content('Cluster was successfully created on Google Container Engine')
+ end
+
+ it 'user sees a error if something worng during creation' do
+ expect(page).to have_content('Cluster is being created on Google Container Engine...')
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).to have_content('Something wrong!')
+ end
+ end
+
+ context 'when user filled form with invalid parameters' do
+ before do
+ click_button 'Create cluster'
+ end
+
+ it 'user sees a validation error' do
+ expect(page).to have_css('#error_explanation')
+ end
+ end
+ end
+ end
+
+ context 'when user has a cluster and visits cluster index page' do
+ let!(:cluster) { create(:cluster, :project, :provided_by_gcp) }
+ let(:project) { cluster.project }
+
+ before do
+ visit project_clusters_path(project)
+ end
+
+ it 'user sees a table with one cluster' do
+ # One is the header row, the other the cluster row
+ expect(page).to have_selector('.gl-responsive-table-row', count: 2)
+ end
+
+ it 'user sees a disabled add cluster button ' do
+ expect(page).to have_selector('.js-add-cluster.disabled')
+ end
+
+ it 'user sees navigation tabs' do
+ expect(page.find('.js-active-tab').text).to include('Active')
+ expect(page.find('.js-active-tab .badge').text).to include('1')
+
+ expect(page.find('.js-inactive-tab').text).to include('Inactive')
+ expect(page.find('.js-inactive-tab .badge').text).to include('0')
+
+ expect(page.find('.js-all-tab').text).to include('All')
+ expect(page.find('.js-all-tab .badge').text).to include('1')
+ end
+
+ context 'update cluster' do
+ it 'user can update cluster' do
+ expect(page).to have_selector('.js-toggle-cluster-list')
+ end
+
+ context 'with sucessfull request' do
+ it 'user sees updated cluster' do
+ expect do
+ page.find('.js-toggle-cluster-list').click
+ wait_for_requests
+ end.to change { cluster.enabled }
+
+ expect(page).not_to have_selector('.is-checked')
+ end
+ end
+
+ context 'with failed request' do
+ it 'user sees not update cluster and error message' do
+ # Cluster was disabled in the last test
+ page.find('.js-toggle-cluster-list').click
+
+ Clusters::Cluster.last.provider.make_errored!('Something wrong!')
+
+ expect(page).not_to have_selector('.js-toggle-cluster-list.is-checked')
+ end
+ end
+ end
+
+ context 'when user clicks on a cluster' do
+ before do
+ click_link cluster.name
+ end
+
+ it 'user sees an cluster details page' do
+ expect(page).to have_button('Save')
+ expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
+
+ # Application Installation buttons
+ page.within('.js-cluster-application-row-helm') do
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ end
+ end
+
+ context 'when user installs application: Helm Tiller' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+
+ page.within('.js-cluster-application-row-helm') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-helm') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_helm.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_helm.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Helm Tiller was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user installs application: Ingress' do
+ before do
+ allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+ # Helm Tiller needs to be installed before you can install Ingress
+ create(:cluster_applications_helm, :installed, cluster: cluster)
+
+ visit project_cluster_path(project, cluster)
+
+ page.within('.js-cluster-application-row-ingress') do
+ page.find(:css, '.js-cluster-application-install-button').click
+ end
+ end
+
+ it 'user sees status transition' do
+ page.within('.js-cluster-application-row-ingress') do
+ # FE sends request and gets the response, then the buttons is "Install"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+
+ Clusters::Cluster.last.application_ingress.make_installing!
+
+ # FE starts polling and update the buttons to "Installing"
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+
+ Clusters::Cluster.last.application_ingress.make_installed!
+
+ expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
+ expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ end
+
+ expect(page).to have_content('Ingress was successfully installed on your cluster')
+ end
+ end
+
+ context 'when user disables the cluster' do
+ before do
+ page.find(:css, '.js-toggle-cluster').click
+ click_button 'Save'
+ end
+
+ it 'user sees the successful message' do
+ expect(page).to have_content('Cluster was successfully updated.')
+ end
+ end
+
+ context 'when user destroys the cluster' do
+ before do
+ page.accept_confirm do
+ click_link 'Remove integration'
+ end
+ end
+
+ it 'user sees creation form with the succeccful message' do
+ expect(page).to have_content('Cluster integration was successfully removed.')
+ expect(page).to have_link('Add cluster')
+ end
+ end
+ end
+ end
+ end
+
+ context 'when user has not signed in Google' do
+>>>>>>> origin/list-multiple-clusters
before do
visit project_clusters_path(project)
+ click_link 'Add cluster'
click_link 'Create on GKE'
end
@@ -22,4 +268,6 @@ feature 'Clusters', :js do
expect(page).to have_button('Create cluster')
end
end
+
+ context
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 951456763dc..033c45a60bf 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2)
+ expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2)
end
it "shows empty features project homepage" do
@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do
end
def toggle_feature_off(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.is-checked").click
end
def toggle_feature_on(feature_name)
- find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.is-checked)").click
end
end
diff --git a/spec/finders/clusters_finder_spec.rb b/spec/finders/clusters_finder_spec.rb
new file mode 100644
index 00000000000..c10efac2432
--- /dev/null
+++ b/spec/finders/clusters_finder_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe ClustersFinder do
+ let(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ describe '#execute' do
+ let(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
+ let(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
+
+ subject { described_class.new(project, user, scope).execute }
+
+ context 'when scope is all' do
+ let(:scope) { :all }
+
+ it { is_expected.to match_array([enabled_cluster, disabled_cluster]) }
+ end
+
+ context 'when scope is active' do
+ let(:scope) { :active }
+
+ it { is_expected.to match_array([enabled_cluster]) }
+ end
+
+ context 'when scope is inactive' do
+ let(:scope) { :inactive }
+
+ it { is_expected.to match_array([disabled_cluster]) }
+ end
+ end
+end
diff --git a/spec/helpers/clusters_helper_spec.rb b/spec/helpers/clusters_helper_spec.rb
new file mode 100644
index 00000000000..459fdfdc5a4
--- /dev/null
+++ b/spec/helpers/clusters_helper_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe ClustersHelper do
+ let(:cluster) { create(:cluster) }
+
+ describe '.can_toggle_cluster' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ subject { helper.can_toggle_cluster?(cluster) }
+
+ context 'when user can update' do
+ before do
+ allow(helper).to receive(:can?).with(any_args).and_return(true)
+ end
+
+ context 'when cluster is created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(true)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when cluster is not created' do
+ before do
+ allow(cluster).to receive(:created?).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when user can not update' do
+ before do
+ allow(helper).to receive(:can?).with(any_args).and_return(false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index 6d6e71cc215..f5be9ea0fb2 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -28,7 +28,7 @@ describe('Clusters', () => {
expect(
cluster.toggleButton.classList,
- ).not.toContain('checked');
+ ).not.toContain('is-checked');
expect(
cluster.toggleInput.getAttribute('value'),
diff --git a/spec/javascripts/clusters/clusters_index_spec.js b/spec/javascripts/clusters/clusters_index_spec.js
new file mode 100644
index 00000000000..0a8b63ed5b4
--- /dev/null
+++ b/spec/javascripts/clusters/clusters_index_spec.js
@@ -0,0 +1,58 @@
+import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
+import setClusterTableToggles from '~/clusters/clusters_index';
+import { setTimeout } from 'core-js/library/web/timers';
+
+describe('Clusters table', () => {
+ preloadFixtures('clusters/index_cluster.html.raw');
+ let mock;
+
+ beforeEach(() => {
+ loadFixtures('clusters/index_cluster.html.raw');
+ mock = new MockAdapter(axios);
+ setClusterTableToggles();
+ });
+
+ describe('update cluster', () => {
+ it('renders loading state while request is made', () => {
+ const button = document.querySelector('.js-toggle-cluster-list');
+
+ button.click();
+
+ expect(button.classList).toContain('is-loading');
+ expect(button.getAttribute('disabled')).toEqual('true');
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows updated state after sucessfull request', (done) => {
+ mock.onPut().reply(200, {}, {});
+ const button = document.querySelector('.js-toggle-cluster-list');
+ button.click();
+
+ expect(button.classList).toContain('is-loading');
+
+ setTimeout(() => {
+ expect(button.classList).not.toContain('is-loading');
+ expect(button.classList).not.toContain('is-checked');
+ done();
+ }, 0);
+ });
+
+ it('shows inital state after failed request', (done) => {
+ mock.onPut().reply(500, {}, {});
+ const button = document.querySelector('.js-toggle-cluster-list');
+
+ button.click();
+ expect(button.classList).toContain('is-loading');
+
+ setTimeout(() => {
+ expect(button.classList).not.toContain('is-loading');
+ expect(button.classList).toContain('is-checked');
+ done();
+ }, 0);
+ });
+ });
+});
diff --git a/spec/javascripts/fixtures/clusters.rb b/spec/javascripts/fixtures/clusters.rb
index 8e74c4f859c..d26ea3febe8 100644
--- a/spec/javascripts/fixtures/clusters.rb
+++ b/spec/javascripts/fixtures/clusters.rb
@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success
store_frontend_fixture(response, example.description)
end
+
+ context 'rendering non-empty state' do
+ before do
+ cluster
+ end
+
+ it 'clusters/index_cluster.html.raw' do |example|
+ get :index,
+ namespace_id: namespace,
+ project_id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+ end
end
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
new file mode 100644
index 00000000000..447d74d4e08
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -0,0 +1,91 @@
+import Vue from 'vue';
+import toggleButton from '~/vue_shared/components/toggle_button.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Toggle Button', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(toggleButton);
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('render output', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ name: 'foo',
+ });
+ });
+
+ it('renders input with provided name', () => {
+ expect(vm.$el.querySelector('input').getAttribute('name')).toEqual('foo');
+ });
+
+ it('renders input with provided value', () => {
+ expect(vm.$el.querySelector('input').getAttribute('value')).toEqual('true');
+ });
+
+ it('renders Enabled and Disabled text data attributes', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-enabled-text')).toEqual('Enabled');
+ expect(vm.$el.querySelector('button').getAttribute('data-disabled-text')).toEqual('Disabled');
+ });
+ });
+
+ describe('is-checked', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ });
+
+ spyOn(vm, '$emit');
+ });
+
+ it('renders is checked class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-checked')).toEqual(true);
+ });
+
+ it('emits change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('change', false);
+ });
+ });
+
+ describe('is-disabled', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ disabledInput: true,
+ });
+ spyOn(vm, '$emit');
+ });
+
+ it('renders disabled button', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-disabled')).toEqual(true);
+ });
+
+ it('does not emit change event when clicked', () => {
+ vm.$el.querySelector('button').click();
+
+ expect(vm.$emit).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('is-loading', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ value: true,
+ isLoading: true,
+ });
+ });
+
+ it('renders loading class', () => {
+ expect(vm.$el.querySelector('button').classList.contains('is-loading')).toEqual(true);
+ });
+ });
+});
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 7f43e747000..2683d21ddbe 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -198,4 +198,26 @@ describe Clusters::Cluster do
end
end
end
+
+ describe '#created?' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { cluster.created? }
+
+ context 'when status_name is :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:created)
+ end
+
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when status_name is not :created' do
+ before do
+ allow(cluster).to receive_message_chain(:provider, :status_name).and_return(:creating)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index 5b6edb73beb..d758bf6beb2 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -7,46 +7,69 @@ describe Clusters::CreateService do
let(:result) { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do
- context 'when correct params' do
- let(:params) do
- {
- name: 'test-cluster',
- provider_type: :gcp,
- provider_gcp_attributes: {
- gcp_project_id: 'gcp-project',
- zone: 'us-central1-a',
- num_nodes: 1,
- machine_type: 'machine_type-a'
+ context 'when project has no clusters' do
+ context 'when correct params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
}
- }
- end
+ end
- it 'creates a cluster object and performs a worker' do
- expect(ClusterProvisionWorker).to receive(:perform_async)
+ it 'creates a cluster object and performs a worker' do
+ expect(ClusterProvisionWorker).to receive(:perform_async)
- expect { result }
- .to change { Clusters::Cluster.count }.by(1)
- .and change { Clusters::Providers::Gcp.count }.by(1)
+ expect { result }
+ .to change { Clusters::Cluster.count }.by(1)
+ .and change { Clusters::Providers::Gcp.count }.by(1)
- expect(result.name).to eq('test-cluster')
- expect(result.user).to eq(user)
- expect(result.project).to eq(project)
- expect(result.provider.gcp_project_id).to eq('gcp-project')
- expect(result.provider.zone).to eq('us-central1-a')
- expect(result.provider.num_nodes).to eq(1)
- expect(result.provider.machine_type).to eq('machine_type-a')
- expect(result.provider.access_token).to eq(access_token)
- expect(result.platform).to be_nil
+ expect(result.name).to eq('test-cluster')
+ expect(result.user).to eq(user)
+ expect(result.project).to eq(project)
+ expect(result.provider.gcp_project_id).to eq('gcp-project')
+ expect(result.provider.zone).to eq('us-central1-a')
+ expect(result.provider.num_nodes).to eq(1)
+ expect(result.provider.machine_type).to eq('machine_type-a')
+ expect(result.provider.access_token).to eq(access_token)
+ expect(result.platform).to be_nil
+ end
+ end
+
+ context 'when invalid params' do
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: '!!!!!!!',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a'
+ }
+ }
+ end
+
+ it 'returns an error' do
+ expect(ClusterProvisionWorker).not_to receive(:perform_async)
+ expect { result }.to change { Clusters::Cluster.count }.by(0)
+ expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ end
end
end
- context 'when invalid params' do
+ context 'when project has a cluster' do
let(:params) do
{
name: 'test-cluster',
provider_type: :gcp,
provider_gcp_attributes: {
- gcp_project_id: '!!!!!!!',
+ gcp_project_id: 'gcp-project',
zone: 'us-central1-a',
num_nodes: 1,
machine_type: 'machine_type-a'
@@ -54,10 +77,13 @@ describe Clusters::CreateService do
}
end
- it 'returns an error' do
+ before do
+ Clusters::Cluster.create(params.merge(user: user, projects: [project]))
+ end
+
+ it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
- expect { result }.to change { Clusters::Cluster.count }.by(0)
- expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present
+ expect { result }.to raise_error(Exception).and change { Clusters::Cluster.count }.by(0)
end
end
end