Commit 29c243b0 authored by Grzegorz Bizon's avatar Grzegorz Bizon

Merge branch 'list-multiple-clusters' into 'master'

List multiple clusters

See merge request gitlab-org/gitlab-ce!15403
parents 67a94b2f 0ef16b27
...@@ -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 loading and disables button
* - 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 endpoint = toggleButton.getAttribute('data-endpoint');
toggleValue(toggleButton);
toggleLoadingButton(toggleButton);
const value = toggleButton.classList.contains('is-checked');
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);
}
} }
...@@ -562,7 +562,15 @@ import ProjectVariables from './project_variables'; ...@@ -562,7 +562,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,17 @@ ...@@ -14,3 +14,17 @@
} }
@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;
}
.top-area .nav-controls > .btn.btn-add-cluster {
margin-right: 0;
}
}
...@@ -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,11 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -8,11 +8,11 @@ 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.page(params[:page])
else @active_count = ClustersFinder.new(project, current_user, :active).execute.count
redirect_to new_project_cluster_path(project) @inactive_count = ClustersFinder.new(project, current_user, :inactive).execute.count
end @all_count = @active_count + @inactive_count
end end
def new def new
...@@ -39,10 +39,20 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -39,10 +39,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, 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
...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController ...@@ -63,6 +73,19 @@ class Projects::ClustersController < Projects::ApplicationController
.present(current_user: current_user) .present(current_user: current_user)
end 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 def update_params
if cluster.managed? if cluster.managed?
params.require(:cluster).permit( params.require(:cluster).permit(
......
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
...@@ -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,
......
...@@ -189,7 +189,6 @@ class Project < ActiveRecord::Base ...@@ -189,7 +189,6 @@ class Project < ActiveRecord::Base
has_one :statistics, class_name: 'ProjectStatistics' has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project' has_one :cluster_project, class_name: 'Clusters::Project'
has_one :cluster, through: :cluster_project, class_name: 'Clusters::Cluster'
has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster' has_many :clusters, through: :cluster_project, class_name: 'Clusters::Cluster'
# Container repositories need to remove data from the container registry, # Container repositories need to remove data from the container registry,
......
...@@ -5,5 +5,9 @@ module Clusters ...@@ -5,5 +5,9 @@ module Clusters
def gke_cluster_url def gke_cluster_url
"https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp? "https://console.cloud.google.com/kubernetes/clusters/details/#{provider.zone}/#{name}" if gcp?
end end
def can_toggle_cluster?
can?(current_user, :update_cluster, cluster) && created?
end
end end
end end
...@@ -5,6 +5,8 @@ module Clusters ...@@ -5,6 +5,8 @@ module Clusters
def execute(access_token = nil) def execute(access_token = nil)
@access_token = access_token @access_token = access_token
raise ArgumentError.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?
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&.actual_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 !cluster.can_toggle_cluster?}",
"aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !cluster.can_toggle_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'
...@@ -5,12 +5,11 @@ ...@@ -5,12 +5,11 @@
= field.hidden_field :enabled, { class: 'js-toggle-input'} = field.hidden_field :enabled, { class: 'js-toggle-input'}
%button{ type: 'button', %button{ type: 'button',
class: "js-toggle-cluster project-feature-toggle #{'checked' unless !@cluster.enabled?} #{'disabled' unless can?(current_user, :update_cluster, @cluster)}", class: "js-toggle-cluster project-feature-toggle #{'is-checked' unless !@cluster.enabled?} #{'is-disabled' unless can?(current_user, :update_cluster, @cluster)}",
'aria-label': s_('ClusterIntegration|Toggle Cluster'), "aria-label": s_("ClusterIntegration|Toggle Cluster"),
disabled: !can?(current_user, :update_cluster, @cluster), disabled: !can?(current_user, :update_cluster, @cluster),
data: { 'enabled-text': 'Enabled', 'disabled-text': 'Disabled' } } data: { "enabled-text": s_("ClusterIntegration|Active"), "disabled-text": s_("ClusterIntegration|Inactive"), } }
- if can?(current_user, :update_cluster, @cluster) - if can?(current_user, :update_cluster, @cluster)
.form-group .form-group
= field.submit _('Save'), class: 'btn btn-success' = field.submit _('Save'), 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
- 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.present(current_user: current_user)
= 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?
...@@ -28,7 +29,6 @@ ...@@ -28,7 +29,6 @@
%button.btn.js-settings-toggle %button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand' = expanded ? 'Collapse' : 'Expand'
%p= s_('ClusterIntegration|See and edit the details for your cluster') %p= s_('ClusterIntegration|See and edit the details for your cluster')
.settings-content .settings-content
- if @cluster.managed? - if @cluster.managed?
= render 'projects/clusters/gcp/show' = render 'projects/clusters/gcp/show'
......
...@@ -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-05 20:31+0100\n"
"PO-Revision-Date: 2017-10-22 16:40+0300\n" "PO-Revision-Date: 2017-12-05 20:31+0100\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 ""
...@@ -53,9 +58,18 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts ...@@ -53,9 +58,18 @@ msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts
msgstr[0] "" msgstr[0] ""
msgstr[1] "" msgstr[1] ""
msgid "%{text} is available"
msgstr ""
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] ""
...@@ -100,9 +114,6 @@ msgstr "" ...@@ -100,9 +114,6 @@ msgstr ""
msgid "Add License" msgid "Add License"
msgstr "" msgstr ""
msgid "Add an SSH key to your profile to pull or push via SSH."
msgstr ""
msgid "Add new directory" msgid "Add new directory"
msgstr "" msgstr ""
...@@ -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 ""
...@@ -124,6 +141,12 @@ msgstr "" ...@@ -124,6 +141,12 @@ msgstr ""
msgid "Applications" msgid "Applications"
msgstr "" msgstr ""
msgid "Apr"
msgstr ""
msgid "April"
msgstr ""
msgid "Archived project! Repository is read-only" msgid "Archived project! Repository is read-only"
msgstr "" msgstr ""
...@@ -151,6 +174,12 @@ msgstr "" ...@@ -151,6 +174,12 @@ msgstr ""
msgid "Attach a file by drag &amp; drop or %{upload_link}" msgid "Attach a file by drag &amp; drop or %{upload_link}"
msgstr "" msgstr ""
msgid "Aug"
msgstr ""
msgid "August"
msgstr ""
msgid "Authentication Log" msgid "Authentication Log"
msgstr "" msgstr ""
...@@ -184,6 +213,9 @@ msgstr "" ...@@ -184,6 +213,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] ""
...@@ -195,6 +227,12 @@ msgstr "" ...@@ -195,6 +227,12 @@ msgstr ""
msgid "Branch has changed" msgid "Branch has changed"
msgstr "" msgstr ""
msgid "Branch is already taken"
msgstr ""
msgid "Branch name"
msgstr ""
msgid "BranchSwitcherPlaceholder|Search branches" msgid "BranchSwitcherPlaceholder|Search branches"
msgstr "" msgstr ""
...@@ -330,6 +368,12 @@ msgstr "" ...@@ -330,6 +368,12 @@ msgstr ""
msgid "Chat" msgid "Chat"
msgstr "" msgstr ""
msgid "Checking %{text} availability…"
msgstr ""
msgid "Checking branch availability..."
msgstr ""
msgid "Cherry-pick this commit" msgid "Cherry-pick this commit"
msgstr "" msgstr ""
...@@ -399,7 +443,40 @@ msgstr "" ...@@ -399,7 +443,40 @@ 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|API URL"
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|CA Certificate"
msgstr ""
msgid "ClusterIntegration|Certificate Authority bundle (PEM format)"
msgstr ""
msgid "ClusterIntegration|Choose how to set up cluster integration"
msgstr ""
msgid "ClusterIntegration|Cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|Cluster details" msgid "ClusterIntegration|Cluster details"
...@@ -423,21 +500,54 @@ msgstr "" ...@@ -423,21 +500,54 @@ 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. Refresh the page to see cluster's details"
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 API URL"
msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
msgid "ClusterIntegration|Copy Token"
msgstr "" msgstr ""
msgid "ClusterIntegration|Copy cluster name" msgid "ClusterIntegration|Copy cluster name"
msgstr "" msgstr ""
msgid "ClusterIntegration|Create a new cluster on Google 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 cluster on Google Container Engine"
msgstr ""
msgid "ClusterIntegration|Create on GKE"
msgstr "" 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|Enter the details for your 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,27 +557,75 @@ msgstr "" ...@@ -447,27 +557,75 @@ 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 ""
msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters" msgid "ClusterIntegration|Make sure your account %{link_to_requirements} to create clusters"
msgstr "" msgstr ""
msgid "ClusterIntegration|Manage Cluster integration on your GitLab project" msgid "ClusterIntegration|Manage cluster integration on your GitLab project"
msgstr "" 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 enter access information for your cluster. If you need help, you can read our %{link_to_help_page} on clusters"
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|Problem setting up the cluster"
msgstr ""
msgid "ClusterIntegration|Problem setting up the clusters list"
msgstr ""
msgid "ClusterIntegration|Project ID"
msgstr ""
msgid "ClusterIntegration|Project namespace"
msgstr ""
msgid "ClusterIntegration|Project namespace (optional, unique)" msgid "ClusterIntegration|Project namespace (optional, unique)"
msgstr "" msgstr ""
...@@ -483,6 +641,12 @@ msgstr "" ...@@ -483,6 +641,12 @@ 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|Save changes"
msgstr ""
msgid "ClusterIntegration|See and edit the details for your cluster" msgid "ClusterIntegration|See and edit the details for your cluster"
msgstr "" msgstr ""
...@@ -495,15 +659,33 @@ msgstr "" ...@@ -495,15 +659,33 @@ msgstr ""
msgid "ClusterIntegration|See zones" msgid "ClusterIntegration|See zones"
msgstr "" msgstr ""
msgid "ClusterIntegration|Service token"
msgstr ""
msgid "ClusterIntegration|Show"
msgstr ""
msgid "ClusterIntegration|Something went wrong on our end." msgid "ClusterIntegration|Something went wrong on our end."
msgstr "" 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 ""
msgid "ClusterIntegration|Token"
msgstr ""
msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way." msgid "ClusterIntegration|With a cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr "" msgstr ""
...@@ -519,9 +701,15 @@ msgstr "" ...@@ -519,9 +701,15 @@ msgstr ""
msgid "ClusterIntegration|cluster" msgid "ClusterIntegration|cluster"
msgstr "" msgstr ""
msgid "ClusterIntegration|documentation"
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 ""
...@@ -617,6 +805,15 @@ msgstr "" ...@@ -617,6 +805,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 +832,21 @@ msgstr "" ...@@ -635,12 +832,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 ""
...@@ -698,6 +904,12 @@ msgstr "" ...@@ -698,6 +904,12 @@ msgstr ""
msgid "DashboardProjects|Personal" msgid "DashboardProjects|Personal"
msgstr "" msgstr ""
msgid "Dec"
msgstr ""
msgid "December"
msgstr ""
msgid "Define a custom pattern with cron syntax" msgid "Define a custom pattern with cron syntax"
msgstr "" msgstr ""
...@@ -766,6 +978,60 @@ msgstr "" ...@@ -766,6 +978,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 +1071,15 @@ msgstr "" ...@@ -805,6 +1071,15 @@ msgstr ""
msgid "Failed to remove the pipeline schedule" msgid "Failed to remove the pipeline schedule"
msgstr "" msgstr ""
msgid "Feb"
msgstr ""
msgid "February"
msgstr ""
msgid "File name"
msgstr ""
msgid "Files" msgid "Files"
msgstr "" msgstr ""
...@@ -981,6 +1256,24 @@ msgstr "" ...@@ -981,6 +1256,24 @@ msgstr ""
msgid "Issues" msgid "Issues"
msgstr "" msgstr ""
msgid "Jan"
msgstr ""
msgid "January"
msgstr ""
msgid "Jul"
msgstr ""
msgid "July"
msgstr ""
msgid "Jun"
msgstr ""
msgid "June"
msgstr ""
msgid "LFSStatus|Disabled" msgid "LFSStatus|Disabled"
msgstr "" msgstr ""
...@@ -1048,9 +1341,18 @@ msgstr "" ...@@ -1048,9 +1341,18 @@ msgstr ""
msgid "Login" msgid "Login"
msgstr "" msgstr ""
msgid "Mar"
msgstr ""
msgid "March"
msgstr ""
msgid "Maximum git storage failures" msgid "Maximum git storage failures"
msgstr "" msgstr ""
msgid "May"
msgstr ""
msgid "Median" msgid "Median"
msgstr "" msgstr ""
...@@ -1092,6 +1394,9 @@ msgstr "" ...@@ -1092,6 +1394,9 @@ msgstr ""
msgid "New branch" msgid "New branch"
msgstr "" msgstr ""
msgid "New branch unavailable"
msgstr ""
msgid "New directory" msgid "New directory"
msgstr "" msgstr ""
...@@ -1131,6 +1436,12 @@ msgstr "" ...@@ -1131,6 +1436,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 +1505,24 @@ msgstr "" ...@@ -1194,6 +1505,24 @@ msgstr ""
msgid "Notifications" msgid "Notifications"
msgstr "" msgstr ""
msgid "Nov"
msgstr ""
msgid "November"
msgstr ""
msgid "Number of access attempts"
msgstr ""
msgid "Number of failures before backing off"
msgstr ""
msgid "Oct"
msgstr ""
msgid "October"
msgstr ""
msgid "OfSearchInADropdown|Filter" msgid "OfSearchInADropdown|Filter"
msgstr "" msgstr ""
...@@ -1431,6 +1760,12 @@ msgstr "" ...@@ -1431,6 +1760,12 @@ msgstr ""
msgid "ProjectNetworkGraph|Graph" msgid "ProjectNetworkGraph|Graph"
msgstr "" msgstr ""
msgid "ProjectSettings|Immediately run a pipeline on the default branch"
msgstr ""
msgid "ProjectSettings|Problem setting up the CI/CD settings JavaScript"
msgstr ""
msgid "Projects" msgid "Projects"
msgstr "" msgstr ""
...@@ -1455,6 +1790,39 @@ msgstr "" ...@@ -1455,6 +1790,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 ""
...@@ -1563,6 +1931,12 @@ msgstr "" ...@@ -1563,6 +1931,12 @@ msgstr ""
msgid "Select target branch" msgid "Select target branch"
msgstr "" msgstr ""
msgid "Sep"
msgstr ""
msgid "September"
msgstr ""
msgid "Service Templates" msgid "Service Templates"
msgstr "" msgstr ""
...@@ -1601,7 +1975,7 @@ msgstr "" ...@@ -1601,7 +1975,7 @@ msgstr ""
msgid "Something went wrong on our end." msgid "Something went wrong on our end."
msgstr "" msgstr ""
msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName(this.issuableType)}" msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
msgstr "" msgstr ""
msgid "Something went wrong while fetching the projects." msgid "Something went wrong while fetching the projects."
...@@ -1700,9 +2074,15 @@ msgstr "" ...@@ -1700,9 +2074,15 @@ msgstr ""
msgid "SortOptions|Start soon" msgid "SortOptions|Start soon"
msgstr "" msgstr ""
msgid "Source"
msgstr ""
msgid "Source code" msgid "Source code"
msgstr "" msgstr ""
msgid "Source is not available"
msgstr ""
msgid "Spam Logs" msgid "Spam Logs"
msgstr "" msgstr ""
...@@ -1721,9 +2101,15 @@ msgstr "" ...@@ -1721,9 +2101,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 +2124,84 @@ msgstr[1] "" ...@@ -1738,12 +2124,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 +2214,12 @@ msgstr "" ...@@ -1756,6 +2214,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 +2440,9 @@ msgstr "" ...@@ -1976,6 +2440,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 +2455,9 @@ msgstr "" ...@@ -1988,6 +2455,9 @@ msgstr ""
msgid "Unstar" msgid "Unstar"
msgstr "" msgstr ""
msgid "Unsubscribe"
msgstr ""
msgid "Upload New File" msgid "Upload New File"
msgstr "" msgstr ""
...@@ -2189,6 +2659,9 @@ msgstr "" ...@@ -2189,6 +2659,9 @@ msgstr ""
msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile" msgid "You won't be able to pull or push project code via SSH until you %{add_ssh_key_link} to your profile"
msgstr "" msgstr ""
msgid "You won't be able to pull or push project code via SSH until you add an SSH key to your profile"
msgstr ""
msgid "Your comment will not be visible to the public." msgid "Your comment will not be visible to the public."
msgstr "" msgstr ""
...@@ -2201,6 +2674,9 @@ msgstr "" ...@@ -2201,6 +2674,9 @@ msgstr ""
msgid "Your projects" msgid "Your projects"
msgstr "" msgstr ""
msgid "branch name"
msgstr ""
msgid "day" msgid "day"
msgid_plural "days" msgid_plural "days"
msgstr[0] "" msgstr[0] ""
...@@ -2223,5 +2699,8 @@ msgstr "" ...@@ -2223,5 +2699,8 @@ msgstr ""
msgid "personal access token" msgid "personal access token"
msgstr "" msgstr ""
msgid "source"
msgstr ""
msgid "username" msgid "username"
msgstr "" msgstr ""
...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do ...@@ -143,9 +143,9 @@ describe Projects::Clusters::GcpController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Providers::Gcp.count } .and change { Clusters::Providers::Gcp.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.cluster).to be_gcp expect(project.clusters.first).to be_gcp
expect(project.cluster).to be_kubernetes expect(project.clusters.first).to be_kubernetes
end end
end end
end end
......
...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do ...@@ -64,7 +64,9 @@ describe Projects::Clusters::UserController do
expect(ClusterProvisionWorker).to receive(:perform_async) expect(ClusterProvisionWorker).to receive(:perform_async)
expect { go }.to change { Clusters::Cluster.count } expect { go }.to change { Clusters::Cluster.count }
.and change { Clusters::Platforms::Kubernetes.count } .and change { Clusters::Platforms::Kubernetes.count }
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, project.clusters.first))
expect(project.clusters.first).to be_user
expect(project.clusters.first).to be_kubernetes
end end
end end
end end
......
...@@ -15,14 +15,72 @@ describe Projects::ClustersController do ...@@ -15,14 +15,72 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when project has a cluster' do context 'when project has one or more clusters' do
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } let(:project) { create(:project) }
let!(:enabled_cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
let!(:disabled_cluster) { create(:cluster, :disabled, :provided_by_gcp, projects: [project]) }
it 'lists available clusters' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index)
expect(assigns(:clusters)).to match_array([enabled_cluster, disabled_cluster])
end
it 'assigns counters to correct values' do
go
expect(assigns(:active_count)).to eq(1)
expect(assigns(:inactive_count)).to eq(1)
end
context 'when page is specified' do
let(:last_page) { project.clusters.page.total_pages }
before do
allow(Clusters::Cluster).to receive(:paginates_per).and_return(1)
create_list(:cluster, 2, :provided_by_gcp, projects: [project])
get :index, namespace_id: project.namespace, project_id: project, page: last_page
end
it 'redirects to the page' do
expect(response).to have_gitlab_http_status(:ok)
expect(assigns(:clusters).current_page).to eq(last_page)
end
end
context 'when only enabled clusters are requested' do
it 'returns only enabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'active'
expect(assigns(:clusters)).to all(have_attributes(enabled: true))
end
end
it { expect(go).to redirect_to(project_cluster_path(project, project.cluster)) } context 'when only disabled clusters are requested' do
it 'returns only disabled clusters' do
get :index, namespace_id: project.namespace, project_id: project, scope: 'inactive'
expect(assigns(:clusters)).to all(have_attributes(enabled: false))
end
end
end end
context 'when project does not have a cluster' do context 'when project does not have a cluster' do
it { expect(go).to redirect_to(new_project_cluster_path(project)) } let(:project) { create(:project) }
it 'returns an empty state page' do
go
expect(response).to have_gitlab_http_status(:ok)
expect(response).to render_template(:index, partial: :empty_state)
expect(assigns(:clusters)).to eq([])
end
it 'assigns counters to zero' do
go
expect(assigns(:active_count)).to eq(0)
expect(assigns(:inactive_count)).to eq(0)
end
end end
end end
...@@ -146,7 +204,7 @@ describe Projects::ClustersController do ...@@ -146,7 +204,7 @@ describe Projects::ClustersController do
go go
cluster.reload cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.') expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
end end
...@@ -180,7 +238,55 @@ describe Projects::ClustersController do ...@@ -180,7 +238,55 @@ describe Projects::ClustersController do
sign_in(user) sign_in(user)
end end
context 'when format is json' do
context 'when changing parameters' do context 'when changing parameters' do
context 'when valid parameters are used' 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_json
cluster.reload
expect(response).to have_http_status(:no_content)
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
end
context 'when invalid parameters are used' do
let(:params) do
{
cluster: {
enabled: false,
platform_kubernetes_attributes: {
namespace: 'my invalid namespace #@'
}
}
}
end
it "rejects changes" do
go_json
expect(response).to have_http_status(:bad_request)
end
end
end
end
context 'when format is html' do
context 'when update enabled' do
let(:params) do let(:params) do
{ {
cluster: { cluster: {
...@@ -197,7 +303,7 @@ describe Projects::ClustersController do ...@@ -197,7 +303,7 @@ describe Projects::ClustersController do
go go
cluster.reload cluster.reload
expect(response).to redirect_to(project_cluster_path(project, project.cluster)) expect(response).to redirect_to(project_cluster_path(project, cluster))
expect(flash[:notice]).to eq('Cluster was successfully updated.') expect(flash[:notice]).to eq('Cluster was successfully updated.')
expect(cluster.enabled).to be_falsey expect(cluster.enabled).to be_falsey
expect(cluster.name).to eq('my-new-cluster-name') expect(cluster.name).to eq('my-new-cluster-name')
...@@ -205,6 +311,7 @@ describe Projects::ClustersController do ...@@ -205,6 +311,7 @@ describe Projects::ClustersController do
end end
end end
end end
end
describe 'security' do describe 'security' do
set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) } set(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
...@@ -228,6 +335,13 @@ describe Projects::ClustersController do ...@@ -228,6 +335,13 @@ describe Projects::ClustersController do
project_id: project, project_id: project,
id: cluster) id: cluster)
end end
def go_json
put :update, params.merge(namespace_id: project.namespace,
project_id: project,
id: cluster,
format: :json)
end
end end
describe 'DELETE destroy' do describe 'DELETE destroy' do
......
...@@ -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
...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do ...@@ -24,6 +24,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do ...@@ -116,7 +117,7 @@ feature 'Gcp Cluster', :js do
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.') expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Create on GKE') expect(page).to have_link('Add cluster')
end end
end end
end end
...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do ...@@ -126,6 +127,7 @@ feature 'Gcp Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Create on GKE' click_link 'Create on GKE'
end end
......
...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do ...@@ -16,6 +16,7 @@ feature 'User Cluster', :js do
before do before do
visit project_clusters_path(project) visit project_clusters_path(project)
click_link 'Add cluster'
click_link 'Add an existing cluster' click_link 'Add an existing cluster'
end end
...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do ...@@ -94,7 +95,7 @@ feature 'User Cluster', :js do
it 'user sees creation form with the successful message' do it 'user sees creation form with the successful message' do
expect(page).to have_content('Cluster integration was successfully removed.') expect(page).to have_content('Cluster integration was successfully removed.')
expect(page).to have_link('Add an existing cluster') expect(page).to have_link('Add cluster')
end end
end end
end end
......
...@@ -14,12 +14,78 @@ feature 'Clusters', :js do ...@@ -14,12 +14,78 @@ feature 'Clusters', :js do
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
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 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
click_link 'Create on GKE' context 'inline update of cluster' do
it 'user can update cluster' do
expect(page).to have_selector('.js-toggle-cluster-list')
end end
it 'user sees a new page' do context 'with sucessfull request' do
expect(page).to have_button('Create cluster') it 'user sees updated cluster' do
expect do
page.find('.js-toggle-cluster-list').click
wait_for_requests
end.to change { cluster.reload.enabled }
expect(page).not_to have_selector('.is-checked')
expect(cluster.reload).not_to be_enabled
end
end
context 'with failed request' do
it 'user sees not update cluster and error message' do
expect_any_instance_of(Clusters::UpdateService).to receive(:execute).and_call_original
allow_any_instance_of(Clusters::Cluster).to receive(:valid?) { false }
page.find('.js-toggle-cluster-list').click
expect(page).to have_content('Something went wrong on our end.')
expect(page).to have_selector('.is-checked')
expect(cluster.reload).to be_enabled
end
end
end
context 'when user clicks on a cluster' do
before do
click_link cluster.name
end
it 'user sees a cluster details page' do
expect(page).to have_button('Save')
expect(page.find(:css, '.cluster-name').value).to eq(cluster.name)
end
end end
end end
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
...@@ -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);
});
});
});
...@@ -57,7 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do ...@@ -57,7 +57,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.platform_type).to eq('kubernetes') expect(cluster.platform_type).to eq('kubernetes')
expect(cluster.project).to eq(project) expect(cluster.project).to eq(project)
expect(project.cluster).to eq(cluster) expect(project.clusters).to include(cluster)
expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status) expect(cluster.provider_gcp.status).to eq(status)
...@@ -134,7 +134,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do ...@@ -134,7 +134,7 @@ describe MigrateGcpClustersToNewClustersArchitectures, :migration do
expect(cluster.platform_type).to eq('kubernetes') expect(cluster.platform_type).to eq('kubernetes')
expect(cluster.project).to eq(project) expect(cluster.project).to eq(project)
expect(project.cluster).to eq(cluster) expect(project.clusters).to include(cluster)
expect(cluster.provider_gcp.cluster).to eq(cluster) expect(cluster.provider_gcp.cluster).to eq(cluster)
expect(cluster.provider_gcp.status).to eq(status) expect(cluster.provider_gcp.status).to eq(status)
......
...@@ -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
...@@ -78,7 +78,7 @@ describe Project do ...@@ -78,7 +78,7 @@ describe Project do
it { is_expected.to have_many(:uploads).dependent(:destroy) } it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_many(:pipeline_schedules) } it { is_expected.to have_many(:pipeline_schedules) }
it { is_expected.to have_many(:members_and_requesters) } it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_one(:cluster) } it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') } it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
context 'after initialized' do context 'after initialized' do
......
...@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do ...@@ -31,4 +31,44 @@ describe Clusters::ClusterPresenter do
it { is_expected.to include(cluster.provider.zone) } it { is_expected.to include(cluster.provider.zone) }
it { is_expected.to include(cluster.name) } it { is_expected.to include(cluster.name) }
end end
describe '#can_toggle_cluster' do
let(:user) { create(:user) }
before do
allow(cluster).to receive(:current_user).and_return(user)
end
subject { described_class.new(cluster).can_toggle_cluster? }
context 'when user can update' do
before do
allow_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).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_any_instance_of(described_class).to receive(:can?).with(user, :update_cluster, cluster).and_return(false)
end
it { is_expected.to eq(false) }
end
end
end end
...@@ -4,10 +4,11 @@ describe Clusters::CreateService do ...@@ -4,10 +4,11 @@ describe Clusters::CreateService do
let(:access_token) { 'xxx' } let(:access_token) { 'xxx' }
let(:project) { create(:project) } let(:project) { create(:project) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:result) { described_class.new(project, user, params).execute(access_token) }
subject { described_class.new(project, user, params).execute(access_token) }
context 'when provider is gcp' do context 'when provider is gcp' do
context 'when correct params' do shared_context 'valid params' do
let(:params) do let(:params) do
{ {
name: 'test-cluster', name: 'test-cluster',
...@@ -20,27 +21,9 @@ describe Clusters::CreateService do ...@@ -20,27 +21,9 @@ describe Clusters::CreateService do
} }
} }
end end
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.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 end
context 'when invalid params' do shared_context 'invalid params' do
let(:params) do let(:params) do
{ {
name: 'test-cluster', name: 'test-cluster',
...@@ -53,11 +36,57 @@ describe Clusters::CreateService do ...@@ -53,11 +36,57 @@ describe Clusters::CreateService do
} }
} }
end end
end
shared_examples 'create cluster' do
it 'creates a cluster object and performs a worker' do
expect(ClusterProvisionWorker).to receive(:perform_async)
expect { subject }
.to change { Clusters::Cluster.count }.by(1)
.and change { Clusters::Providers::Gcp.count }.by(1)
expect(subject.name).to eq('test-cluster')
expect(subject.user).to eq(user)
expect(subject.project).to eq(project)
expect(subject.provider.gcp_project_id).to eq('gcp-project')
expect(subject.provider.zone).to eq('us-central1-a')
expect(subject.provider.num_nodes).to eq(1)
expect(subject.provider.machine_type).to eq('machine_type-a')
expect(subject.provider.access_token).to eq(access_token)
expect(subject.platform).to be_nil
end
end
shared_examples 'error' do
it 'returns an error' do it 'returns an error' do
expect(ClusterProvisionWorker).not_to receive(:perform_async) expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { result }.to change { Clusters::Cluster.count }.by(0) expect { subject }.to change { Clusters::Cluster.count }.by(0)
expect(result.errors[:"provider_gcp.gcp_project_id"]).to be_present expect(subject.errors[:"provider_gcp.gcp_project_id"]).to be_present
end
end
context 'when project has no clusters' do
context 'when correct params' do
include_context 'valid params'
include_examples 'create cluster'
end
context 'when invalid params' do
include_context 'invalid params'
include_examples 'error'
end
end
context 'when project has a cluster' do
include_context 'valid params'
let!(:cluster) { create(:cluster, :provided_by_gcp, projects: [project]) }
it 'does not create a cluster' do
expect(ClusterProvisionWorker).not_to receive(:perform_async)
expect { subject }.to raise_error(ArgumentError).and change { Clusters::Cluster.count }.by(0)
end 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