Commit f1cce0cb authored by Kamil Trzcinski's avatar Kamil Trzcinski

Merge remote-tracking branch 'origin/list-multiple-clusters' into cluster-page-with-list-clusters

parents 5ea53d2e 363c5746
...@@ -150,8 +150,8 @@ export default class Clusters { ...@@ -150,8 +150,8 @@ export default class Clusters {
} }
toggle() { toggle() {
this.toggleButton.classList.toggle('checked'); this.toggleButton.classList.toggle('is-checked');
this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('checked').toString()); this.toggleInput.setAttribute('value', this.toggleButton.classList.contains('is-checked').toString());
} }
showToken() { showToken() {
......
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.'));
});
}));
}
...@@ -17,4 +17,8 @@ export default class ClusterService { ...@@ -17,4 +17,8 @@ export default class ClusterService {
installApplication(appId) { installApplication(appId) {
return axios.post(this.appInstallEndpointMap[appId]); return axios.post(this.appInstallEndpointMap[appId]);
} }
static updateCluster(endpoint, data) {
return axios.put(endpoint, data);
}
} }
...@@ -558,7 +558,15 @@ import ProjectVariables from './project_variables'; ...@@ -558,7 +558,15 @@ import ProjectVariables from './project_variables';
import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle') import(/* webpackChunkName: "clusters" */ './clusters/clusters_bundle')
.then(cluster => new cluster.default()) // eslint-disable-line new-cap .then(cluster => new cluster.default()) // eslint-disable-line new-cap
.catch((err) => { .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; throw err;
}); });
break; break;
......
<script> <script>
import projectFeatureToggle from './project_feature_toggle.vue'; import projectFeatureToggle from '../../../vue_shared/components/toggle_button.vue';
export default { export default {
props: { props: {
......
<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>
<script> <script>
import projectFeatureSetting from './project_feature_setting.vue'; 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 projectSettingRow from './project_setting_row.vue';
import { visibilityOptions, visibilityLevelDescriptions } from '../constants'; import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
import { toggleHiddenClassBySelector } from '../external'; import { toggleHiddenClassBySelector } from '../external';
......
<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>
...@@ -44,6 +44,7 @@ ...@@ -44,6 +44,7 @@
@import "framework/tabs"; @import "framework/tabs";
@import "framework/timeline"; @import "framework/timeline";
@import "framework/tooltips"; @import "framework/tooltips";
@import "framework/toggle";
@import "framework/typography"; @import "framework/typography";
@import "framework/zen"; @import "framework/zen";
@import "framework/blank"; @import "framework/blank";
......
/**
* 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; }
}
}
...@@ -14,3 +14,13 @@ ...@@ -14,3 +14,13 @@
} }
@include new-style-dropdown('.clusters-dropdown '); @include new-style-dropdown('.clusters-dropdown ');
.clusters-container {
.nav-bar-right {
padding: $gl-padding-top $gl-padding;
}
.empty-state .svg-content img {
width: 145px;
}
}
...@@ -126,93 +126,6 @@ ...@@ -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, .project-home-panel,
.group-home-panel { .group-home-panel {
padding-top: 24px; padding-top: 24px;
......
...@@ -8,11 +8,12 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -8,11 +8,12 @@ class Projects::ClustersController < Projects::ApplicationController
STATUS_POLLING_INTERVAL = 10_000 STATUS_POLLING_INTERVAL = 10_000
def index def index
if project.cluster @scope = params[:scope] || 'all'
redirect_to project_cluster_path(project, project.cluster) clusters = ClustersFinder.new(project, current_user, @scope).execute
else @clusters = clusters.page(params[:page])
redirect_to new_project_cluster_path(project) @active_count = project.clusters.enabled.count
end @inactive_count = project.clusters.disabled.count
@all_count = @active_count + @inactive_count
end end
def new def new
...@@ -39,10 +40,20 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -39,10 +40,20 @@ class Projects::ClustersController < Projects::ApplicationController
.execute(cluster) .execute(cluster)
if cluster.valid? if cluster.valid?
respond_to do |format|
format.json do
head :no_content
end
format.html do
flash[:notice] = "Cluster was successfully updated." flash[:notice] = "Cluster was successfully updated."
redirect_to project_cluster_path(project, project.cluster) redirect_to project_cluster_path(project, project.cluster)
end
end
else else
render :show respond_to do |format|
format.json { head :bad_request }
format.html { render :show }
end
end end
end end
...@@ -59,7 +70,20 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -59,7 +70,20 @@ class Projects::ClustersController < Projects::ApplicationController
private private
def cluster 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 end
def update_params def update_params
......
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
module ClustersHelper
def can_toggle_cluster?(cluster)
can?(current_user, :update_cluster, cluster) && cluster.created?
end
end
...@@ -55,6 +55,10 @@ module Clusters ...@@ -55,6 +55,10 @@ module Clusters
end end
end end
def created?
status_name == :created
end
def applications def applications
[ [
application_helm || build_application_helm, application_helm || build_application_helm,
......
...@@ -5,6 +5,8 @@ module Clusters ...@@ -5,6 +5,8 @@ module Clusters
def execute(access_token) def execute(access_token)
@access_token = access_token @access_token = access_token
raise Exception.new('Instance does not support multiple clusters') unless can_create_cluster?
create_cluster.tap do |cluster| create_cluster.tap do |cluster|
ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted? ClusterProvisionWorker.perform_async(cluster.id) if cluster.persisted?
end end
...@@ -25,5 +27,9 @@ module Clusters ...@@ -25,5 +27,9 @@ module Clusters
@cluster_params = params.merge(user: current_user, projects: [project]) @cluster_params = params.merge(user: current_user, projects: [project])
end end
def can_create_cluster?
return project.clusters.empty?
end
end end
end end
...@@ -187,7 +187,7 @@ ...@@ -187,7 +187,7 @@
= nav_link(controller: [:clusters, :user, :gcp]) do = nav_link(controller: [:clusters, :user, :gcp]) do
= link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do = link_to project_clusters_path(@project), title: 'Cluster', class: 'shortcuts-cluster' do
%span %span
Cluster Clusters
- if @project.feature_available?(:builds, current_user) && !@project.empty_repo? - if @project.feature_available?(:builds, current_user) && !@project.empty_repo?
= nav_link(path: 'pipelines#charts') do = nav_link(path: 'pipelines#charts') do
......
.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")
.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'
.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")
- 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")
- @content_class = "limit-container-width" unless fluid_layout - @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") - page_title _("Cluster")
- expanded = Rails.env.test? - expanded = Rails.env.test?
......
...@@ -8,8 +8,8 @@ msgid "" ...@@ -8,8 +8,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: gitlab 1.0.0\n" "Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2017-10-22 16:40+0300\n" "POT-Creation-Date: 2017-12-02 15:22+0000\n"
"PO-Revision-Date: 2017-10-22 16:40+0300\n" "PO-Revision-Date: 2017-12-02 15:22+0000\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n" "Language: \n"
...@@ -36,6 +36,11 @@ msgstr[1] "" ...@@ -36,6 +36,11 @@ msgstr[1] ""
msgid "%{commit_author_link} committed %{commit_timeago}" msgid "%{commit_author_link} committed %{commit_timeago}"
msgstr "" 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" msgid "%{number_commits_behind} commits behind %{default_branch}, %{number_commits_ahead} commits ahead"
msgstr "" msgstr ""
...@@ -56,6 +61,12 @@ msgstr[1] "" ...@@ -56,6 +61,12 @@ msgstr[1] ""
msgid "(checkout the %{link} for information on how to install it)." msgid "(checkout the %{link} for information on how to install it)."
msgstr "" msgstr ""
msgid "+ %{moreCount} more"
msgstr ""
msgid "- show less"
msgstr ""
msgid "1 pipeline" msgid "1 pipeline"
msgid_plural "%d pipelines" msgid_plural "%d pipelines"
msgstr[0] "" msgstr[0] ""
...@@ -115,6 +126,12 @@ msgstr "" ...@@ -115,6 +126,12 @@ msgstr ""
msgid "All" msgid "All"
msgstr "" 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." msgid "An error occurred. Please try again."
msgstr "" msgstr ""
...@@ -184,6 +201,9 @@ msgstr "" ...@@ -184,6 +201,9 @@ msgstr ""
msgid "AutoDevOps|You can activate %{link_to_settings} for this project." msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
msgstr "" msgstr ""
msgid "Available"
msgstr ""
msgid "Branch" msgid "Branch"
msgid_plural "Branches" msgid_plural "Branches"
msgstr[0] "" msgstr[0] ""
...@@ -399,7 +419,34 @@ msgstr "" ...@@ -399,7 +419,34 @@ msgstr ""
msgid "Cluster" msgid "Cluster"
msgstr "" 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 "" msgstr ""
msgid "ClusterIntegration|Cluster details" msgid "ClusterIntegration|Cluster details"
...@@ -420,24 +467,48 @@ msgstr "" ...@@ -420,24 +467,48 @@ msgstr ""
msgid "ClusterIntegration|Cluster is being created on Google Container Engine..." msgid "ClusterIntegration|Cluster is being created on Google Container Engine..."
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster management"
msgstr ""
msgid "ClusterIntegration|Cluster name" msgid "ClusterIntegration|Cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine" msgid "ClusterIntegration|Cluster was successfully created on Google Container Engine"
msgstr "" 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" msgid "ClusterIntegration|Copy cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create a new cluster on Google Container Engine right from GitLab"
msgstr ""
msgid "ClusterIntegration|Create cluster" msgid "ClusterIntegration|Create cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create new cluster on Google Container Engine" msgid "ClusterIntegration|Create new cluster on Google Container Engine"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create on GKE"
msgstr ""
msgid "ClusterIntegration|Enable cluster integration" msgid "ClusterIntegration|Enable cluster integration"
msgstr "" 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" msgid "ClusterIntegration|Google Cloud Platform project ID"
msgstr "" msgstr ""
...@@ -447,9 +518,36 @@ msgstr "" ...@@ -447,9 +518,36 @@ msgstr ""
msgid "ClusterIntegration|Google Container Engine project" msgid "ClusterIntegration|Google Container Engine project"
msgstr "" 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}" msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Learn more about Clusters"
msgstr ""
msgid "ClusterIntegration|Machine type" msgid "ClusterIntegration|Machine type"
msgstr "" msgstr ""
...@@ -459,16 +557,31 @@ msgstr "" ...@@ -459,16 +557,31 @@ msgstr ""
msgid "ClusterIntegration|Manage Cluster integration on your GitLab project" msgid "ClusterIntegration|Manage Cluster integration on your GitLab project"
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage Kubernetes integration"
msgstr ""
msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}" msgid "ClusterIntegration|Manage your cluster by visiting %{link_gke}"
msgstr "" msgstr ""
msgid "ClusterIntegration|Multiple clusters are available in GitLab Entreprise Edition Premium and Ultimate"
msgstr ""
msgid "ClusterIntegration|Note:"
msgstr ""
msgid "ClusterIntegration|Number of nodes" msgid "ClusterIntegration|Number of nodes"
msgstr "" msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:" msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr "" 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 "" msgstr ""
msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration." msgid "ClusterIntegration|Read our %{link_to_help_page} on cluster integration."
...@@ -483,6 +596,9 @@ msgstr "" ...@@ -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." 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 "" msgstr ""
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster" msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "" msgstr ""
...@@ -501,6 +617,15 @@ msgstr "" ...@@ -501,6 +617,15 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine" msgid "ClusterIntegration|Something went wrong while creating your cluster on Google Container Engine"
msgstr "" 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" msgid "ClusterIntegration|Toggle Cluster"
msgstr "" msgstr ""
...@@ -522,6 +647,9 @@ msgstr "" ...@@ -522,6 +647,9 @@ msgstr ""
msgid "ClusterIntegration|help page" msgid "ClusterIntegration|help page"
msgstr "" msgstr ""
msgid "ClusterIntegration|installing applications"
msgstr ""
msgid "ClusterIntegration|meets the requirements" msgid "ClusterIntegration|meets the requirements"
msgstr "" msgstr ""
...@@ -536,6 +664,11 @@ msgid_plural "Commits" ...@@ -536,6 +664,11 @@ msgid_plural "Commits"
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "Commit %d file"
msgid_plural "Commit %d files"
msgstr[0] ""
msgstr[1] ""
msgid "Commit Message" msgid "Commit Message"
msgstr "" msgstr ""
...@@ -617,6 +750,15 @@ msgstr "" ...@@ -617,6 +750,15 @@ msgstr ""
msgid "Contributors" msgid "Contributors"
msgstr "" 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" msgid "Copy URL to clipboard"
msgstr "" msgstr ""
...@@ -635,12 +777,21 @@ msgstr "" ...@@ -635,12 +777,21 @@ msgstr ""
msgid "Create empty bare repository" msgid "Create empty bare repository"
msgstr "" msgstr ""
msgid "Create file"
msgstr ""
msgid "Create merge request" msgid "Create merge request"
msgstr "" msgstr ""
msgid "Create new branch" msgid "Create new branch"
msgstr "" msgstr ""
msgid "Create new directory"
msgstr ""
msgid "Create new file"
msgstr ""
msgid "Create new..." msgid "Create new..."
msgstr "" msgstr ""
...@@ -766,6 +917,60 @@ msgstr "" ...@@ -766,6 +917,60 @@ msgstr ""
msgid "Emails" msgid "Emails"
msgstr "" 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" msgid "EventFilterBy|Filter by all"
msgstr "" msgstr ""
...@@ -805,6 +1010,9 @@ msgstr "" ...@@ -805,6 +1010,9 @@ msgstr ""
msgid "Failed to remove the pipeline schedule" msgid "Failed to remove the pipeline schedule"
msgstr "" msgstr ""
msgid "File name"
msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
...@@ -1131,6 +1339,12 @@ msgstr "" ...@@ -1131,6 +1339,12 @@ msgstr ""
msgid "No schedules" msgid "No schedules"
msgstr "" msgstr ""
msgid "No time spent"
msgstr ""
msgid "None"
msgstr ""
msgid "Not available" msgid "Not available"
msgstr "" msgstr ""
...@@ -1194,6 +1408,12 @@ msgstr "" ...@@ -1194,6 +1408,12 @@ msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "" msgstr ""
...@@ -1455,6 +1675,39 @@ msgstr "" ...@@ -1455,6 +1675,39 @@ msgstr ""
msgid "ProjectsDropdown|This feature requires browser localStorage support" msgid "ProjectsDropdown|This feature requires browser localStorage support"
msgstr "" 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." msgid "Public - The group and any public projects can be viewed without any authentication."
msgstr "" msgstr ""
...@@ -1721,9 +1974,15 @@ msgstr "" ...@@ -1721,9 +1974,15 @@ msgstr ""
msgid "Start the Runner!" msgid "Start the Runner!"
msgstr "" msgstr ""
msgid "Stopped"
msgstr ""
msgid "Subgroups" msgid "Subgroups"
msgstr "" msgstr ""
msgid "Subscribe"
msgstr ""
msgid "Switch branch/tag" msgid "Switch branch/tag"
msgstr "" msgstr ""
...@@ -1738,12 +1997,84 @@ msgstr[1] "" ...@@ -1738,12 +1997,84 @@ msgstr[1] ""
msgid "Tags" msgid "Tags"
msgstr "" 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" msgid "Target Branch"
msgstr "" msgstr ""
msgid "Team" msgid "Team"
msgstr "" 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." 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 "" msgstr ""
...@@ -1756,6 +2087,12 @@ 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." 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 "" 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}." 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 "" msgstr ""
...@@ -1976,6 +2313,9 @@ msgstr "" ...@@ -1976,6 +2313,9 @@ msgstr ""
msgid "Total Time" msgid "Total Time"
msgstr "" msgstr ""
msgid "Total issue time spent"
msgstr ""
msgid "Total test time for all commits/merges" msgid "Total test time for all commits/merges"
msgstr "" msgstr ""
...@@ -1988,6 +2328,9 @@ msgstr "" ...@@ -1988,6 +2328,9 @@ msgstr ""
msgid "Unstar" msgid "Unstar"
msgstr "" msgstr ""
msgid "Unsubscribe"
msgstr ""
msgid "Upload New File" msgid "Upload New File"
msgstr "" msgstr ""
......
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
...@@ -28,5 +28,9 @@ FactoryGirl.define do ...@@ -28,5 +28,9 @@ FactoryGirl.define do
provider_type :gcp provider_type :gcp
provider_gcp factory: [:cluster_provider_gcp, :creating] provider_gcp factory: [:cluster_provider_gcp, :creating]
end end
trait :disabled do
enabled false
end
end end
end end
...@@ -11,10 +11,256 @@ feature 'Clusters', :js do ...@@ -11,10 +11,256 @@ feature 'Clusters', :js do
gitlab_sign_in(user) gitlab_sign_in(user)
end 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 context 'when user does not have a cluster and visits cluster index page' do
before do before do
visit project_clusters_path(project) 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' click_link 'Create on GKE'
end end
...@@ -22,4 +268,6 @@ feature 'Clusters', :js do ...@@ -22,4 +268,6 @@ feature 'Clusters', :js do
expect(page).to have_button('Create cluster') expect(page).to have_button('Create cluster')
end end
end end
context
end end
...@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do ...@@ -177,7 +177,7 @@ describe 'Edit Project Settings' do
click_button "Save changes" click_button "Save changes"
end 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 end
it "shows empty features project homepage" do it "shows empty features project homepage" do
...@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do ...@@ -272,10 +272,10 @@ describe 'Edit Project Settings' do
end end
def toggle_feature_off(feature_name) 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 end
def toggle_feature_on(feature_name) 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
end end
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
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
...@@ -28,7 +28,7 @@ describe('Clusters', () => { ...@@ -28,7 +28,7 @@ describe('Clusters', () => {
expect( expect(
cluster.toggleButton.classList, cluster.toggleButton.classList,
).not.toContain('checked'); ).not.toContain('is-checked');
expect( expect(
cluster.toggleInput.getAttribute('value'), cluster.toggleInput.getAttribute('value'),
......
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);
});
});
});
...@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle ...@@ -31,4 +31,19 @@ describe Projects::ClustersController, '(JavaScript fixtures)', type: :controlle
expect(response).to be_success expect(response).to be_success
store_frontend_fixture(response, example.description) store_frontend_fixture(response, example.description)
end 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 end
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);
});
});
});
...@@ -198,4 +198,26 @@ describe Clusters::Cluster do ...@@ -198,4 +198,26 @@ describe Clusters::Cluster do
end end
end 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 end
...@@ -7,6 +7,7 @@ describe Clusters::CreateService do ...@@ -7,6 +7,7 @@ describe Clusters::CreateService do
let(:result) { described_class.new(project, user, params).execute(access_token) } let(:result) { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do context 'when provider is gcp' do
context 'when project has no clusters' do
context 'when correct params' do context 'when correct params' do
let(:params) do let(:params) do
{ {
...@@ -61,4 +62,29 @@ describe Clusters::CreateService do ...@@ -61,4 +62,29 @@ describe Clusters::CreateService do
end end
end end
end end
context 'when project has a cluster' 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
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 raise_error(Exception).and change { Clusters::Cluster.count }.by(0)
end
end
end
end end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment