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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-07-18 21:08:47 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-07-18 21:08:47 +0300
commit128d4d89e98177996d1ff6e0b3d7a8a0c9b35929 (patch)
tree88b02d3bf972bac281d673e99f854303e0dd13ed
parentcc1066db64a2a283a3d229b9bbb67c01716ca871 (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.gitlab/ci/review-apps/main.gitlab-ci.yml2
-rw-r--r--app/assets/javascripts/google_cloud/components/app.vue63
-rw-r--r--app/assets/javascripts/google_cloud/components/errors/gcp_error.vue29
-rw-r--r--app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue26
-rw-r--r--app/assets/javascripts/google_cloud/components/google_cloud_menu.vue85
-rw-r--r--app/assets/javascripts/google_cloud/components/home.vue81
-rw-r--r--app/assets/javascripts/google_cloud/components/incubation_banner.vue28
-rw-r--r--app/assets/javascripts/google_cloud/configuration/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/configuration/panel.vue88
-rw-r--r--app/assets/javascripts/google_cloud/databases/cloudsql/create_instance_form.vue (renamed from app/assets/javascripts/google_cloud/components/cloudsql/create_instance_form.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue (renamed from app/assets/javascripts/google_cloud/components/cloudsql/instance_table.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/databases/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/databases/panel.vue38
-rw-r--r--app/assets/javascripts/google_cloud/databases/service_table.vue (renamed from app/assets/javascripts/google_cloud/components/databases/service_table.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/deployments/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/deployments/panel.vue50
-rw-r--r--app/assets/javascripts/google_cloud/deployments/service_table.vue (renamed from app/assets/javascripts/google_cloud/components/deployments_service_table.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/gcp_regions/form.vue (renamed from app/assets/javascripts/google_cloud/components/gcp_regions_form.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/gcp_regions/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/gcp_regions/list.vue (renamed from app/assets/javascripts/google_cloud/components/gcp_regions_list.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/index.js12
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/form.vue (renamed from app/assets/javascripts/google_cloud/components/service_accounts_form.vue)0
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/index.js11
-rw-r--r--app/assets/javascripts/google_cloud/service_accounts/list.vue (renamed from app/assets/javascripts/google_cloud/components/service_accounts_list.vue)0
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue73
-rw-r--r--app/assets/javascripts/groups/constants.js22
-rw-r--r--app/assets/javascripts/groups/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/configuration/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/databases/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/deployments/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/gcp_regions/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/google_cloud/service_accounts/index.js3
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue9
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue3
-rw-r--r--app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue258
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/pipelines/stores/test_reports/constants.js2
-rw-r--r--app/assets/stylesheets/pages/groups.scss1
-rw-r--r--app/controllers/oauth/authorizations_controller.rb2
-rw-r--r--app/controllers/projects/google_cloud/base_controller.rb18
-rw-r--r--app/controllers/projects/google_cloud/configuration_controller.rb38
-rw-r--r--app/controllers/projects/google_cloud/databases_controller.rb18
-rw-r--r--app/controllers/projects/google_cloud/deployments_controller.rb29
-rw-r--r--app/controllers/projects/google_cloud/gcp_regions_controller.rb12
-rw-r--r--app/controllers/projects/google_cloud/revoke_oauth_controller.rb7
-rw-r--r--app/controllers/projects/google_cloud/service_accounts_controller.rb25
-rw-r--r--app/controllers/projects/google_cloud_controller.rb34
-rw-r--r--app/controllers/projects/pipelines/tests_controller.rb2
-rw-r--r--app/helpers/ci/pipeline_editor_helper.rb1
-rw-r--r--app/models/customer_relations/contact.rb17
-rw-r--r--app/models/issue.rb53
-rw-r--r--app/models/project_setting.rb2
-rw-r--r--app/services/google_cloud/gcp_region_add_or_replace_service.rb2
-rw-r--r--app/services/groups/transfer_service.rb6
-rw-r--r--app/views/groups/_shared_projects.html.haml2
-rw-r--r--app/views/projects/google_cloud/configuration/index.html.haml7
-rw-r--r--app/views/projects/google_cloud/databases/index.html.haml7
-rw-r--r--app/views/projects/google_cloud/deployments/index.html.haml7
-rw-r--r--app/views/projects/google_cloud/errors/gcp_error.html.haml6
-rw-r--r--app/views/projects/google_cloud/errors/no_gcp_projects.html.haml6
-rw-r--r--app/views/projects/google_cloud/gcp_regions/index.html.haml6
-rw-r--r--app/views/projects/google_cloud/index.html.haml6
-rw-r--r--app/views/projects/google_cloud/service_accounts/index.html.haml6
-rw-r--r--config/feature_flags/ops/enforce_memory_watchdog.yml (renamed from config/feature_flags/development/omniauth_login_minimal_scopes.yml)12
-rw-r--r--config/feature_flags/ops/gitlab_memory_watchdog.yml8
-rw-r--r--config/initializers/memory_watchdog.rb19
-rw-r--r--config/routes/project.rb7
-rw-r--r--doc/integration/gitlab.md7
-rw-r--r--doc/user/infrastructure/clusters/connect/new_civo_cluster.md6
-rw-r--r--doc/user/usage_quotas.md54
-rw-r--r--lib/gitlab/memory/watchdog.rb192
-rw-r--r--lib/sidebars/projects/menus/infrastructure_menu.rb10
-rw-r--r--locale/gitlab.pot69
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb2
-rwxr-xr-xscripts/review_apps/review-apps.sh1
-rw-r--r--spec/controllers/projects/pipelines/tests_controller_spec.rb2
-rw-r--r--spec/features/groups/show_spec.rb25
-rw-r--r--spec/frontend/ci_lint/mock_data.js11
-rw-r--r--spec/frontend/google_cloud/components/app_spec.js77
-rw-r--r--spec/frontend/google_cloud/components/errors/gcp_error_spec.js34
-rw-r--r--spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js33
-rw-r--r--spec/frontend/google_cloud/components/google_cloud_menu_spec.js40
-rw-r--r--spec/frontend/google_cloud/components/home_spec.js66
-rw-r--r--spec/frontend/google_cloud/components/incubation_banner_spec.js21
-rw-r--r--spec/frontend/google_cloud/components/revoke_oauth_spec.js2
-rw-r--r--spec/frontend/google_cloud/configuration/panel_spec.js65
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js (renamed from spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js (renamed from spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/databases/panel_spec.js36
-rw-r--r--spec/frontend/google_cloud/databases/service_table_spec.js (renamed from spec/frontend/google_cloud/components/databases/service_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/deployments/panel_spec.js46
-rw-r--r--spec/frontend/google_cloud/deployments/service_table_spec.js (renamed from spec/frontend/google_cloud/components/deployments_service_table_spec.js)4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/form_spec.js (renamed from spec/frontend/google_cloud/components/gcp_regions_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/gcp_regions/list_spec.js (renamed from spec/frontend/google_cloud/components/gcp_regions_list_spec.js)4
-rw-r--r--spec/frontend/google_cloud/service_accounts/form_spec.js (renamed from spec/frontend/google_cloud/components/service_accounts_form_spec.js)4
-rw-r--r--spec/frontend/google_cloud/service_accounts/list_spec.js (renamed from spec/frontend/google_cloud/components/service_accounts_list_spec.js)4
-rw-r--r--spec/frontend/groups/components/group_item_spec.js101
-rw-r--r--spec/frontend/groups/components/groups_spec.js72
-rw-r--r--spec/frontend/groups/mock_data.js1
-rw-r--r--spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js22
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js78
-rw-r--r--spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js251
-rw-r--r--spec/frontend/pipeline_editor/mock_data.js1
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js2
-rw-r--r--spec/helpers/ci/pipeline_editor_helper_spec.rb2
-rw-r--r--spec/lib/gitlab/memory/watchdog_spec.rb308
-rw-r--r--spec/models/project_setting_spec.rb11
-rw-r--r--spec/requests/api/graphql/crm/contacts_spec.rb69
-rw-r--r--spec/requests/api/graphql/project/issues_spec.rb37
-rw-r--r--spec/requests/projects/google_cloud/configuration_controller_spec.rb141
-rw-r--r--spec/requests/projects/google_cloud/databases_controller_spec.rb135
-rw-r--r--spec/requests/projects/google_cloud/deployments_controller_spec.rb27
-rw-r--r--spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb16
-rw-r--r--spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb12
-rw-r--r--spec/requests/projects/google_cloud/service_accounts_controller_spec.rb67
-rw-r--r--spec/requests/projects/google_cloud_controller_spec.rb178
-rw-r--r--spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb2
-rw-r--r--spec/services/groups/transfer_service_spec.rb6
-rw-r--r--spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb2
121 files changed, 2719 insertions, 967 deletions
diff --git a/.gitlab/ci/review-apps/main.gitlab-ci.yml b/.gitlab/ci/review-apps/main.gitlab-ci.yml
index 45ada3fd09e..7f3ae70da8c 100644
--- a/.gitlab/ci/review-apps/main.gitlab-ci.yml
+++ b/.gitlab/ci/review-apps/main.gitlab-ci.yml
@@ -77,7 +77,7 @@ review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "a6a609a19166f00b1a7774374041cd38a9f7e20d"
+ GITLAB_HELM_CHART_REF: "138c146a5ba787942f66d4c7d795d224d6ba206a"
environment:
name: review/${CI_COMMIT_REF_SLUG}${SCHEDULE_TYPE} # No separator for SCHEDULE_TYPE so it's compatible as before and looks nice without it
url: https://gitlab-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}
diff --git a/app/assets/javascripts/google_cloud/components/app.vue b/app/assets/javascripts/google_cloud/components/app.vue
deleted file mode 100644
index b3d773e6bee..00000000000
--- a/app/assets/javascripts/google_cloud/components/app.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { __ } from '~/locale';
-
-import Home from './home.vue';
-import IncubationBanner from './incubation_banner.vue';
-import ServiceAccountsForm from './service_accounts_form.vue';
-import GcpRegionsForm from './gcp_regions_form.vue';
-import NoGcpProjects from './errors/no_gcp_projects.vue';
-import GcpError from './errors/gcp_error.vue';
-
-const SCREEN_GCP_ERROR = 'gcp_error';
-const SCREEN_HOME = 'home';
-const SCREEN_NO_GCP_PROJECTS = 'no_gcp_projects';
-const SCREEN_SERVICE_ACCOUNTS_FORM = 'service_accounts_form';
-const SCREEN_GCP_REGIONS_FORM = 'gcp_regions_form';
-
-export default {
- components: {
- IncubationBanner,
- },
- inheritAttrs: false,
- props: {
- screen: {
- required: true,
- type: String,
- },
- },
- computed: {
- mainComponent() {
- switch (this.screen) {
- case SCREEN_HOME:
- return Home;
- case SCREEN_GCP_ERROR:
- return GcpError;
- case SCREEN_NO_GCP_PROJECTS:
- return NoGcpProjects;
- case SCREEN_SERVICE_ACCOUNTS_FORM:
- return ServiceAccountsForm;
- case SCREEN_GCP_REGIONS_FORM:
- return GcpRegionsForm;
- default:
- throw new Error(__('Unknown screen'));
- }
- },
- },
- methods: {
- feedbackUrl(template) {
- return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=${template}`;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <incubation-banner
- :share-feedback-url="feedbackUrl('general_feedback')"
- :report-bug-url="feedbackUrl('report_bug')"
- :feature-request-url="feedbackUrl('feature_request')"
- />
- <component :is="mainComponent" v-bind="$attrs" />
- </div>
-</template>
diff --git a/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue b/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue
deleted file mode 100644
index 90aa0e1ae68..00000000000
--- a/app/assets/javascripts/google_cloud/components/errors/gcp_error.vue
+++ /dev/null
@@ -1,29 +0,0 @@
-<script>
-import { GlAlert } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: { GlAlert },
- props: {
- error: {
- type: String,
- required: true,
- },
- },
- i18n: {
- title: __('Google Cloud project misconfigured'),
- description: __(
- 'GitLab and Google Cloud configuration seems to be incomplete. This probably can be fixed by your GitLab administration team. You may share these logs with them:',
- ),
- },
-};
-</script>
-
-<template>
- <gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
- {{ $options.i18n.description }}
- <blockquote>
- <code>{{ error }}</code>
- </blockquote>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue b/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue
deleted file mode 100644
index da229ac3f0e..00000000000
--- a/app/assets/javascripts/google_cloud/components/errors/no_gcp_projects.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { GlAlert, GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: { GlAlert, GlButton },
- i18n: {
- title: __('Google Cloud project required'),
- description: __(
- 'You do not have any Google Cloud projects. Please create a Google Cloud project and then reload this page.',
- ),
- createLabel: __('Create Google Cloud project'),
- },
-};
-</script>
-
-<template>
- <gl-alert :dismissible="false" variant="warning" :title="$options.i18n.title">
- {{ $options.i18n.description }}
- <template #actions>
- <gl-button href="https://console.cloud.google.com/projectcreate" target="_blank">
- {{ $options.i18n.createLabel }}
- </gl-button>
- </template>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue b/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue
new file mode 100644
index 00000000000..d6b7c702b54
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/google_cloud_menu.vue
@@ -0,0 +1,85 @@
+<script>
+import { s__ } from '~/locale';
+
+const CONFIGURATION_KEY = 'configuration';
+const DEPLOYMENTS_KEY = 'deployments';
+const DATABASES_KEY = 'databases';
+
+const i18n = {
+ configuration: { title: s__('CloudSeed|Configuration') },
+ deployments: { title: s__('CloudSeed|Deployments') },
+ databases: { title: s__('CloudSeed|Databases') },
+};
+
+export default {
+ props: {
+ active: {
+ type: String,
+ required: true,
+ },
+ configurationUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsUrl: {
+ type: String,
+ required: true,
+ },
+ databasesUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isConfigurationActive() {
+ return this.active === CONFIGURATION_KEY;
+ },
+ isDeploymentsActive() {
+ return this.active === DEPLOYMENTS_KEY;
+ },
+ isDatabasesActive() {
+ return this.active === DATABASES_KEY;
+ },
+ },
+ i18n,
+};
+</script>
+<template>
+ <div class="tabs gl-tabs">
+ <ul role="tablist" class="nav gl-tabs-nav">
+ <li role="presentation" class="nav-item">
+ <a
+ data-testid="configurationLink"
+ role="tab"
+ :href="configurationUrl"
+ class="nav-link gl-tab-nav-item"
+ :class="{ 'gl-tab-nav-item-active': isConfigurationActive }"
+ >
+ {{ $options.i18n.configuration.title }}</a
+ >
+ </li>
+ <li role="presentation" class="nav-item">
+ <a
+ data-testid="deploymentsLink"
+ role="tab"
+ :href="deploymentsUrl"
+ class="nav-link gl-tab-nav-item"
+ :class="{ 'gl-tab-nav-item-active': isDeploymentsActive }"
+ >
+ {{ $options.i18n.deployments.title }}
+ </a>
+ </li>
+ <li role="presentation" class="nav-item">
+ <a
+ data-testid="databasesLink"
+ role="tab"
+ :href="databasesUrl"
+ class="nav-link gl-tab-nav-item"
+ :class="{ 'gl-tab-nav-item-active': isDatabasesActive }"
+ >
+ {{ $options.i18n.databases.title }}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue
deleted file mode 100644
index e41337e2679..00000000000
--- a/app/assets/javascripts/google_cloud/components/home.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import { GlTabs, GlTab } from '@gitlab/ui';
-import DeploymentsServiceTable from './deployments_service_table.vue';
-import RevokeOauth from './revoke_oauth.vue';
-import ServiceAccountsList from './service_accounts_list.vue';
-import GcpRegionsList from './gcp_regions_list.vue';
-
-export default {
- components: {
- GlTabs,
- GlTab,
- DeploymentsServiceTable,
- RevokeOauth,
- ServiceAccountsList,
- GcpRegionsList,
- },
- props: {
- serviceAccounts: {
- type: Array,
- required: true,
- },
- createServiceAccountUrl: {
- type: String,
- required: true,
- },
- configureGcpRegionsUrl: {
- type: String,
- required: true,
- },
- emptyIllustrationUrl: {
- type: String,
- required: true,
- },
- enableCloudRunUrl: {
- type: String,
- required: true,
- },
- enableCloudStorageUrl: {
- type: String,
- required: true,
- },
- gcpRegions: {
- type: Array,
- required: true,
- },
- revokeOauthUrl: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <gl-tabs>
- <gl-tab :title="__('Configuration')">
- <service-accounts-list
- class="gl-mx-4"
- :list="serviceAccounts"
- :create-url="createServiceAccountUrl"
- :empty-illustration-url="emptyIllustrationUrl"
- />
- <hr />
- <gcp-regions-list
- class="gl-mx-4"
- :empty-illustration-url="emptyIllustrationUrl"
- :create-url="configureGcpRegionsUrl"
- :list="gcpRegions"
- />
- <hr v-if="revokeOauthUrl" />
- <revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
- </gl-tab>
- <gl-tab :title="__('Deployments')">
- <deployments-service-table
- :cloud-run-url="enableCloudRunUrl"
- :cloud-storage-url="enableCloudStorageUrl"
- />
- </gl-tab>
- <gl-tab :title="__('Services')" disabled />
- </gl-tabs>
-</template>
diff --git a/app/assets/javascripts/google_cloud/components/incubation_banner.vue b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
index 652b8c1aecb..128b3dcb1d9 100644
--- a/app/assets/javascripts/google_cloud/components/incubation_banner.vue
+++ b/app/assets/javascripts/google_cloud/components/incubation_banner.vue
@@ -1,22 +1,20 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+const FEATURE_REQUEST_KEY = 'feature_request';
+const REPORT_BUG_KEY = 'report_bug';
+const GENERAL_FEEDBACK_KEY = 'general_feedback';
+
export default {
components: { GlAlert, GlLink, GlSprintf },
- props: {
- shareFeedbackUrl: {
- required: true,
- type: String,
- },
- reportBugUrl: {
- required: true,
- type: String,
- },
- featureRequestUrl: {
- required: true,
- type: String,
+ methods: {
+ feedbackUrl(template) {
+ return `https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=${template}`;
},
},
+ FEATURE_REQUEST_KEY,
+ REPORT_BUG_KEY,
+ GENERAL_FEEDBACK_KEY,
};
</script>
@@ -31,13 +29,13 @@ export default {
"
>
<template #featureLink="{ content }">
- <gl-link :href="featureRequestUrl">{{ content }}</gl-link>
+ <gl-link :href="feedbackUrl($options.FEATURE_REQUEST_KEY)">{{ content }}</gl-link>
</template>
<template #bugLink="{ content }">
- <gl-link :href="reportBugUrl">{{ content }}</gl-link>
+ <gl-link :href="feedbackUrl($options.REPORT_BUG_KEY)">{{ content }}</gl-link>
</template>
<template #feedbackLink="{ content }">
- <gl-link :href="shareFeedbackUrl">{{ content }}</gl-link>
+ <gl-link :href="feedbackUrl($options.GENERAL_FEEDBACK_KEY)">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
diff --git a/app/assets/javascripts/google_cloud/configuration/index.js b/app/assets/javascripts/google_cloud/configuration/index.js
new file mode 100644
index 00000000000..580315588d0
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/configuration/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Panel from './panel.vue';
+
+export default (containerId = '#js-google-cloud-configuration') => {
+ const element = document.querySelector(containerId);
+ const { ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Panel, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/configuration/panel.vue b/app/assets/javascripts/google_cloud/configuration/panel.vue
new file mode 100644
index 00000000000..ee046eb1988
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/configuration/panel.vue
@@ -0,0 +1,88 @@
+<script>
+import GcpRegionsList from '../gcp_regions/list.vue';
+import GoogleCloudMenu from '../components/google_cloud_menu.vue';
+import IncubationBanner from '../components/incubation_banner.vue';
+import RevokeOauth from '../components/revoke_oauth.vue';
+import ServiceAccountsList from '../service_accounts/list.vue';
+
+export default {
+ components: {
+ GcpRegionsList,
+ GoogleCloudMenu,
+ IncubationBanner,
+ RevokeOauth,
+ ServiceAccountsList,
+ },
+ props: {
+ configurationUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsUrl: {
+ type: String,
+ required: true,
+ },
+ databasesUrl: {
+ type: String,
+ required: true,
+ },
+ serviceAccounts: {
+ type: Array,
+ required: true,
+ },
+ createServiceAccountUrl: {
+ type: String,
+ required: true,
+ },
+ emptyIllustrationUrl: {
+ type: String,
+ required: true,
+ },
+ configureGcpRegionsUrl: {
+ type: String,
+ required: true,
+ },
+ gcpRegions: {
+ type: Array,
+ required: true,
+ },
+ revokeOauthUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner />
+
+ <google-cloud-menu
+ active="configuration"
+ :configuration-url="configurationUrl"
+ :deployments-url="deploymentsUrl"
+ :databases-url="databasesUrl"
+ />
+
+ <service-accounts-list
+ class="gl-mx-4"
+ :list="serviceAccounts"
+ :create-url="createServiceAccountUrl"
+ :empty-illustration-url="emptyIllustrationUrl"
+ />
+
+ <hr />
+
+ <gcp-regions-list
+ class="gl-mx-4"
+ :empty-illustration-url="emptyIllustrationUrl"
+ :create-url="configureGcpRegionsUrl"
+ :list="gcpRegions"
+ />
+
+ <hr v-if="revokeOauthUrl" />
+
+ <revoke-oauth v-if="revokeOauthUrl" :url="revokeOauthUrl" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/cloudsql/create_instance_form.vue b/app/assets/javascripts/google_cloud/databases/cloudsql/create_instance_form.vue
index 0ac561b6132..0ac561b6132 100644
--- a/app/assets/javascripts/google_cloud/components/cloudsql/create_instance_form.vue
+++ b/app/assets/javascripts/google_cloud/databases/cloudsql/create_instance_form.vue
diff --git a/app/assets/javascripts/google_cloud/components/cloudsql/instance_table.vue b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
index 823895214df..823895214df 100644
--- a/app/assets/javascripts/google_cloud/components/cloudsql/instance_table.vue
+++ b/app/assets/javascripts/google_cloud/databases/cloudsql/instance_table.vue
diff --git a/app/assets/javascripts/google_cloud/databases/index.js b/app/assets/javascripts/google_cloud/databases/index.js
new file mode 100644
index 00000000000..e240a1116e8
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/databases/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Panel from './panel.vue';
+
+export default (containerId = '#js-google-cloud-databases') => {
+ const element = document.querySelector(containerId);
+ const { ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Panel, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/databases/panel.vue b/app/assets/javascripts/google_cloud/databases/panel.vue
new file mode 100644
index 00000000000..e2f18c286a5
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/databases/panel.vue
@@ -0,0 +1,38 @@
+<script>
+import GoogleCloudMenu from '../components/google_cloud_menu.vue';
+import IncubationBanner from '../components/incubation_banner.vue';
+
+export default {
+ components: {
+ IncubationBanner,
+ GoogleCloudMenu,
+ },
+ props: {
+ configurationUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsUrl: {
+ type: String,
+ required: true,
+ },
+ databasesUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner />
+
+ <google-cloud-menu
+ active="databases"
+ :configuration-url="configurationUrl"
+ :deployments-url="deploymentsUrl"
+ :databases-url="databasesUrl"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/databases/service_table.vue b/app/assets/javascripts/google_cloud/databases/service_table.vue
index 80bd6ef28fb..80bd6ef28fb 100644
--- a/app/assets/javascripts/google_cloud/components/databases/service_table.vue
+++ b/app/assets/javascripts/google_cloud/databases/service_table.vue
diff --git a/app/assets/javascripts/google_cloud/deployments/index.js b/app/assets/javascripts/google_cloud/deployments/index.js
new file mode 100644
index 00000000000..fcbb2209c40
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/deployments/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Panel from './panel.vue';
+
+export default (containerId = '#js-google-cloud-deployments') => {
+ const element = document.querySelector(containerId);
+ const { ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Panel, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/deployments/panel.vue b/app/assets/javascripts/google_cloud/deployments/panel.vue
new file mode 100644
index 00000000000..89db132ad5e
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/deployments/panel.vue
@@ -0,0 +1,50 @@
+<script>
+import GoogleCloudMenu from '../components/google_cloud_menu.vue';
+import IncubationBanner from '../components/incubation_banner.vue';
+import ServiceTable from './service_table.vue';
+
+export default {
+ components: {
+ ServiceTable,
+ IncubationBanner,
+ GoogleCloudMenu,
+ },
+ props: {
+ configurationUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsUrl: {
+ type: String,
+ required: true,
+ },
+ databasesUrl: {
+ type: String,
+ required: true,
+ },
+ enableCloudRunUrl: {
+ type: String,
+ required: true,
+ },
+ enableCloudStorageUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-banner />
+
+ <google-cloud-menu
+ active="deployments"
+ :configuration-url="configurationUrl"
+ :deployments-url="deploymentsUrl"
+ :databases-url="databasesUrl"
+ />
+
+ <service-table :cloud-run-url="enableCloudRunUrl" :cloud-storage-url="enableCloudStorageUrl" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/deployments_service_table.vue b/app/assets/javascripts/google_cloud/deployments/service_table.vue
index 26c9fd14dc6..26c9fd14dc6 100644
--- a/app/assets/javascripts/google_cloud/components/deployments_service_table.vue
+++ b/app/assets/javascripts/google_cloud/deployments/service_table.vue
diff --git a/app/assets/javascripts/google_cloud/components/gcp_regions_form.vue b/app/assets/javascripts/google_cloud/gcp_regions/form.vue
index 23011e5a5b0..23011e5a5b0 100644
--- a/app/assets/javascripts/google_cloud/components/gcp_regions_form.vue
+++ b/app/assets/javascripts/google_cloud/gcp_regions/form.vue
diff --git a/app/assets/javascripts/google_cloud/gcp_regions/index.js b/app/assets/javascripts/google_cloud/gcp_regions/index.js
new file mode 100644
index 00000000000..da37c612805
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/gcp_regions/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Form from './form.vue';
+
+export default (containerId = '#js-google-cloud-gcp-regions') => {
+ const element = document.querySelector(containerId);
+ const { ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Form, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue b/app/assets/javascripts/google_cloud/gcp_regions/list.vue
index 5d403d5cd65..5d403d5cd65 100644
--- a/app/assets/javascripts/google_cloud/components/gcp_regions_list.vue
+++ b/app/assets/javascripts/google_cloud/gcp_regions/list.vue
diff --git a/app/assets/javascripts/google_cloud/index.js b/app/assets/javascripts/google_cloud/index.js
deleted file mode 100644
index ab9e8227812..00000000000
--- a/app/assets/javascripts/google_cloud/index.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import Vue from 'vue';
-import App from './components/app.vue';
-
-export default () => {
- const root = '#js-google-cloud';
- const element = document.querySelector(root);
- const { screen, ...attrs } = JSON.parse(element.getAttribute('data'));
- return new Vue({
- el: element,
- render: (createElement) => createElement(App, { props: { screen }, attrs }),
- });
-};
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts_form.vue b/app/assets/javascripts/google_cloud/service_accounts/form.vue
index faec94e735b..faec94e735b 100644
--- a/app/assets/javascripts/google_cloud/components/service_accounts_form.vue
+++ b/app/assets/javascripts/google_cloud/service_accounts/form.vue
diff --git a/app/assets/javascripts/google_cloud/service_accounts/index.js b/app/assets/javascripts/google_cloud/service_accounts/index.js
new file mode 100644
index 00000000000..5207b44deac
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/service_accounts/index.js
@@ -0,0 +1,11 @@
+import Vue from 'vue';
+import Form from './form.vue';
+
+export default (containerId = '#js-google-cloud-service-accounts') => {
+ const element = document.querySelector(containerId);
+ const { ...attrs } = JSON.parse(element.getAttribute('data'));
+ return new Vue({
+ el: element,
+ render: (createElement) => createElement(Form, { attrs }),
+ });
+};
diff --git a/app/assets/javascripts/google_cloud/components/service_accounts_list.vue b/app/assets/javascripts/google_cloud/service_accounts/list.vue
index 4b580c594f5..4b580c594f5 100644
--- a/app/assets/javascripts/google_cloud/components/service_accounts_list.vue
+++ b/app/assets/javascripts/google_cloud/service_accounts/list.vue
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index c01db435f53..7345afb8545 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -5,13 +5,23 @@ import {
GlBadge,
GlIcon,
GlLabel,
+ GlButton,
+ GlPopover,
+ GlLink,
GlTooltipDirective,
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { __ } from '~/locale';
+import {
+ VISIBILITY_TYPE_ICON,
+ GROUP_VISIBILITY_TYPE,
+ ITEM_TYPE,
+ VISIBILITY_PRIVATE,
+} from '../constants';
import eventHub from '../event_hub';
import itemActions from './item_actions.vue';
@@ -30,12 +40,16 @@ export default {
GlLoadingIcon,
GlIcon,
GlLabel,
+ GlButton,
+ GlPopover,
+ GlLink,
UserAccessRoleBadge,
itemCaret,
itemTypeIcon,
itemActions,
itemStats,
},
+ inject: ['currentGroupVisibility'],
props: {
parentGroup: {
type: Object,
@@ -56,6 +70,9 @@ export default {
groupDomId() {
return `group-${this.group.id}`;
},
+ itemTestId() {
+ return `group-overview-item-${this.group.id}`;
+ },
rowClass() {
return {
'is-open': this.group.isOpen,
@@ -74,10 +91,10 @@ export default {
return Boolean(this.group.complianceFramework?.name);
},
isGroup() {
- return this.group.type === 'group';
+ return this.group.type === ITEM_TYPE.GROUP;
},
isGroupPendingRemoval() {
- return this.group.type === 'group' && this.group.pendingRemoval;
+ return this.group.type === ITEM_TYPE.GROUP && this.group.pendingRemoval;
},
visibilityIcon() {
return VISIBILITY_TYPE_ICON[this.group.visibility];
@@ -94,6 +111,13 @@ export default {
showActionsMenu() {
return this.isGroup && (this.group.canEdit || this.group.canRemove || this.group.canLeave);
},
+ shouldShowVisibilityWarning() {
+ return (
+ this.action === 'shared' &&
+ this.currentGroupVisibility === VISIBILITY_PRIVATE &&
+ this.group.visibility !== VISIBILITY_PRIVATE
+ );
+ },
},
methods: {
onClickRowGroup(e) {
@@ -110,6 +134,17 @@ export default {
}
},
},
+ i18n: {
+ popoverTitle: __('Less restrictive visibility'),
+ popoverBody: __('Project visibility level is less restrictive than the group settings.'),
+ learnMore: __('Learn more'),
+ },
+ shareProjectsWithGroupsHelpPagePath: helpPagePath(
+ 'user/project/members/share_project_with_groups',
+ {
+ anchor: 'share-a-public-project-with-private-group',
+ },
+ ),
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
AVATAR_SHAPE_OPTION_RECT,
};
@@ -118,6 +153,7 @@ export default {
<template>
<li
:id="groupDomId"
+ :data-testid="itemTestId"
:class="rowClass"
class="group-row"
:itemprop="microdata.itemprop"
@@ -163,7 +199,7 @@ export default {
data-testid="group-name"
:href="group.relativePath"
:title="group.fullName"
- class="no-expand gl-mr-3 gl-mt-3 gl-text-gray-900!"
+ class="no-expand gl-mr-3 gl-text-gray-900!"
:itemprop="microdata.nameItemprop"
>
{{
@@ -174,17 +210,40 @@ export default {
</a>
<gl-icon
v-gl-tooltip.hover.bottom
- class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-mt-3 gl-text-gray-500"
+ class="gl-display-inline-flex gl-align-items-center gl-mr-3 gl-text-gray-500"
:name="visibilityIcon"
:title="visibilityTooltip"
data-testid="group-visibility-icon"
/>
- <user-access-role-badge v-if="group.permission" class="gl-mt-3">
+ <template v-if="shouldShowVisibilityWarning">
+ <gl-button
+ ref="visibilityWarningButton"
+ class="gl-p-1! gl-bg-transparent! gl-mr-3"
+ category="tertiary"
+ icon="warning"
+ :aria-label="$options.i18n.popoverTitle"
+ @click.stop
+ />
+ <gl-popover
+ :target="() => $refs.visibilityWarningButton.$el"
+ :title="$options.i18n.popoverTitle"
+ triggers="hover focus"
+ >
+ {{ $options.i18n.popoverBody }}
+ <div class="gl-mt-3">
+ <gl-link
+ class="gl-font-sm"
+ :href="$options.shareProjectsWithGroupsHelpPagePath"
+ >{{ $options.i18n.learnMore }}</gl-link
+ >
+ </div>
+ </gl-popover>
+ </template>
+ <user-access-role-badge v-if="group.permission" class="gl-mr-3">
{{ group.permission }}
</user-access-role-badge>
<gl-label
v-if="hasComplianceFramework"
- class="gl-mt-3"
:title="complianceFramework.name"
:background-color="complianceFramework.color"
:description="complianceFramework.description"
diff --git a/app/assets/javascripts/groups/constants.js b/app/assets/javascripts/groups/constants.js
index cacba2dfd23..29981d09155 100644
--- a/app/assets/javascripts/groups/constants.js
+++ b/app/assets/javascripts/groups/constants.js
@@ -28,28 +28,32 @@ export const ITEM_TYPE = {
GROUP: 'group',
};
+export const VISIBILITY_PUBLIC = 'public';
+export const VISIBILITY_INTERNAL = 'internal';
+export const VISIBILITY_PRIVATE = 'private';
+
export const GROUP_VISIBILITY_TYPE = {
- public: __(
+ [VISIBILITY_PUBLIC]: __(
'Public - The group and any public projects can be viewed without any authentication.',
),
- internal: __(
+ [VISIBILITY_INTERNAL]: __(
'Internal - The group and any internal projects can be viewed by any logged in user except external users.',
),
- private: __('Private - The group and its projects can only be viewed by members.'),
+ [VISIBILITY_PRIVATE]: __('Private - The group and its projects can only be viewed by members.'),
};
export const PROJECT_VISIBILITY_TYPE = {
- public: __('Public - The project can be accessed without any authentication.'),
- internal: __(
+ [VISIBILITY_PUBLIC]: __('Public - The project can be accessed without any authentication.'),
+ [VISIBILITY_INTERNAL]: __(
'Internal - The project can be accessed by any logged in user except external users.',
),
- private: __(
+ [VISIBILITY_PRIVATE]: __(
'Private - Project access must be granted explicitly to each user. If this project is part of a group, access will be granted to members of the group.',
),
};
export const VISIBILITY_TYPE_ICON = {
- public: 'earth',
- internal: 'shield',
- private: 'lock',
+ [VISIBILITY_PUBLIC]: 'earth',
+ [VISIBILITY_INTERNAL]: 'shield',
+ [VISIBILITY_PRIVATE]: 'lock',
};
diff --git a/app/assets/javascripts/groups/index.js b/app/assets/javascripts/groups/index.js
index dfcee80aec7..a502fcd31ad 100644
--- a/app/assets/javascripts/groups/index.js
+++ b/app/assets/javascripts/groups/index.js
@@ -55,6 +55,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState,
canCreateSubgroups,
canCreateProjects,
+ currentGroupVisibility,
},
} = this.$options.el;
@@ -67,6 +68,7 @@ export default (containerId = 'js-groups-tree', endpoint, action = '') => {
renderEmptyState: parseBoolean(renderEmptyState),
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
+ currentGroupVisibility,
};
},
data() {
diff --git a/app/assets/javascripts/pages/projects/google_cloud/configuration/index.js b/app/assets/javascripts/pages/projects/google_cloud/configuration/index.js
new file mode 100644
index 00000000000..abececa44ee
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/configuration/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/configuration/index';
+
+init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/databases/index.js b/app/assets/javascripts/pages/projects/google_cloud/databases/index.js
new file mode 100644
index 00000000000..5482324f1cd
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/databases/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/databases/index';
+
+init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/deployments/index.js b/app/assets/javascripts/pages/projects/google_cloud/deployments/index.js
new file mode 100644
index 00000000000..b5a29b3825b
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/deployments/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/deployments/index';
+
+init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/gcp_regions/index.js b/app/assets/javascripts/pages/projects/google_cloud/gcp_regions/index.js
new file mode 100644
index 00000000000..fb66e2fa051
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/gcp_regions/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/gcp_regions/index';
+
+init();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/index.js b/app/assets/javascripts/pages/projects/google_cloud/index.js
deleted file mode 100644
index 4506ea8efd1..00000000000
--- a/app/assets/javascripts/pages/projects/google_cloud/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initGoogleCloud from '~/google_cloud/index';
-
-initGoogleCloud();
diff --git a/app/assets/javascripts/pages/projects/google_cloud/service_accounts/index.js b/app/assets/javascripts/pages/projects/google_cloud/service_accounts/index.js
new file mode 100644
index 00000000000..8b644c2b324
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/google_cloud/service_accounts/index.js
@@ -0,0 +1,3 @@
+import init from '~/google_cloud/service_accounts/index';
+
+init();
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index 23f1592cac1..610a570c4ce 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -19,7 +19,7 @@ export const i18n = {
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
unavailableValidation: s__('Pipelines|Configuration validation currently not available.'),
- valid: s__('Pipelines|This GitLab CI configuration is valid.'),
+ valid: s__('Pipelines|Pipeline syntax is correct.'),
};
export default {
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
index 9a789ccab4d..0f19b9386e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
@@ -52,6 +52,11 @@ export default {
required: false,
default: false,
},
+ hideAlert: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isValid: {
type: Boolean,
required: true,
@@ -63,7 +68,8 @@ export default {
},
lintHelpPagePath: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
warnings: {
type: Array,
@@ -96,6 +102,7 @@ export default {
<template>
<div>
<gl-alert
+ v-if="!hideAlert"
class="gl-mb-5"
:variant="status.variant"
:title="__('Status:')"
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index 4ebfec7a715..99ee244577e 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -219,8 +219,7 @@ export default {
:title="$options.i18n.tabValidate"
@click="setCurrentTab($options.tabConstants.VALIDATE_TAB)"
>
- <gl-loading-icon v-if="isLoading" size="lg" class="gl-m-3" />
- <ci-validate v-else />
+ <ci-validate :ci-file-content="ciFileContent" />
</editor-tab>
<editor-tab
v-else
diff --git a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
index 4453796a0e4..47673119db9 100644
--- a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
@@ -1,10 +1,35 @@
<script>
-import { GlButton, GlDropdown, GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlButton,
+ GlDropdown,
+ GlIcon,
+ GlLoadingIcon,
+ GlLink,
+ GlTooltip,
+ GlTooltipDirective,
+ GlSprintf,
+} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue';
+import CiLintResults from '../lint/ci_lint_results.vue';
+import getBlobContent from '../../graphql/queries/blob_content.query.graphql';
+import getCurrentBranch from '../../graphql/queries/client/current_branch.query.graphql';
+import lintCiMutation from '../../graphql/mutations/client/lint_ci.mutation.graphql';
export const i18n = {
+ alertDesc: s__(
+ 'PipelineEditor|Simulated a %{codeStart}git push%{codeEnd} event for a default branch. %{codeStart}Rules%{codeEnd}, %{codeStart}only%{codeEnd}, %{codeStart}except%{codeEnd}, and %{codeStart}needs%{codeEnd} job dependencies logic have been evaluated. %{linkStart}Learn more%{linkEnd}',
+ ),
+ cancelBtn: __('Cancel'),
+ contentChange: s__(
+ 'PipelineEditor|Configuration content has changed. Re-run validation for updated results.',
+ ),
+ cta: s__('PipelineEditor|Validate pipeline'),
+ ctaDisabledTooltip: s__('PipelineEditor|Waiting for CI content to load...'),
+ errorAlertTitle: s__('PipelineEditor|Pipeline simulation completed with errors'),
help: __('Help'),
+ loading: s__('PipelineEditor|Validating pipeline... It can take up to a minute.'),
pipelineSource: s__('PipelineEditor|Pipeline Source'),
pipelineSourceDefault: s__('PipelineEditor|Git push event to the default branch'),
pipelineSourceTooltip: s__('PipelineEditor|Other pipeline sources are not available yet.'),
@@ -15,48 +40,179 @@ export const i18n = {
simulationNote: s__(
'PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies.',
),
- cta: s__('PipelineEditor|Validate pipeline'),
+ successAlertTitle: s__('PipelineEditor|Simulation completed successfully'),
};
+export const VALIDATE_TAB_INIT = 'VALIDATE_TAB_INIT';
+export const VALIDATE_TAB_RESULTS = 'VALIDATE_TAB_RESULTS';
+export const VALIDATE_TAB_LOADING = 'VALIDATE_TAB_LOADING';
+const BASE_CLASSES = [
+ 'gl-display-flex',
+ 'gl-flex-direction-column',
+ 'gl-align-items-center',
+ 'gl-mt-11',
+];
+
export default {
name: 'CiValidateTab',
components: {
+ CiLintResults,
+ GlAlert,
GlButton,
GlDropdown,
GlIcon,
+ GlLoadingIcon,
+ GlLink,
GlSprintf,
+ GlTooltip,
ValidatePipelinePopover,
},
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['validateTabIllustrationPath'],
+ inject: ['ciConfigPath', 'ciLintPath', 'projectFullPath', 'validateTabIllustrationPath'],
+ props: {
+ ciFileContent: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ initialBlobContent: {
+ query: getBlobContent,
+ variables() {
+ return {
+ projectPath: this.projectFullPath,
+ path: this.ciConfigPath,
+ ref: this.currentBranch,
+ };
+ },
+ update(data) {
+ return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
+ },
+ },
+ currentBranch: {
+ query: getCurrentBranch,
+ update(data) {
+ return data.workBranches?.current?.name;
+ },
+ },
+ },
+ data() {
+ return {
+ yaml: this.ciFileContent,
+ state: VALIDATE_TAB_INIT,
+ errors: [],
+ hasCiContentChanged: false,
+ isValid: false,
+ jobs: [],
+ warnings: [],
+ };
+ },
+ computed: {
+ isInitialCiContentLoading() {
+ return this.$apollo.queries.initialBlobContent.loading;
+ },
+ isInitState() {
+ return this.state === VALIDATE_TAB_INIT;
+ },
+ isSimulationLoading() {
+ return this.state === VALIDATE_TAB_LOADING;
+ },
+ hasSimulationResults() {
+ return this.state === VALIDATE_TAB_RESULTS;
+ },
+ resultStatus() {
+ return {
+ title: this.isValid ? i18n.successAlertTitle : i18n.errorAlertTitle,
+ variant: this.isValid ? 'success' : 'danger',
+ };
+ },
+ },
+ watch: {
+ ciFileContent(value) {
+ this.yaml = value;
+ this.hasCiContentChanged = true;
+ },
+ },
+ methods: {
+ cancelSimulation() {
+ this.state = VALIDATE_TAB_INIT;
+ },
+ async validateYaml() {
+ this.state = VALIDATE_TAB_LOADING;
+
+ try {
+ const {
+ data: {
+ lintCI: { errors, jobs, valid, warnings },
+ },
+ } = await this.$apollo.mutate({
+ mutation: lintCiMutation,
+ variables: {
+ dry_run: true,
+ content: this.yaml,
+ endpoint: this.ciLintPath,
+ },
+ });
+
+ // only save the result if the user did not cancel the simulation
+ if (this.state === VALIDATE_TAB_LOADING) {
+ this.errors = errors;
+ this.jobs = jobs;
+ this.warnings = warnings;
+ this.isValid = valid;
+ this.state = VALIDATE_TAB_RESULTS;
+ this.hasCiContentChanged = false;
+ }
+ } catch (error) {
+ this.cancelSimulation();
+ }
+ },
+ },
i18n,
+ BASE_CLASSES,
};
</script>
<template>
<div>
- <div class="gl-mt-3">
- <label>{{ $options.i18n.pipelineSource }}</label>
- <gl-dropdown
- v-gl-tooltip.hover
- :title="$options.i18n.pipelineSourceTooltip"
- :text="$options.i18n.pipelineSourceDefault"
- disabled
- data-testid="pipeline-source"
- />
- <validate-pipeline-popover />
- <gl-icon
- id="validate-pipeline-help"
- name="question-o"
- class="gl-ml-1 gl-fill-blue-500"
- category="secondary"
- variant="confirm"
- :aria-label="$options.i18n.help"
- />
+ <div class="gl-display-flex gl-justify-content-space-between gl-mt-3">
+ <div>
+ <label>{{ $options.i18n.pipelineSource }}</label>
+ <gl-dropdown
+ v-gl-tooltip.hover
+ class="gl-ml-3"
+ :title="$options.i18n.pipelineSourceTooltip"
+ :text="$options.i18n.pipelineSourceDefault"
+ disabled
+ data-testid="pipeline-source"
+ />
+ <validate-pipeline-popover />
+ <gl-icon
+ id="validate-pipeline-help"
+ name="question-o"
+ class="gl-ml-1 gl-fill-blue-500"
+ category="secondary"
+ variant="confirm"
+ :aria-label="$options.i18n.help"
+ />
+ </div>
+ <div v-if="hasSimulationResults && hasCiContentChanged">
+ <span class="gl-text-gray-400" data-testid="content-status">
+ {{ $options.i18n.contentChange }}
+ </span>
+ <gl-button
+ variant="confirm"
+ class="gl-ml-2 gl-mb-2"
+ data-testid="resimulate-pipeline-button"
+ @click="validateYaml"
+ >
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
</div>
- <div class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-mt-11">
+ <div v-if="isInitState" :class="$options.BASE_CLASSES">
<img :src="validateTabIllustrationPath" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
<ul>
@@ -69,9 +225,61 @@ export default {
</gl-sprintf>
</li>
</ul>
- <gl-button variant="confirm" class="gl-mt-3" data-qa-selector="simulate_pipeline">
- {{ $options.i18n.cta }}
- </gl-button>
+ <div ref="simulatePipelineButton">
+ <gl-button
+ ref="simulatePipelineButton"
+ variant="confirm"
+ class="gl-mt-3"
+ :disabled="isInitialCiContentLoading"
+ data-testid="simulate-pipeline-button"
+ @click="validateYaml"
+ >
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
+ <gl-tooltip
+ v-if="isInitialCiContentLoading"
+ :target="() => $refs.simulatePipelineButton"
+ :title="$options.i18n.ctaDisabledTooltip"
+ data-testid="cta-tooltip"
+ />
+ </div>
+ <div v-else-if="isSimulationLoading" :class="$options.BASE_CLASSES">
+ <gl-loading-icon size="lg" class="gl-m-3" />
+ <h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.loading }}</h1>
+ <div>
+ <gl-button class="gl-mt-3" data-testid="cancel-simulation" @click="cancelSimulation">
+ {{ $options.i18n.cancelBtn }}
+ </gl-button>
+ <gl-button class="gl-mt-3" loading data-testid="simulate-pipeline-button">
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
+ </div>
+ <div v-else-if="hasSimulationResults" class="gl-mt-5">
+ <gl-alert
+ class="gl-mb-5"
+ :dismissible="false"
+ :title="resultStatus.title"
+ :variant="resultStatus.variant"
+ >
+ <gl-sprintf :message="$options.i18n.alertDesc">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ <template #link="{ content }">
+ <gl-link target="_blank" href="#">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+ <ci-lint-results
+ dry-run
+ hide-alert
+ :is-valid="isValid"
+ :jobs="jobs"
+ :errors="errors"
+ :warnings="warnings"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index b056b086d57..4f5b69107bf 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -27,6 +27,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
+ ciLintPath,
defaultBranch,
emptyStateIllustrationPath,
helpPaths,
@@ -116,6 +117,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
+ ciLintPath,
configurationPaths,
dataMethod: 'graphql',
defaultBranch,
diff --git a/app/assets/javascripts/pipelines/stores/test_reports/constants.js b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
index 8eebfb6b208..83d14e1a109 100644
--- a/app/assets/javascripts/pipelines/stores/test_reports/constants.js
+++ b/app/assets/javascripts/pipelines/stores/test_reports/constants.js
@@ -1 +1 @@
-export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts have expired';
+export const ARTIFACTS_EXPIRED_ERROR_MESSAGE = 'Test report artifacts not found';
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 96ca9fbcb43..2e1bb9b9eac 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -209,7 +209,6 @@ table.pipeline-project-metrics tr td {
}
.title {
- margin-top: -$gl-padding-8; // negative margin required for flex-wrap
font-size: $gl-font-size;
}
diff --git a/app/controllers/oauth/authorizations_controller.rb b/app/controllers/oauth/authorizations_controller.rb
index c9c51289d3a..2e9fbb1d0d9 100644
--- a/app/controllers/oauth/authorizations_controller.rb
+++ b/app/controllers/oauth/authorizations_controller.rb
@@ -54,8 +54,6 @@ class Oauth::AuthorizationsController < Doorkeeper::AuthorizationsController
# limit scopes when signing in with GitLab
def downgrade_scopes!
- return unless Feature.enabled?(:omniauth_login_minimal_scopes, current_user)
-
auth_type = params.delete('gl_auth_type')
return unless auth_type == 'login'
diff --git a/app/controllers/projects/google_cloud/base_controller.rb b/app/controllers/projects/google_cloud/base_controller.rb
index 980e9bdcdad..050b26a40c7 100644
--- a/app/controllers/projects/google_cloud/base_controller.rb
+++ b/app/controllers/projects/google_cloud/base_controller.rb
@@ -12,7 +12,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def admin_project_google_cloud!
unless can?(current_user, :admin_project_google_cloud, project)
- track_event('admin_project_google_cloud!', 'access_denied', 'invalid_user')
+ track_event('admin_project_google_cloud!', 'error_access_denied', 'invalid_user')
access_denied!
end
end
@@ -20,7 +20,11 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
def google_oauth2_enabled!
config = Gitlab::Auth::OAuth::Provider.config_for('google_oauth2')
if config.app_id.blank? || config.app_secret.blank?
- track_event('google_oauth2_enabled!', 'access_denied', { reason: 'google_oauth2_not_configured', config: config })
+ track_event(
+ 'google_oauth2_enabled!',
+ 'error_access_denied',
+ { reason: 'google_oauth2_not_configured', config: config }
+ )
access_denied! 'This GitLab instance not configured for Google Oauth2.'
end
end
@@ -31,7 +35,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
enabled_for_project = Feature.enabled?(:incubation_5mp_google_cloud, project)
feature_is_enabled = enabled_for_user || enabled_for_group || enabled_for_project
unless feature_is_enabled
- track_event('feature_flag_enabled!', 'access_denied', 'feature_flag_not_enabled')
+ track_event('feature_flag_enabled!', 'error_access_denied', 'feature_flag_not_enabled')
access_denied!
end
end
@@ -42,7 +46,7 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
return if is_token_valid
- return_url = project_google_cloud_index_path(project)
+ return_url = project_google_cloud_configuration_path(project)
state = generate_session_key_redirect(request.url, return_url)
@authorize_url = GoogleApi::CloudPlatform::Client.new(nil,
callback_google_api_auth_url,
@@ -65,12 +69,6 @@ class Projects::GoogleCloud::BaseController < Projects::ApplicationController
session[GoogleApi::CloudPlatform::Client.session_key_for_expires_at]
end
- def handle_gcp_error(action, error)
- track_event(action, 'gcp_error', error)
- @js_data = { screen: 'gcp_error', error: error.to_s }.to_json
- render status: :unauthorized, template: 'projects/google_cloud/errors/gcp_error'
- end
-
def track_event(action, label, property)
options = { label: label, project: project, user: current_user }
diff --git a/app/controllers/projects/google_cloud/configuration_controller.rb b/app/controllers/projects/google_cloud/configuration_controller.rb
new file mode 100644
index 00000000000..fa672058247
--- /dev/null
+++ b/app/controllers/projects/google_cloud/configuration_controller.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Projects
+ module GoogleCloud
+ class ConfigurationController < Projects::GoogleCloud::BaseController
+ def index
+ @google_cloud_path = project_google_cloud_configuration_path(project)
+ js_data = {
+ configurationUrl: project_google_cloud_configuration_path(project),
+ deploymentsUrl: project_google_cloud_deployments_path(project),
+ databasesUrl: project_google_cloud_databases_path(project),
+ serviceAccounts: ::GoogleCloud::ServiceAccountsService.new(project).find_for_project,
+ createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
+ emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
+ configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
+ gcpRegions: gcp_regions,
+ revokeOauthUrl: revoke_oauth_url
+ }
+ @js_data = js_data.to_json
+ track_event('configuration#index', 'success', js_data)
+ end
+
+ private
+
+ def gcp_regions
+ params = { key: Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY }
+ list = ::Ci::VariablesFinder.new(project, params).execute
+ list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
+ end
+
+ def revoke_oauth_url
+ google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
+ .validate_token(expires_at_in_session)
+ google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/google_cloud/databases_controller.rb b/app/controllers/projects/google_cloud/databases_controller.rb
new file mode 100644
index 00000000000..711409e7550
--- /dev/null
+++ b/app/controllers/projects/google_cloud/databases_controller.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Projects
+ module GoogleCloud
+ class DatabasesController < Projects::GoogleCloud::BaseController
+ def index
+ @google_cloud_path = project_google_cloud_configuration_path(project)
+ js_data = {
+ configurationUrl: project_google_cloud_configuration_path(project),
+ deploymentsUrl: project_google_cloud_deployments_path(project),
+ databasesUrl: project_google_cloud_databases_path(project)
+ }
+ @js_data = js_data.to_json
+ track_event('databases#index', 'success', js_data)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/google_cloud/deployments_controller.rb b/app/controllers/projects/google_cloud/deployments_controller.rb
index 1ad3511fbc0..4aa17b36fad 100644
--- a/app/controllers/projects/google_cloud/deployments_controller.rb
+++ b/app/controllers/projects/google_cloud/deployments_controller.rb
@@ -3,32 +3,47 @@
class Projects::GoogleCloud::DeploymentsController < Projects::GoogleCloud::BaseController
before_action :validate_gcp_token!
+ def index
+ @google_cloud_path = project_google_cloud_configuration_path(project)
+ js_data = {
+ configurationUrl: project_google_cloud_configuration_path(project),
+ deploymentsUrl: project_google_cloud_deployments_path(project),
+ databasesUrl: project_google_cloud_databases_path(project),
+ enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
+ enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project)
+ }
+ @js_data = js_data.to_json
+ track_event('deployments#index', 'success', js_data)
+ end
+
def cloud_run
params = { google_oauth2_token: token_in_session }
enable_cloud_run_response = GoogleCloud::EnableCloudRunService
.new(project, current_user, params).execute
if enable_cloud_run_response[:status] == :error
- track_event('deployments#cloud_run', 'enable_cloud_run_error', enable_cloud_run_response)
+ track_event('deployments#cloud_run', 'error_enable_cloud_run', enable_cloud_run_response)
flash[:error] = enable_cloud_run_response[:message]
- redirect_to project_google_cloud_index_path(project)
+ redirect_to project_google_cloud_deployments_path(project)
else
params = { action: GoogleCloud::GeneratePipelineService::ACTION_DEPLOY_TO_CLOUD_RUN }
generate_pipeline_response = GoogleCloud::GeneratePipelineService
.new(project, current_user, params).execute
if generate_pipeline_response[:status] == :error
- track_event('deployments#cloud_run', 'generate_pipeline_error', generate_pipeline_response)
+ track_event('deployments#cloud_run', 'error_generate_pipeline', generate_pipeline_response)
flash[:error] = 'Failed to generate pipeline'
- redirect_to project_google_cloud_index_path(project)
+ redirect_to project_google_cloud_deployments_path(project)
else
cloud_run_mr_params = cloud_run_mr_params(generate_pipeline_response[:branch_name])
- track_event('deployments#cloud_run', 'cloud_run_success', cloud_run_mr_params)
+ track_event('deployments#cloud_run', 'success', cloud_run_mr_params)
redirect_to project_new_merge_request_path(project, merge_request: cloud_run_mr_params)
end
end
- rescue Google::Apis::ClientError => error
- handle_gcp_error('deployments#cloud_run', error)
+ rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
+ track_event('deployments#cloud_run', 'error_gcp', error)
+ flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
+ redirect_to project_google_cloud_deployments_path(project)
end
def cloud_storage
diff --git a/app/controllers/projects/google_cloud/gcp_regions_controller.rb b/app/controllers/projects/google_cloud/gcp_regions_controller.rb
index beeb91cfd80..3fbe9a96284 100644
--- a/app/controllers/projects/google_cloud/gcp_regions_controller.rb
+++ b/app/controllers/projects/google_cloud/gcp_regions_controller.rb
@@ -6,8 +6,10 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
# Source https://cloud.google.com/run/docs/locations 2022-01-30
AVAILABLE_REGIONS = %w[asia-east1 asia-northeast1 asia-southeast1 europe-north1 europe-west1 europe-west4 us-central1 us-east1 us-east4 us-west1].freeze
+ GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
+
def index
- @google_cloud_path = project_google_cloud_index_path(project)
+ @google_cloud_path = project_google_cloud_configuration_path(project)
params = { per_page: 50 }
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
tags = TagsFinder.new(project.repository, params).execute(gitaly_pagination: true)
@@ -16,16 +18,16 @@ class Projects::GoogleCloud::GcpRegionsController < Projects::GoogleCloud::BaseC
screen: 'gcp_regions_form',
availableRegions: AVAILABLE_REGIONS,
refs: refs,
- cancelPath: project_google_cloud_index_path(project)
+ cancelPath: project_google_cloud_configuration_path(project)
}
@js_data = js_data.to_json
- track_event('gcp_regions#index', 'form_render', js_data)
+ track_event('gcp_regions#index', 'success', js_data)
end
def create
permitted_params = params.permit(:ref, :gcp_region)
response = GoogleCloud::GcpRegionAddOrReplaceService.new(project).execute(permitted_params[:ref], permitted_params[:gcp_region])
- track_event('gcp_regions#create', 'form_submit', response)
- redirect_to project_google_cloud_index_path(project), notice: _('GCP region configured')
+ track_event('gcp_regions#create', 'success', response)
+ redirect_to project_google_cloud_configuration_path(project), notice: _('GCP region configured')
end
end
diff --git a/app/controllers/projects/google_cloud/revoke_oauth_controller.rb b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb
index 03d1474707b..1a9a2daf4f2 100644
--- a/app/controllers/projects/google_cloud/revoke_oauth_controller.rb
+++ b/app/controllers/projects/google_cloud/revoke_oauth_controller.rb
@@ -8,16 +8,15 @@ class Projects::GoogleCloud::RevokeOauthController < Projects::GoogleCloud::Base
response = google_api_client.revoke_authorizations
if response.success?
- status = 'success'
redirect_message = { notice: s_('GoogleCloud|Google OAuth2 token revocation requested') }
+ track_event('revoke_oauth#create', 'success', response.to_json)
else
- status = 'failed'
redirect_message = { alert: s_('GoogleCloud|Google OAuth2 token revocation request failed') }
+ track_event('revoke_oauth#create', 'error', response.to_json)
end
session.delete(GoogleApi::CloudPlatform::Client.session_key_for_token)
- track_event('revoke_oauth#create', 'create', status)
- redirect_to project_google_cloud_index_path(project), redirect_message
+ redirect_to project_google_cloud_configuration_path(project), redirect_message
end
end
diff --git a/app/controllers/projects/google_cloud/service_accounts_controller.rb b/app/controllers/projects/google_cloud/service_accounts_controller.rb
index 5d8b2030d5c..dbd83be19db 100644
--- a/app/controllers/projects/google_cloud/service_accounts_controller.rb
+++ b/app/controllers/projects/google_cloud/service_accounts_controller.rb
@@ -4,14 +4,15 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
before_action :validate_gcp_token!
def index
- @google_cloud_path = project_google_cloud_index_path(project)
+ @google_cloud_path = project_google_cloud_configuration_path(project)
google_api_client = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
gcp_projects = google_api_client.list_projects
if gcp_projects.empty?
@js_data = { screen: 'no_gcp_projects' }.to_json
- track_event('service_accounts#index', 'form_error', 'no_gcp_projects')
- render status: :unauthorized, template: 'projects/google_cloud/errors/no_gcp_projects'
+ track_event('service_accounts#index', 'error_form', 'no_gcp_projects')
+ flash[:warning] = _('No Google Cloud projects - You need at least one Google Cloud project')
+ redirect_to project_google_cloud_configuration_path(project)
else
params = { per_page: 50 }
branches = BranchesFinder.new(project.repository, params).execute(gitaly_pagination: true)
@@ -21,14 +22,16 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
screen: 'service_accounts_form',
gcpProjects: gcp_projects,
refs: refs,
- cancelPath: project_google_cloud_index_path(project)
+ cancelPath: project_google_cloud_configuration_path(project)
}
@js_data = js_data.to_json
- track_event('service_accounts#index', 'form_success', js_data)
+ track_event('service_accounts#index', 'success', js_data)
end
- rescue Google::Apis::ClientError => error
- handle_gcp_error('service_accounts#index', error)
+ rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
+ track_event('service_accounts#index', 'error_gcp', error)
+ flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
+ redirect_to project_google_cloud_configuration_path(project)
end
def create
@@ -42,9 +45,11 @@ class Projects::GoogleCloud::ServiceAccountsController < Projects::GoogleCloud::
environment_name: permitted_params[:ref]
).execute
- track_event('service_accounts#create', 'form_submit', response)
- redirect_to project_google_cloud_index_path(project), notice: response.message
+ track_event('service_accounts#create', 'success', response)
+ redirect_to project_google_cloud_configuration_path(project), notice: response.message
rescue Google::Apis::ClientError, Google::Apis::ServerError, Google::Apis::AuthorizationError => error
- handle_gcp_error('service_accounts#create', error)
+ track_event('service_accounts#create', 'error_gcp', error)
+ flash[:warning] = _('Google Cloud Error - %{error}') % { error: error }
+ redirect_to project_google_cloud_configuration_path(project)
end
end
diff --git a/app/controllers/projects/google_cloud_controller.rb b/app/controllers/projects/google_cloud_controller.rb
deleted file mode 100644
index 49bb4bec859..00000000000
--- a/app/controllers/projects/google_cloud_controller.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-class Projects::GoogleCloudController < Projects::GoogleCloud::BaseController
- GCP_REGION_CI_VAR_KEY = 'GCP_REGION'
-
- def index
- js_data = {
- screen: 'home',
- serviceAccounts: GoogleCloud::ServiceAccountsService.new(project).find_for_project,
- createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
- enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
- enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
- emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
- configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
- gcpRegions: gcp_regions,
- revokeOauthUrl: revoke_oauth_url
- }
- @js_data = js_data.to_json
- track_event('google_cloud#index', 'index', js_data)
- end
-
- private
-
- def gcp_regions
- list = ::Ci::VariablesFinder.new(project, { key: GCP_REGION_CI_VAR_KEY }).execute
- list.map { |variable| { gcp_region: variable.value, environment: variable.environment_scope } }
- end
-
- def revoke_oauth_url
- google_token_valid = GoogleApi::CloudPlatform::Client.new(token_in_session, nil)
- .validate_token(expires_at_in_session)
- google_token_valid ? project_google_cloud_revoke_oauth_index_path(project) : nil
- end
-end
diff --git a/app/controllers/projects/pipelines/tests_controller.rb b/app/controllers/projects/pipelines/tests_controller.rb
index 20bdb4d3af1..e42cb9b8422 100644
--- a/app/controllers/projects/pipelines/tests_controller.rb
+++ b/app/controllers/projects/pipelines/tests_controller.rb
@@ -35,7 +35,7 @@ module Projects
def validate_test_reports!
unless pipeline.has_test_reports?
- render json: { errors: 'Test report artifacts have expired' }, status: :not_found
+ render json: { errors: 'Test report artifacts not found' }, status: :not_found
end
end
diff --git a/app/helpers/ci/pipeline_editor_helper.rb b/app/helpers/ci/pipeline_editor_helper.rb
index a3f1a540581..d044a93213a 100644
--- a/app/helpers/ci/pipeline_editor_helper.rb
+++ b/app/helpers/ci/pipeline_editor_helper.rb
@@ -18,6 +18,7 @@ module Ci
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
+ "ci-lint-path" => project_ci_lint_path(project),
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => image_path('illustrations/empty-state/empty-dag-md.svg'),
"initial-branch-name" => initial_branch,
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index ded6ab8687a..0f13c45b84d 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -57,7 +57,22 @@ class CustomerRelations::Contact < ApplicationRecord
end
def self.sort_by_name
- order("last_name ASC, first_name ASC")
+ order(Gitlab::Pagination::Keyset::Order.build([
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'last_name',
+ order_expression: arel_table[:last_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'first_name',
+ order_expression: arel_table[:first_name].asc,
+ distinct: false
+ ),
+ Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table[:id].asc
+ )
+ ]))
end
def self.find_ids_by_emails(group, emails)
diff --git a/app/models/issue.rb b/app/models/issue.rb
index c529f41e8cd..cae42115bef 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -124,8 +124,24 @@ class Issue < ApplicationRecord
scope :order_due_date_desc, -> { reorder(arel_table[:due_date].desc.nulls_last) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
- scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
- scope :order_severity_desc, -> { includes(:issuable_severity).order('issuable_severities.severity DESC NULLS LAST') }
+ scope :order_severity_asc, -> do
+ build_keyset_order_on_joined_column(
+ scope: includes(:issuable_severity),
+ attribute_name: 'issuable_severities_severity',
+ column: IssuableSeverity.arel_table[:severity],
+ direction: :asc,
+ nullable: :nulls_first
+ )
+ end
+ scope :order_severity_desc, -> do
+ build_keyset_order_on_joined_column(
+ scope: includes(:issuable_severity),
+ attribute_name: 'issuable_severities_severity',
+ column: IssuableSeverity.arel_table[:severity],
+ direction: :desc,
+ nullable: :nulls_last
+ )
+ end
scope :order_escalation_status_asc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].asc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_escalation_status_desc, -> { includes(:incident_management_issuable_escalation_status).order(IncidentManagement::IssuableEscalationStatus.arel_table[:status].desc.nulls_last).references(:incident_management_issuable_escalation_status) }
scope :order_closed_at_asc, -> { reorder(arel_table[:closed_at].asc.nulls_last) }
@@ -234,6 +250,31 @@ class Issue < ApplicationRecord
alias_method :with_state, :with_state_id
alias_method :with_states, :with_state_ids
+ def build_keyset_order_on_joined_column(scope:, attribute_name:, column:, direction:, nullable:)
+ reversed_direction = direction == :asc ? :desc : :asc
+
+ # rubocop: disable GitlabSecurity/PublicSend
+ order = ::Gitlab::Pagination::Keyset::Order.build([
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: attribute_name,
+ column_expression: column,
+ order_expression: column.send(direction).send(nullable),
+ reversed_order_expression: column.send(reversed_direction).send(nullable),
+ order_direction: direction,
+ distinct: false,
+ add_to_projections: true,
+ nullable: nullable
+ ),
+ ::Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
+ attribute_name: 'id',
+ order_expression: arel_table['id'].desc
+ )
+ ])
+ # rubocop: enable GitlabSecurity/PublicSend
+
+ order.apply_cursor_conditions(scope).order(order)
+ end
+
override :order_upvotes_desc
def order_upvotes_desc
reorder(upvotes_count: :desc)
@@ -331,10 +372,10 @@ class Issue < ApplicationRecord
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
when 'relative_position', 'relative_position_asc' then order_by_relative_position
- when 'severity_asc' then order_severity_asc.with_order_id_desc
- when 'severity_desc' then order_severity_desc.with_order_id_desc
- when 'escalation_status_asc' then order_escalation_status_asc.with_order_id_desc
- when 'escalation_status_desc' then order_escalation_status_desc.with_order_id_desc
+ when 'severity_asc' then order_severity_asc
+ when 'severity_desc' then order_severity_desc
+ when 'escalation_status_asc' then order_escalation_status_asc
+ when 'escalation_status_desc' then order_escalation_status_desc
when 'closed_at', 'closed_at_asc' then order_closed_at_asc
when 'closed_at_desc' then order_closed_at_desc
else
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index e9fd7e4446c..59d2e3deb4f 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -5,6 +5,8 @@ class ProjectSetting < ApplicationRecord
belongs_to :project, inverse_of: :project_setting
+ scope :for_projects, ->(projects) { where(project_id: projects) }
+
enum squash_option: {
never: 0,
always: 1,
diff --git a/app/services/google_cloud/gcp_region_add_or_replace_service.rb b/app/services/google_cloud/gcp_region_add_or_replace_service.rb
index 6edf7726719..f79df707a08 100644
--- a/app/services/google_cloud/gcp_region_add_or_replace_service.rb
+++ b/app/services/google_cloud/gcp_region_add_or_replace_service.rb
@@ -3,7 +3,7 @@
module GoogleCloud
class GcpRegionAddOrReplaceService < ::GoogleCloud::BaseService
def execute(environment, region)
- gcp_region_key = Projects::GoogleCloudController::GCP_REGION_CI_VAR_KEY
+ gcp_region_key = Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY
change_params = { variable_params: { key: gcp_region_key, value: region, environment_scope: environment } }
filter_params = { key: gcp_region_key, filter: { environment_scope: environment } }
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index a0021ae2ccb..29e3a9473ab 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -162,6 +162,12 @@ module Groups
projects_to_update
.update_all(visibility_level: @new_parent_group.visibility_level)
+
+ update_project_settings(@updated_project_ids)
+ end
+
+ # Overridden in EE
+ def update_project_settings(updated_project_ids)
end
def update_two_factor_authentication
diff --git a/app/views/groups/_shared_projects.html.haml b/app/views/groups/_shared_projects.html.haml
index ef6410ad439..6063d160fab 100644
--- a/app/views/groups/_shared_projects.html.haml
+++ b/app/views/groups/_shared_projects.html.haml
@@ -3,5 +3,5 @@
%p= _("There are no projects shared with this group yet")
%ul.content-list{ data: { hide_projects: 'false', group_id: group.id, path: group_path(group) } }
- .js-groups-list-holder
+ .js-groups-list-holder{ data: { current_group_visibility: group.visibility } }
= gl_loading_icon
diff --git a/app/views/projects/google_cloud/configuration/index.html.haml b/app/views/projects/google_cloud/configuration/index.html.haml
new file mode 100644
index 00000000000..ec977898f47
--- /dev/null
+++ b/app/views/projects/google_cloud/configuration/index.html.haml
@@ -0,0 +1,7 @@
+- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
+- breadcrumb_title s_('CloudSeed|Configuration')
+- page_title s_('CloudSeed|Configuration')
+
+- @content_class = "limit-container-width" unless fluid_layout
+
+#js-google-cloud-configuration{ data: @js_data }
diff --git a/app/views/projects/google_cloud/databases/index.html.haml b/app/views/projects/google_cloud/databases/index.html.haml
new file mode 100644
index 00000000000..ad732317d8d
--- /dev/null
+++ b/app/views/projects/google_cloud/databases/index.html.haml
@@ -0,0 +1,7 @@
+- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
+- breadcrumb_title s_('CloudSeed|Databases')
+- page_title s_('CloudSeed|Databases')
+
+- @content_class = "limit-container-width" unless fluid_layout
+
+#js-google-cloud-databases{ data: @js_data }
diff --git a/app/views/projects/google_cloud/deployments/index.html.haml b/app/views/projects/google_cloud/deployments/index.html.haml
new file mode 100644
index 00000000000..b140159a7f5
--- /dev/null
+++ b/app/views/projects/google_cloud/deployments/index.html.haml
@@ -0,0 +1,7 @@
+- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
+- breadcrumb_title s_('CloudSeed|Deployments')
+- page_title s_('CloudSeed|Deployments')
+
+- @content_class = "limit-container-width" unless fluid_layout
+
+#js-google-cloud-deployments{ data: @js_data }
diff --git a/app/views/projects/google_cloud/errors/gcp_error.html.haml b/app/views/projects/google_cloud/errors/gcp_error.html.haml
deleted file mode 100644
index 69e481501d5..00000000000
--- a/app/views/projects/google_cloud/errors/gcp_error.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- breadcrumb_title _('Google Cloud')
-- page_title _('Google Cloud')
-
-- @content_class = "limit-container-width" unless fluid_layout
-
-#js-google-cloud{ data: @js_data }
diff --git a/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml b/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml
deleted file mode 100644
index 69e481501d5..00000000000
--- a/app/views/projects/google_cloud/errors/no_gcp_projects.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- breadcrumb_title _('Google Cloud')
-- page_title _('Google Cloud')
-
-- @content_class = "limit-container-width" unless fluid_layout
-
-#js-google-cloud{ data: @js_data }
diff --git a/app/views/projects/google_cloud/gcp_regions/index.html.haml b/app/views/projects/google_cloud/gcp_regions/index.html.haml
index 3a6f8ca059d..d7cabaa029b 100644
--- a/app/views/projects/google_cloud/gcp_regions/index.html.haml
+++ b/app/views/projects/google_cloud/gcp_regions/index.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
-- breadcrumb_title _('Regions')
-- page_title _('Regions')
+- breadcrumb_title _('CloudSeed|Regions')
+- page_title s_('CloudSeed|Regions')
- @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_gcp_regions_path(@project), method: 'post' do
- #js-google-cloud{ data: @js_data }
+ #js-google-cloud-gcp-regions{ data: @js_data }
diff --git a/app/views/projects/google_cloud/index.html.haml b/app/views/projects/google_cloud/index.html.haml
deleted file mode 100644
index 69e481501d5..00000000000
--- a/app/views/projects/google_cloud/index.html.haml
+++ /dev/null
@@ -1,6 +0,0 @@
-- breadcrumb_title _('Google Cloud')
-- page_title _('Google Cloud')
-
-- @content_class = "limit-container-width" unless fluid_layout
-
-#js-google-cloud{ data: @js_data }
diff --git a/app/views/projects/google_cloud/service_accounts/index.html.haml b/app/views/projects/google_cloud/service_accounts/index.html.haml
index 9b82bc0acb5..6191de577fe 100644
--- a/app/views/projects/google_cloud/service_accounts/index.html.haml
+++ b/app/views/projects/google_cloud/service_accounts/index.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs _('Google Cloud'), @google_cloud_path
-- breadcrumb_title _('Service Account')
-- page_title _('Service Account')
+- breadcrumb_title s_('CloudSeed|Service Account')
+- page_title s_('CloudSeed|Service Account')
- @content_class = "limit-container-width" unless fluid_layout
= form_tag project_google_cloud_service_accounts_path(@project), method: 'post' do
- #js-google-cloud{ data: @js_data }
+ #js-google-cloud-service-accounts{ data: @js_data }
diff --git a/config/feature_flags/development/omniauth_login_minimal_scopes.yml b/config/feature_flags/ops/enforce_memory_watchdog.yml
index b2ca3484a98..1d1f9a4eef0 100644
--- a/config/feature_flags/development/omniauth_login_minimal_scopes.yml
+++ b/config/feature_flags/ops/enforce_memory_watchdog.yml
@@ -1,8 +1,8 @@
---
-name: omniauth_login_minimal_scopes
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/78556
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/351331
-milestone: '14.8'
-type: development
-group: 'group::authentication and authorization'
+name: enforce_memory_watchdog
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91910
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367534
+milestone: '15.2'
+type: ops
+group: group::memory
default_enabled: false
diff --git a/config/feature_flags/ops/gitlab_memory_watchdog.yml b/config/feature_flags/ops/gitlab_memory_watchdog.yml
new file mode 100644
index 00000000000..9b995ea607e
--- /dev/null
+++ b/config/feature_flags/ops/gitlab_memory_watchdog.yml
@@ -0,0 +1,8 @@
+---
+name: gitlab_memory_watchdog
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/91910
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/367534
+milestone: '15.2'
+type: ops
+group: group::memory
+default_enabled: false
diff --git a/config/initializers/memory_watchdog.rb b/config/initializers/memory_watchdog.rb
new file mode 100644
index 00000000000..72779a18b10
--- /dev/null
+++ b/config/initializers/memory_watchdog.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+return unless Gitlab::Runtime.application?
+return unless Gitlab::Utils.to_boolean(ENV['GITLAB_MEMORY_WATCHDOG_ENABLED'])
+
+Gitlab::Cluster::LifecycleEvents.on_worker_start do
+ handler =
+ if Gitlab::Runtime.puma?
+ Gitlab::Memory::Watchdog::PumaHandler.new
+ elsif Gitlab::Runtime.sidekiq?
+ Gitlab::Memory::Watchdog::TermProcessHandler.new
+ else
+ Gitlab::Memory::Watchdog::NullHandler.instance
+ end
+
+ Gitlab::Memory::Watchdog.new(
+ handler: handler, logger: Gitlab::AppLogger
+ ).start
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index ba4dba47f4c..5eb0b9396c9 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -298,15 +298,18 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :terraform, only: [:index]
- resources :google_cloud, only: [:index]
-
namespace :google_cloud do
+ get '/configuration', to: 'configuration#index'
+
resources :revoke_oauth, only: [:create]
resources :service_accounts, only: [:index, :create]
resources :gcp_regions, only: [:index, :create]
+ get '/deployments', to: 'deployments#index'
get '/deployments/cloud_run', to: 'deployments#cloud_run'
get '/deployments/cloud_storage', to: 'deployments#cloud_storage'
+
+ get '/databases', to: 'databases#index'
end
resources :environments, except: [:destroy] do
diff --git a/doc/integration/gitlab.md b/doc/integration/gitlab.md
index 68ba676b539..02705d9dec3 100644
--- a/doc/integration/gitlab.md
+++ b/doc/integration/gitlab.md
@@ -117,10 +117,9 @@ signed in.
## Reduce access privileges on sign in
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337663) in GitLab 14.8 [with a flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. Disabled by default.
-
-FLAG:
-On self-managed GitLab, by default this feature is not available. To make it available, ask an administrator to [enable the feature flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. On GitLab.com, this feature is not available.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/337663) in GitLab 14.8 [with a flag](../administration/feature_flags.md) named `omniauth_login_minimal_scopes`. Disabled by default.
+> - [Enabled on GitLab.com](https://gitlab.com/gitlab-org/gitlab/-/issues/351331) in GitLab 14.9.
+> - [Feature flag `omniauth_login_minimal_scopes`](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/83453) removed in GitLab 15.2
If you use a GitLab instance for authentication, you can reduce access rights when an OAuth application is used for sign in.
diff --git a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
index d8401d5a286..fad75ca6cab 100644
--- a/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
+++ b/doc/user/infrastructure/clusters/connect/new_civo_cluster.md
@@ -64,7 +64,7 @@ Use CI/CD environment variables to configure your project.
1. On the left sidebar, select **Settings > CI/CD**.
1. Expand **Variables**.
-1. Set the variable `BASE64_CIVO_CREDENTIALS` to the [token](https://www.civo.com/account/security) from your Civo account.
+1. Set the variable `BASE64_CIVO_TOKEN` to the [token](https://www.civo.com/account/security) from your Civo account.
1. Set the variable `TF_VAR_agent_token` to the agent token you received in the previous task.
1. Set the variable `TF_VAR_kas_address` to the agent server address in the previous task.
@@ -78,8 +78,8 @@ contains other variables that you can override according to your needs:
- `TF_VAR_civo_region`: Set your cluster's region.
- `TF_VAR_cluster_name`: Set your cluster's name.
- `TF_VAR_cluster_description`: Set a description for the cluster. To create a reference to your GitLab project on your Civo cluster detail page, set this value to `$CI_PROJECT_URL`. This value helps you determine which project was responsible for provisioning the cluster you see on the Civo dashboard.
-- `TF_VAR_machine_type`: Set the machine type for the Kubernetes nodes.
-- `TF_VAR_node_count`: Set the number of Kubernetes nodes.
+- `TF_VAR_target_nodes_size`: Set the size of the nodes to use for the cluster
+- `TF_VAR_num_target_nodes`: Set the number of Kubernetes nodes.
- `TF_VAR_agent_version`: Set the version of the GitLab agent.
- `TF_VAR_agent_namespace`: Set the Kubernetes namespace for the GitLab agent.
diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md
index 84c98a60917..c863a9d8270 100644
--- a/doc/user/usage_quotas.md
+++ b/doc/user/usage_quotas.md
@@ -10,11 +10,57 @@ info: To determine the technical writer assigned to the Stage/Group associated w
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/13294) in GitLab 12.0.
> - Moved to GitLab Free.
-NOTE:
-Free tier namespaces on GitLab SaaS have a 5GB storage limit. This limit is not visible on the storage quota page nor currently enforced for users who exceed the limit. To learn more, visit our [pricing page](https://about.gitlab.com/pricing/).
+## Namespace storage limit
-A project's repository has a free storage quota of 10 GB. When a project's repository reaches
-the quota it is locked. You cannot push changes to a locked project. To monitor the size of each
+Namespaces on a GitLab SaaS Free tier have a 5 GB storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/).
+This limit is not visible on the storage quota page, but we plan to make it visible and enforced starting October 19, 2022.
+
+Storage types that add to the total namespace storage are:
+
+- Git repository
+- Git LFS
+- Artifacts
+- Container registry
+- Package registry
+- Dependecy proxy
+- Wiki
+- Snippets
+
+If your total namespace storage exceeds the available namespace storage quota, all projects under the namespace are locked. A locked project will not be able to push to the repository, run pipelines and jobs, or build and push packages.
+
+To prevent exceeding the namespace storage quota, you can:
+
+1. [Purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer).
+1. [Upgrade to a paid tier](../subscriptions/gitlab_com/#upgrade-your-gitlab-saas-subscription-tier).
+1. [Reduce storage usage](#manage-your-storage-usage).
+
+### Namespace storage limit enforcement schedule
+
+Starting October 19, 2022, a storage limit will be enforced on all GitLab Free namespaces.
+We will start with a large limit enforcement and eventually reduce it to 5 GB.
+
+The following table describes the enforcement schedule, which is subject to change.
+
+| Target enforcement date | Limit | Expected Impact | Status |
+| ------ | ------ | ------ | ------ |
+| October 19, 2022 | 45,000 GB | LOW | Pending (**{hourglass}**)|
+| October 20, 2022 | 7,500 GB | LOW | Pending (**{hourglass}**)|
+| October 24, 2022 | 500 GB | MEDIUM | Pending (**{hourglass}**)|
+| October 27, 2022 | 75 GB | MEDIUM HIGH | Pending (**{hourglass}**)|
+| November 2, 2022 | 10 GB | HIGH | Pending (**{hourglass}**)|
+| November 9, 2022 | 5 GB | VERY HIGH | Pending (**{hourglass}**)|
+
+Namespaces that reach the enforced limit will have their projects locked. To unlock your project, you will have to [manage its storage](#manage-your-storage-usage).
+
+### Project storage limit
+
+Namespaces on a GitLab SaaS **paid** tier (Premium and Ultimate) have a storage limit on their project repositories.
+A project's repository has a storage quota of 10 GB. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both.
+
+- Paid tier namespaces have project-level storage limits enforced.
+- Free tier namespaces have namespace-level storage limits.
+
+When a project's repository reaches the quota, the project is locked. You cannot push changes to a locked project. To monitor the size of each
repository in a namespace, including a breakdown for each project, you can
[view storage usage](#view-storage-usage). To allow a project's repository to exceed the free quota
you must purchase additional storage. For more details, see [Excess storage usage](#excess-storage-usage).
diff --git a/lib/gitlab/memory/watchdog.rb b/lib/gitlab/memory/watchdog.rb
new file mode 100644
index 00000000000..db75ba8a47d
--- /dev/null
+++ b/lib/gitlab/memory/watchdog.rb
@@ -0,0 +1,192 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Memory
+ # A background thread that observes Ruby heap fragmentation and calls
+ # into a handler when the Ruby heap has been fragmented for an extended
+ # period of time.
+ #
+ # See Gitlab::Metrics::Memory for how heap fragmentation is defined.
+ #
+ # To decide whether a given fragmentation level is being exceeded,
+ # the watchdog regularly polls the GC. Whenever a violation occurs
+ # a strike is issued. If the maximum number of strikes are reached,
+ # a handler is invoked to deal with the situation.
+ #
+ # The duration for which a process may be above a given fragmentation
+ # threshold is computed as `max_strikes * sleep_time_seconds`.
+ class Watchdog < Daemon
+ DEFAULT_SLEEP_TIME_SECONDS = 60
+ DEFAULT_HEAP_FRAG_THRESHOLD = 0.5
+ DEFAULT_MAX_STRIKES = 5
+
+ # This handler does nothing. It returns `false` to indicate to the
+ # caller that the situation has not been dealt with so it will
+ # receive calls repeatedly if fragmentation remains high.
+ #
+ # This is useful for "dress rehearsals" in production since it allows
+ # us to observe how frequently the handler is invoked before taking action.
+ class NullHandler
+ include Singleton
+
+ def on_high_heap_fragmentation(value)
+ # NOP
+ false
+ end
+ end
+
+ # This handler sends SIGTERM and considers the situation handled.
+ class TermProcessHandler
+ def initialize(pid = $$)
+ @pid = pid
+ end
+
+ def on_high_heap_fragmentation(value)
+ Process.kill(:TERM, @pid)
+ true
+ end
+ end
+
+ # This handler invokes Puma's graceful termination handler, which takes
+ # into account a configurable grace period during which a process may
+ # remain unresponsive to a SIGTERM.
+ class PumaHandler
+ def initialize(puma_options = ::Puma.cli_config.options)
+ @worker = ::Puma::Cluster::WorkerHandle.new(0, $$, 0, puma_options)
+ end
+
+ def on_high_heap_fragmentation(value)
+ @worker.term
+ true
+ end
+ end
+
+ # max_heap_fragmentation:
+ # The degree to which the Ruby heap is allowed to be fragmented. Range [0,1].
+ # max_strikes:
+ # How many times the process is allowed to be above max_heap_fragmentation before
+ # a handler is invoked.
+ # sleep_time_seconds:
+ # Used to control the frequency with which the watchdog will wake up and poll the GC.
+ def initialize(
+ handler: NullHandler.instance,
+ logger: Logger.new($stdout),
+ max_heap_fragmentation: ENV['GITLAB_MEMWD_MAX_HEAP_FRAG']&.to_f || DEFAULT_HEAP_FRAG_THRESHOLD,
+ max_strikes: ENV['GITLAB_MEMWD_MAX_STRIKES']&.to_i || DEFAULT_MAX_STRIKES,
+ sleep_time_seconds: ENV['GITLAB_MEMWD_SLEEP_TIME_SEC']&.to_i || DEFAULT_SLEEP_TIME_SECONDS,
+ **options)
+ super(**options)
+
+ @handler = handler
+ @logger = logger
+ @max_heap_fragmentation = max_heap_fragmentation
+ @sleep_time_seconds = sleep_time_seconds
+ @max_strikes = max_strikes
+
+ @alive = true
+ @strikes = 0
+
+ init_prometheus_metrics(max_heap_fragmentation)
+ end
+
+ attr_reader :strikes, :max_heap_fragmentation, :max_strikes, :sleep_time_seconds
+
+ def run_thread
+ @logger.info(log_labels.merge(message: 'started'))
+
+ while @alive
+ sleep(@sleep_time_seconds)
+
+ monitor_heap_fragmentation if Feature.enabled?(:gitlab_memory_watchdog, type: :ops)
+ end
+
+ @logger.info(log_labels.merge(message: 'stopped'))
+ end
+
+ private
+
+ def monitor_heap_fragmentation
+ heap_fragmentation = Gitlab::Metrics::Memory.gc_heap_fragmentation
+
+ if heap_fragmentation > @max_heap_fragmentation
+ @strikes += 1
+ @heap_frag_violations.increment
+ else
+ @strikes = 0
+ end
+
+ if @strikes > @max_strikes
+ # If the handler returns true, it means the event is handled and we can shut down.
+ @alive = !handle_heap_fragmentation_limit_exceeded(heap_fragmentation)
+ @strikes = 0
+ end
+ end
+
+ def handle_heap_fragmentation_limit_exceeded(value)
+ @logger.warn(
+ log_labels.merge(
+ message: 'heap fragmentation limit exceeded',
+ memwd_cur_heap_frag: value
+ ))
+ @heap_frag_violations_handled.increment
+
+ handler.on_high_heap_fragmentation(value)
+ end
+
+ def handler
+ # This allows us to keep the watchdog running but turn it into "friendly mode" where
+ # all that happens is we collect logs and Prometheus events for fragmentation violations.
+ return NullHandler.instance unless Feature.enabled?(:enforce_memory_watchdog, type: :ops)
+
+ @handler
+ end
+
+ def stop_working
+ @alive = false
+ end
+
+ def log_labels
+ {
+ pid: $$,
+ worker_id: worker_id,
+ memwd_handler_class: handler.class.name,
+ memwd_sleep_time_s: @sleep_time_seconds,
+ memwd_max_heap_frag: @max_heap_fragmentation,
+ memwd_max_strikes: @max_strikes,
+ memwd_cur_strikes: @strikes,
+ memwd_rss_bytes: process_rss_bytes
+ }
+ end
+
+ def worker_id
+ ::Prometheus::PidProvider.worker_id
+ end
+
+ def process_rss_bytes
+ Gitlab::Metrics::System.memory_usage_rss
+ end
+
+ def init_prometheus_metrics(max_heap_fragmentation)
+ default_labels = { pid: worker_id }
+
+ @heap_frag_limit = Gitlab::Metrics.gauge(
+ :gitlab_memwd_heap_frag_limit,
+ 'The configured limit for how fragmented the Ruby heap is allowed to be',
+ default_labels
+ )
+ @heap_frag_limit.set({}, max_heap_fragmentation)
+
+ @heap_frag_violations = Gitlab::Metrics.counter(
+ :gitlab_memwd_heap_frag_violations_total,
+ 'Total number of times heap fragmentation in a Ruby process exceeded its allowed maximum',
+ default_labels
+ )
+ @heap_frag_violations_handled = Gitlab::Metrics.counter(
+ :gitlab_memwd_heap_frag_violations_handled_total,
+ 'Total number of times heap fragmentation violations in a Ruby process were handled',
+ default_labels
+ )
+ end
+ end
+ end
+end
diff --git a/lib/sidebars/projects/menus/infrastructure_menu.rb b/lib/sidebars/projects/menus/infrastructure_menu.rb
index a98cc20d51a..1c04a7b117d 100644
--- a/lib/sidebars/projects/menus/infrastructure_menu.rb
+++ b/lib/sidebars/projects/menus/infrastructure_menu.rb
@@ -88,8 +88,14 @@ module Sidebars
::Sidebars::MenuItem.new(
title: _('Google Cloud'),
- link: project_google_cloud_index_path(context.project),
- active_routes: { controller: [:google_cloud, :service_accounts, :deployments, :gcp_regions] },
+ link: project_google_cloud_configuration_path(context.project),
+ active_routes: { controller: [
+ :configuration,
+ :service_accounts,
+ :databases,
+ :deployments,
+ :gcp_regions
+ ] },
item_id: :google_cloud
)
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index df30e10058f..035792e23df 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8318,6 +8318,9 @@ msgstr ""
msgid "CloudSeed|CloudSQL Instance"
msgstr ""
+msgid "CloudSeed|Configuration"
+msgstr ""
+
msgid "CloudSeed|Create cluster"
msgstr ""
@@ -8336,6 +8339,12 @@ msgstr ""
msgid "CloudSeed|Database version"
msgstr ""
+msgid "CloudSeed|Databases"
+msgstr ""
+
+msgid "CloudSeed|Deployments"
+msgstr ""
+
msgid "CloudSeed|Description"
msgstr ""
@@ -8393,12 +8402,18 @@ msgstr ""
msgid "CloudSeed|Refs"
msgstr ""
+msgid "CloudSeed|Regions"
+msgstr ""
+
msgid "CloudSeed|Scalable, secure, and highly available in-memory service for Redis"
msgstr ""
msgid "CloudSeed|Service"
msgstr ""
+msgid "CloudSeed|Service Account"
+msgstr ""
+
msgid "CloudSeed|Services"
msgstr ""
@@ -10662,9 +10677,6 @@ msgstr ""
msgid "Create %{workspace} label"
msgstr ""
-msgid "Create Google Cloud project"
-msgstr ""
-
msgid "Create New Directory"
msgstr ""
@@ -17494,9 +17506,6 @@ msgstr ""
msgid "GitLab account request rejected"
msgstr ""
-msgid "GitLab and Google Cloud configuration seems to be incomplete. This probably can be fixed by your GitLab administration team. You may share these logs with them:"
-msgstr ""
-
msgid "GitLab commit"
msgstr ""
@@ -17983,16 +17992,13 @@ msgstr ""
msgid "Google Cloud"
msgstr ""
-msgid "Google Cloud Project"
-msgstr ""
-
-msgid "Google Cloud authorizations required"
+msgid "Google Cloud Error - %{error}"
msgstr ""
-msgid "Google Cloud project misconfigured"
+msgid "Google Cloud Project"
msgstr ""
-msgid "Google Cloud project required"
+msgid "Google Cloud authorizations required"
msgstr ""
msgid "GoogleCloud|Cancel"
@@ -23204,6 +23210,9 @@ msgstr ""
msgid "Less Details"
msgstr ""
+msgid "Less restrictive visibility"
+msgstr ""
+
msgid "Let's Encrypt does not accept emails on example.com"
msgstr ""
@@ -25982,6 +25991,9 @@ msgstr ""
msgid "No Epic"
msgstr ""
+msgid "No Google Cloud projects - You need at least one Google Cloud project"
+msgstr ""
+
msgid "No Matching Results"
msgstr ""
@@ -28378,6 +28390,9 @@ msgstr ""
msgid "PipelineEditorTutorial|🚀 Run your first pipeline"
msgstr ""
+msgid "PipelineEditor|Configuration content has changed. Re-run validation for updated results."
+msgstr ""
+
msgid "PipelineEditor|Current content in the Edit tab will be used for the simulation."
msgstr ""
@@ -28396,6 +28411,15 @@ msgstr ""
msgid "PipelineEditor|Pipeline behavior will be simulated including the %{codeStart}rules%{codeEnd} %{codeStart}only%{codeEnd} %{codeStart}except%{codeEnd} and %{codeStart}needs%{codeEnd} job dependencies. %{linkStart}Learn more%{linkEnd}"
msgstr ""
+msgid "PipelineEditor|Pipeline simulation completed with errors"
+msgstr ""
+
+msgid "PipelineEditor|Simulated a %{codeStart}git push%{codeEnd} event for a default branch. %{codeStart}Rules%{codeEnd}, %{codeStart}only%{codeEnd}, %{codeStart}except%{codeEnd}, and %{codeStart}needs%{codeEnd} job dependencies logic have been evaluated. %{linkStart}Learn more%{linkEnd}"
+msgstr ""
+
+msgid "PipelineEditor|Simulation completed successfully"
+msgstr ""
+
msgid "PipelineEditor|The CI/CD configuration is continuously validated. Errors and warnings are displayed when the CI/CD configuration file is not empty."
msgstr ""
@@ -28417,6 +28441,12 @@ msgstr ""
msgid "PipelineEditor|Validate pipeline under simulated conditions"
msgstr ""
+msgid "PipelineEditor|Validating pipeline... It can take up to a minute."
+msgstr ""
+
+msgid "PipelineEditor|Waiting for CI content to load..."
+msgstr ""
+
msgid "PipelineScheduleIntervalPattern|Custom (%{linkStart}Cron syntax%{linkEnd})"
msgstr ""
@@ -28696,6 +28726,9 @@ msgstr ""
msgid "Pipelines|Pipeline Editor"
msgstr ""
+msgid "Pipelines|Pipeline syntax is correct."
+msgstr ""
+
msgid "Pipelines|Project cache successfully reset."
msgstr ""
@@ -30151,6 +30184,9 @@ msgstr ""
msgid "Project uploads"
msgstr ""
+msgid "Project visibility level is less restrictive than the group settings."
+msgstr ""
+
msgid "Project visibility level will be changed to match namespace rules when transferring to a group."
msgstr ""
@@ -35588,9 +35624,6 @@ msgstr ""
msgid "ServicePing|Turn on service ping to review instance-level analytics."
msgstr ""
-msgid "Services"
-msgstr ""
-
msgid "Session ID"
msgstr ""
@@ -41343,9 +41376,6 @@ msgstr ""
msgid "Unknown response text"
msgstr ""
-msgid "Unknown screen"
-msgstr ""
-
msgid "Unknown user"
msgstr ""
@@ -44417,9 +44447,6 @@ msgstr ""
msgid "You currently have more than %{free_limit} members across all your personal projects. From June 22, 2022, the %{free_limit} most recently active members will remain active, and the remaining members will get a %{link_start}status of Over limit%{link_end} and lose access. To view and manage members, check the members page for each project in your namespace. We recommend you %{move_link_start}move your project to a group%{move_link_end} so you can easily manage users and features."
msgstr ""
-msgid "You do not have any Google Cloud projects. Please create a Google Cloud project and then reload this page."
-msgstr ""
-
msgid "You do not have any subscriptions yet"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
index 23f212e110b..c58865fff03 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
@@ -50,7 +50,7 @@ module QA
it 'shows valid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349128' do
Page::Project::PipelineEditor::Show.perform do |show|
aggregate_failures do
- expect(show.ci_syntax_validate_message).to have_content('CI configuration is valid')
+ expect(show.ci_syntax_validate_message).to have_content('Pipeline syntax is correct')
show.go_to_visualize_tab
{ stage1: 'job1', stage2: 'job2' }.each_pair do |stage, job|
diff --git a/scripts/review_apps/review-apps.sh b/scripts/review_apps/review-apps.sh
index 8a868edce3a..b6ac7b4281b 100755
--- a/scripts/review_apps/review-apps.sh
+++ b/scripts/review_apps/review-apps.sh
@@ -248,6 +248,7 @@ function download_chart() {
helm repo add gitlab https://charts.gitlab.io
echoinfo "Building the gitlab chart's dependencies..."
+ helm dependency build "gitlab-${GITLAB_HELM_CHART_REF}"
}
function base_config_changed() {
diff --git a/spec/controllers/projects/pipelines/tests_controller_spec.rb b/spec/controllers/projects/pipelines/tests_controller_spec.rb
index deb96cc5bf9..ddcab8b048e 100644
--- a/spec/controllers/projects/pipelines/tests_controller_spec.rb
+++ b/spec/controllers/projects/pipelines/tests_controller_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Projects::Pipelines::TestsController do
get_tests_show_json(build_ids)
expect(response).to have_gitlab_http_status(:not_found)
- expect(json_response['errors']).to eq('Test report artifacts have expired')
+ expect(json_response['errors']).to eq('Test report artifacts not found')
end
end
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index fa8db1befb5..9a1e216c6d2 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -97,6 +97,31 @@ RSpec.describe 'Group show page' do
end
end
+ context 'when a public project is shared with a private group' do
+ let_it_be(:private_group) { create(:group, :private) }
+ let_it_be(:public_project) { create(:project, :public) }
+ let_it_be(:project_group_link) { create(:project_group_link, group: private_group, project: public_project) }
+
+ before do
+ private_group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'shows warning popover', :js do
+ visit group_path(private_group)
+
+ click_link _('Shared projects')
+
+ wait_for_requests
+
+ page.within("[data-testid=\"group-overview-item-#{public_project.id}\"]") do
+ click_button _('Less restrictive visibility')
+ end
+
+ expect(page).to have_content _('Project visibility level is less restrictive than the group settings.')
+ end
+ end
+
context 'when user does not have permissions to create new subgroups or projects', :js do
before do
group.add_reporter(user)
diff --git a/spec/frontend/ci_lint/mock_data.js b/spec/frontend/ci_lint/mock_data.js
index 28ea0f55bf8..660b2ad6e8b 100644
--- a/spec/frontend/ci_lint/mock_data.js
+++ b/spec/frontend/ci_lint/mock_data.js
@@ -1,5 +1,16 @@
import { mockJobs } from 'jest/pipeline_editor/mock_data';
+export const mockLintDataError = {
+ data: {
+ lintCI: {
+ errors: ['Error message'],
+ warnings: ['Warning message'],
+ valid: false,
+ jobs: mockJobs,
+ },
+ },
+};
+
export const mockLintDataValid = {
data: {
lintCI: {
diff --git a/spec/frontend/google_cloud/components/app_spec.js b/spec/frontend/google_cloud/components/app_spec.js
deleted file mode 100644
index 0cafe6d3b9d..00000000000
--- a/spec/frontend/google_cloud/components/app_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { mapValues } from 'lodash';
-import App from '~/google_cloud/components/app.vue';
-import Home from '~/google_cloud/components/home.vue';
-import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
-import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
-import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
-import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
-
-const BASE_FEEDBACK_URL =
- 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new';
-const SCREEN_COMPONENTS = {
- Home,
- ServiceAccountsForm,
- GcpError,
- NoGcpProjects,
-};
-const SERVICE_ACCOUNTS_FORM_PROPS = {
- gcpProjects: [1, 2, 3],
- refs: [4, 5, 6],
- cancelPath: '',
-};
-const HOME_PROPS = {
- serviceAccounts: [{}, {}],
- gcpRegions: [{}, {}],
- createServiceAccountUrl: '#url-create-service-account',
- configureGcpRegionsUrl: '#url-configure-gcp-regions',
- emptyIllustrationUrl: '#url-empty-illustration',
- enableCloudRunUrl: '#url-enable-cloud-run',
- enableCloudStorageUrl: '#enableCloudStorageUrl',
- revokeOauthUrl: '#revokeOauthUrl',
-};
-
-describe('google_cloud App component', () => {
- let wrapper;
-
- const findIncubationBanner = () => wrapper.findComponent(IncubationBanner);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe.each`
- screen | extraProps | componentName
- ${'gcp_error'} | ${{ error: 'mock_gcp_client_error' }} | ${'GcpError'}
- ${'no_gcp_projects'} | ${{}} | ${'NoGcpProjects'}
- ${'service_accounts_form'} | ${SERVICE_ACCOUNTS_FORM_PROPS} | ${'ServiceAccountsForm'}
- ${'home'} | ${HOME_PROPS} | ${'Home'}
- `('for screen=$screen', ({ screen, extraProps, componentName }) => {
- const component = SCREEN_COMPONENTS[componentName];
-
- beforeEach(() => {
- wrapper = shallowMount(App, { propsData: { screen, ...extraProps } });
- });
-
- it(`renders only ${componentName}`, () => {
- const existences = mapValues(SCREEN_COMPONENTS, (x) => wrapper.findComponent(x).exists());
-
- expect(existences).toEqual({
- ...mapValues(SCREEN_COMPONENTS, () => false),
- [componentName]: true,
- });
- });
-
- it(`renders the ${componentName} with props`, () => {
- expect(wrapper.findComponent(component).props()).toEqual(extraProps);
- });
-
- it('renders incubation banner', () => {
- expect(findIncubationBanner().props()).toEqual({
- shareFeedbackUrl: `${BASE_FEEDBACK_URL}?issuable_template=general_feedback`,
- reportBugUrl: `${BASE_FEEDBACK_URL}?issuable_template=report_bug`,
- featureRequestUrl: `${BASE_FEEDBACK_URL}?issuable_template=feature_request`,
- });
- });
- });
-});
diff --git a/spec/frontend/google_cloud/components/errors/gcp_error_spec.js b/spec/frontend/google_cloud/components/errors/gcp_error_spec.js
deleted file mode 100644
index 4062a8b902a..00000000000
--- a/spec/frontend/google_cloud/components/errors/gcp_error_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAlert } from '@gitlab/ui';
-import GcpError from '~/google_cloud/components/errors/gcp_error.vue';
-
-describe('GcpError component', () => {
- let wrapper;
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findBlockquote = () => wrapper.find('blockquote');
-
- const propsData = { error: 'IAM and CloudResourceManager API disabled' };
-
- beforeEach(() => {
- wrapper = shallowMount(GcpError, { propsData });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('contains alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('contains relevant text', () => {
- const alertText = findAlert().text();
- expect(findAlert().props('title')).toBe(GcpError.i18n.title);
- expect(alertText).toContain(GcpError.i18n.description);
- });
-
- it('contains error stacktrace', () => {
- expect(findBlockquote().text()).toBe(propsData.error);
- });
-});
diff --git a/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js b/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js
deleted file mode 100644
index e1e20377880..00000000000
--- a/spec/frontend/google_cloud/components/errors/no_gcp_projects_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { GlAlert, GlButton } from '@gitlab/ui';
-import NoGcpProjects from '~/google_cloud/components/errors/no_gcp_projects.vue';
-
-describe('NoGcpProjects component', () => {
- let wrapper;
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- wrapper = mount(NoGcpProjects);
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('contains alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('contains relevant text', () => {
- expect(findAlert().props('title')).toBe(NoGcpProjects.i18n.title);
- expect(findAlert().text()).toContain(NoGcpProjects.i18n.description);
- });
-
- it('contains create gcp project button', () => {
- const button = findButton();
- expect(button.text()).toBe(NoGcpProjects.i18n.createLabel);
- expect(button.attributes('href')).toBe('https://console.cloud.google.com/projectcreate');
- });
-});
diff --git a/spec/frontend/google_cloud/components/google_cloud_menu_spec.js b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
new file mode 100644
index 00000000000..4809ea37045
--- /dev/null
+++ b/spec/frontend/google_cloud/components/google_cloud_menu_spec.js
@@ -0,0 +1,40 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+
+describe('google_cloud/components/google_cloud_menu', () => {
+ let wrapper;
+
+ const props = {
+ active: 'configuration',
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ };
+
+ beforeEach(() => {
+ wrapper = mountExtended(GoogleCloudMenu, { propsData: props });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains active configuration link', () => {
+ const link = wrapper.findByTestId('configurationLink');
+ expect(link.text()).toBe(GoogleCloudMenu.i18n.configuration.title);
+ expect(link.attributes('href')).toBe(props.configurationUrl);
+ expect(link.element.classList.contains('gl-tab-nav-item-active')).toBe(true);
+ });
+
+ it('contains deployments link', () => {
+ const link = wrapper.findByTestId('deploymentsLink');
+ expect(link.text()).toBe(GoogleCloudMenu.i18n.deployments.title);
+ expect(link.attributes('href')).toBe(props.deploymentsUrl);
+ });
+
+ it('contains databases link', () => {
+ const link = wrapper.findByTestId('databasesLink');
+ expect(link.text()).toBe(GoogleCloudMenu.i18n.databases.title);
+ expect(link.attributes('href')).toBe(props.databasesUrl);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/home_spec.js b/spec/frontend/google_cloud/components/home_spec.js
deleted file mode 100644
index 42e3d72577d..00000000000
--- a/spec/frontend/google_cloud/components/home_spec.js
+++ /dev/null
@@ -1,66 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlTab, GlTabs } from '@gitlab/ui';
-import Home from '~/google_cloud/components/home.vue';
-import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
-
-describe('google_cloud Home component', () => {
- let wrapper;
-
- const findTabs = () => wrapper.findComponent(GlTabs);
- const findTabItems = () => findTabs().findAllComponents(GlTab);
- const findTabItemsModel = () =>
- findTabs()
- .findAllComponents(GlTab)
- .wrappers.map((x) => ({
- title: x.attributes('title'),
- disabled: x.attributes('disabled'),
- }));
-
- const TEST_HOME_PROPS = {
- serviceAccounts: [{}, {}],
- gcpRegions: [{}, {}],
- createServiceAccountUrl: '#url-create-service-account',
- configureGcpRegionsUrl: '#url-configure-gcp-regions',
- emptyIllustrationUrl: '#url-empty-illustration',
- enableCloudRunUrl: '#url-enable-cloud-run',
- enableCloudStorageUrl: '#enableCloudStorageUrl',
- revokeOauthUrl: '#revokeOauthUrl',
- };
-
- beforeEach(() => {
- const propsData = {
- screen: 'home',
- ...TEST_HOME_PROPS,
- };
- wrapper = shallowMount(Home, { propsData });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('google_cloud App tabs', () => {
- it('should contain tabs', () => {
- expect(findTabs().exists()).toBe(true);
- });
-
- it('should contain three tab items', () => {
- expect(findTabItemsModel()).toEqual([
- { title: 'Configuration', disabled: undefined },
- { title: 'Deployments', disabled: undefined },
- { title: 'Services', disabled: '' },
- ]);
- });
-
- describe('configuration tab', () => {
- it('should contain service accounts component', () => {
- const serviceAccounts = findTabItems().at(0).findComponent(ServiceAccountsList);
- expect(serviceAccounts.props()).toEqual({
- list: TEST_HOME_PROPS.serviceAccounts,
- createUrl: TEST_HOME_PROPS.createServiceAccountUrl,
- emptyIllustrationUrl: TEST_HOME_PROPS.emptyIllustrationUrl,
- });
- });
- });
- });
-});
diff --git a/spec/frontend/google_cloud/components/incubation_banner_spec.js b/spec/frontend/google_cloud/components/incubation_banner_spec.js
index 89517be4ef1..09a4d92dca2 100644
--- a/spec/frontend/google_cloud/components/incubation_banner_spec.js
+++ b/spec/frontend/google_cloud/components/incubation_banner_spec.js
@@ -2,7 +2,7 @@ import { mount } from '@vue/test-utils';
import { GlAlert, GlLink } from '@gitlab/ui';
import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
-describe('IncubationBanner component', () => {
+describe('google_cloud/components/incubation_banner', () => {
let wrapper;
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -12,12 +12,7 @@ describe('IncubationBanner component', () => {
const findShareFeedbackLink = () => findLinks().at(2);
beforeEach(() => {
- const propsData = {
- shareFeedbackUrl: 'url_general_feedback',
- reportBugUrl: 'url_report_bug',
- featureRequestUrl: 'url_feature_request',
- };
- wrapper = mount(IncubationBanner, { propsData });
+ wrapper = mount(IncubationBanner);
});
afterEach(() => {
@@ -41,20 +36,26 @@ describe('IncubationBanner component', () => {
it('contains feature request link', () => {
const link = findFeatureRequestLink();
+ const expected =
+ 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=feature_request';
expect(link.text()).toBe('request a feature');
- expect(link.attributes('href')).toBe('url_feature_request');
+ expect(link.attributes('href')).toBe(expected);
});
it('contains report bug link', () => {
const link = findReportBugLink();
+ const expected =
+ 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=report_bug';
expect(link.text()).toBe('report a bug');
- expect(link.attributes('href')).toBe('url_report_bug');
+ expect(link.attributes('href')).toBe(expected);
});
it('contains share feedback link', () => {
const link = findShareFeedbackLink();
+ const expected =
+ 'https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/feedback/-/issues/new?issuable_template=general_feedback';
expect(link.text()).toBe('share feedback');
- expect(link.attributes('href')).toBe('url_general_feedback');
+ expect(link.attributes('href')).toBe(expected);
});
});
});
diff --git a/spec/frontend/google_cloud/components/revoke_oauth_spec.js b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
index 87580dbf6de..faaec07fc35 100644
--- a/spec/frontend/google_cloud/components/revoke_oauth_spec.js
+++ b/spec/frontend/google_cloud/components/revoke_oauth_spec.js
@@ -5,7 +5,7 @@ import RevokeOauth, {
GOOGLE_CLOUD_REVOKE_DESCRIPTION,
} from '~/google_cloud/components/revoke_oauth.vue';
-describe('RevokeOauth component', () => {
+describe('google_cloud/components/revoke_oauth', () => {
let wrapper;
const findTitle = () => wrapper.find('h2');
diff --git a/spec/frontend/google_cloud/configuration/panel_spec.js b/spec/frontend/google_cloud/configuration/panel_spec.js
new file mode 100644
index 00000000000..79eb4cb4918
--- /dev/null
+++ b/spec/frontend/google_cloud/configuration/panel_spec.js
@@ -0,0 +1,65 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Panel from '~/google_cloud/configuration/panel.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
+import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue';
+import RevokeOauth from '~/google_cloud/components/revoke_oauth.vue';
+
+describe('google_cloud/configuration/panel', () => {
+ let wrapper;
+
+ const props = {
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ serviceAccounts: [],
+ createServiceAccountUrl: 'create-service-account-url',
+ emptyIllustrationUrl: 'empty-illustration-url',
+ gcpRegions: [],
+ configureGcpRegionsUrl: 'configure-gcp-regions-url',
+ revokeOauthUrl: 'revoke-oauth-url',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(Panel, { propsData: props });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains incubation banner', () => {
+ const target = wrapper.findComponent(IncubationBanner);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains google cloud menu with `configuration` active', () => {
+ const target = wrapper.findComponent(GoogleCloudMenu);
+ expect(target.exists()).toBe(true);
+ expect(target.props('active')).toBe('configuration');
+ expect(target.props('configurationUrl')).toBe(props.configurationUrl);
+ expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
+ expect(target.props('databasesUrl')).toBe(props.databasesUrl);
+ });
+
+ it('contains service accounts list', () => {
+ const target = wrapper.findComponent(ServiceAccountsList);
+ expect(target.exists()).toBe(true);
+ expect(target.props('list')).toBe(props.serviceAccounts);
+ expect(target.props('createUrl')).toBe(props.createServiceAccountUrl);
+ expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl);
+ });
+
+ it('contains gcp regions list', () => {
+ const target = wrapper.findComponent(GcpRegionsList);
+ expect(target.props('list')).toBe(props.gcpRegions);
+ expect(target.props('createUrl')).toBe(props.configureGcpRegionsUrl);
+ expect(target.props('emptyIllustrationUrl')).toBe(props.emptyIllustrationUrl);
+ });
+
+ it('contains revoke oauth', () => {
+ const target = wrapper.findComponent(RevokeOauth);
+ expect(target.props('url')).toBe(props.revokeOauthUrl);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
index de644a33b50..48e4b0ca1ad 100644
--- a/spec/frontend/google_cloud/components/cloudsql/create_instance_form_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/create_instance_form_spec.js
@@ -1,8 +1,8 @@
import { GlFormCheckbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import InstanceForm from '~/google_cloud/components/cloudsql/create_instance_form.vue';
+import InstanceForm from '~/google_cloud/databases/cloudsql/create_instance_form.vue';
-describe('google_cloud::cloudsql::create_instance_form component', () => {
+describe('google_cloud/databases/cloudsql/create_instance_form', () => {
let wrapper;
const findByTestId = (id) => wrapper.findByTestId(id);
diff --git a/spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
index 286f2b8e379..a5736d0a524 100644
--- a/spec/frontend/google_cloud/components/cloudsql/instance_table_spec.js
+++ b/spec/frontend/google_cloud/databases/cloudsql/instance_table_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlTable } from '@gitlab/ui';
-import InstanceTable from '~/google_cloud/components/cloudsql/instance_table.vue';
+import InstanceTable from '~/google_cloud/databases/cloudsql/instance_table.vue';
-describe('google_cloud::databases::service_table component', () => {
+describe('google_cloud/databases/cloudsql/instance_table', () => {
let wrapper;
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
diff --git a/spec/frontend/google_cloud/databases/panel_spec.js b/spec/frontend/google_cloud/databases/panel_spec.js
new file mode 100644
index 00000000000..490c0136651
--- /dev/null
+++ b/spec/frontend/google_cloud/databases/panel_spec.js
@@ -0,0 +1,36 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Panel from '~/google_cloud/databases/panel.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+
+describe('google_cloud/databases/panel', () => {
+ let wrapper;
+
+ const props = {
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(Panel, { propsData: props });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains incubation banner', () => {
+ const target = wrapper.findComponent(IncubationBanner);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains google cloud menu with `databases` active', () => {
+ const target = wrapper.findComponent(GoogleCloudMenu);
+ expect(target.exists()).toBe(true);
+ expect(target.props('active')).toBe('databases');
+ expect(target.props('configurationUrl')).toBe(props.configurationUrl);
+ expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
+ expect(target.props('databasesUrl')).toBe(props.databasesUrl);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/databases/service_table_spec.js b/spec/frontend/google_cloud/databases/service_table_spec.js
index 142e32c1a4b..4a622e544e1 100644
--- a/spec/frontend/google_cloud/components/databases/service_table_spec.js
+++ b/spec/frontend/google_cloud/databases/service_table_spec.js
@@ -1,8 +1,8 @@
import { GlTable } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import ServiceTable from '~/google_cloud/components/databases/service_table.vue';
+import ServiceTable from '~/google_cloud/databases/service_table.vue';
-describe('google_cloud::databases::service_table component', () => {
+describe('google_cloud/databases/service_table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
diff --git a/spec/frontend/google_cloud/deployments/panel_spec.js b/spec/frontend/google_cloud/deployments/panel_spec.js
new file mode 100644
index 00000000000..729db1707a7
--- /dev/null
+++ b/spec/frontend/google_cloud/deployments/panel_spec.js
@@ -0,0 +1,46 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Panel from '~/google_cloud/deployments/panel.vue';
+import IncubationBanner from '~/google_cloud/components/incubation_banner.vue';
+import GoogleCloudMenu from '~/google_cloud/components/google_cloud_menu.vue';
+import ServiceTable from '~/google_cloud/deployments/service_table.vue';
+
+describe('google_cloud/deployments/panel', () => {
+ let wrapper;
+
+ const props = {
+ configurationUrl: 'configuration-url',
+ deploymentsUrl: 'deployments-url',
+ databasesUrl: 'databases-url',
+ enableCloudRunUrl: 'cloud-run-url',
+ enableCloudStorageUrl: 'cloud-storage-url',
+ };
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(Panel, { propsData: props });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('contains incubation banner', () => {
+ const target = wrapper.findComponent(IncubationBanner);
+ expect(target.exists()).toBe(true);
+ });
+
+ it('contains google cloud menu with `deployments` active', () => {
+ const target = wrapper.findComponent(GoogleCloudMenu);
+ expect(target.exists()).toBe(true);
+ expect(target.props('active')).toBe('deployments');
+ expect(target.props('configurationUrl')).toBe(props.configurationUrl);
+ expect(target.props('deploymentsUrl')).toBe(props.deploymentsUrl);
+ expect(target.props('databasesUrl')).toBe(props.databasesUrl);
+ });
+
+ it('contains service-table', () => {
+ const target = wrapper.findComponent(ServiceTable);
+ expect(target.exists()).toBe(true);
+ expect(target.props('cloudRunUrl')).toBe(props.enableCloudRunUrl);
+ expect(target.props('cloudStorageUrl')).toBe(props.enableCloudStorageUrl);
+ });
+});
diff --git a/spec/frontend/google_cloud/components/deployments_service_table_spec.js b/spec/frontend/google_cloud/deployments/service_table_spec.js
index 882376547c4..8faad64e313 100644
--- a/spec/frontend/google_cloud/components/deployments_service_table_spec.js
+++ b/spec/frontend/google_cloud/deployments/service_table_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { GlButton, GlTable } from '@gitlab/ui';
-import DeploymentsServiceTable from '~/google_cloud/components/deployments_service_table.vue';
+import DeploymentsServiceTable from '~/google_cloud/deployments/service_table.vue';
-describe('google_cloud DeploymentsServiceTable component', () => {
+describe('google_cloud/deployments/service_table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
diff --git a/spec/frontend/google_cloud/components/gcp_regions_form_spec.js b/spec/frontend/google_cloud/gcp_regions/form_spec.js
index a8b7593e7c8..1030e9c8a18 100644
--- a/spec/frontend/google_cloud/components/gcp_regions_form_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/form_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect } from '@gitlab/ui';
-import GcpRegionsForm from '~/google_cloud/components/gcp_regions_form.vue';
+import GcpRegionsForm from '~/google_cloud/gcp_regions/form.vue';
-describe('GcpRegionsForm component', () => {
+describe('google_cloud/gcp_regions/form', () => {
let wrapper;
const findHeader = () => wrapper.find('header');
diff --git a/spec/frontend/google_cloud/components/gcp_regions_list_spec.js b/spec/frontend/google_cloud/gcp_regions/list_spec.js
index ab0c17451e8..6d8c389e5a1 100644
--- a/spec/frontend/google_cloud/components/gcp_regions_list_spec.js
+++ b/spec/frontend/google_cloud/gcp_regions/list_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
-import GcpRegionsList from '~/google_cloud/components/gcp_regions_list.vue';
+import GcpRegionsList from '~/google_cloud/gcp_regions/list.vue';
-describe('GcpRegions component', () => {
+describe('google_cloud/gcp_regions/list', () => {
describe('when the project does not have any configured regions', () => {
let wrapper;
diff --git a/spec/frontend/google_cloud/components/service_accounts_form_spec.js b/spec/frontend/google_cloud/service_accounts/form_spec.js
index 38602d4e8cc..8be481774fa 100644
--- a/spec/frontend/google_cloud/components/service_accounts_form_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/form_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlFormGroup, GlFormSelect, GlFormCheckbox } from '@gitlab/ui';
-import ServiceAccountsForm from '~/google_cloud/components/service_accounts_form.vue';
+import ServiceAccountsForm from '~/google_cloud/service_accounts/form.vue';
-describe('ServiceAccountsForm component', () => {
+describe('google_cloud/service_accounts/form', () => {
let wrapper;
const findHeader = () => wrapper.find('header');
diff --git a/spec/frontend/google_cloud/components/service_accounts_list_spec.js b/spec/frontend/google_cloud/service_accounts/list_spec.js
index f7051c8a53d..7a76a893757 100644
--- a/spec/frontend/google_cloud/components/service_accounts_list_spec.js
+++ b/spec/frontend/google_cloud/service_accounts/list_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { GlAlert, GlButton, GlEmptyState, GlTable } from '@gitlab/ui';
-import ServiceAccountsList from '~/google_cloud/components/service_accounts_list.vue';
+import ServiceAccountsList from '~/google_cloud/service_accounts/list.vue';
-describe('ServiceAccounts component', () => {
+describe('google_cloud/service_accounts/list', () => {
describe('when the project does not have any service accounts', () => {
let wrapper;
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index 8ea7e54aef4..0bc80df6535 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { GlPopover } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import GroupFolder from '~/groups/components/group_folder.vue';
import GroupItem from '~/groups/components/group_item.vue';
@@ -6,14 +6,25 @@ import ItemActions from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
import { getGroupItemMicrodata } from '~/groups/store/utils';
import * as urlUtilities from '~/lib/utils/url_utility';
+import {
+ ITEM_TYPE,
+ VISIBILITY_INTERNAL,
+ VISIBILITY_PRIVATE,
+ VISIBILITY_PUBLIC,
+} from '~/groups/constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
const createComponent = (
propsData = { group: mockParentGroupItem, parentGroup: mockChildren[0] },
+ provide = {
+ currentGroupVisibility: VISIBILITY_PRIVATE,
+ },
) => {
- return mount(GroupItem, {
+ return mountExtended(GroupItem, {
propsData,
components: { GroupFolder },
+ provide,
});
};
@@ -276,4 +287,90 @@ describe('GroupItemComponent', () => {
});
});
});
+
+ describe('visibility warning popover', () => {
+ const findPopover = () => wrapper.findComponent(GlPopover);
+
+ const itDoesNotRenderVisibilityWarningPopover = () => {
+ it('does not render visibility warning popover', () => {
+ expect(findPopover().exists()).toBe(false);
+ });
+ };
+
+ describe('when showing groups', () => {
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
+
+ itDoesNotRenderVisibilityWarningPopover();
+ });
+
+ describe('when `action` prop is not `shared`', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ group: mockParentGroupItem,
+ parentGroup: mockChildren[0],
+ action: 'subgroups_and_projects',
+ });
+ });
+
+ itDoesNotRenderVisibilityWarningPopover();
+ });
+
+ describe('when showing projects', () => {
+ describe.each`
+ itemVisibility | currentGroupVisibility | isPopoverShown
+ ${VISIBILITY_PRIVATE} | ${VISIBILITY_PUBLIC} | ${false}
+ ${VISIBILITY_INTERNAL} | ${VISIBILITY_PUBLIC} | ${false}
+ ${VISIBILITY_PUBLIC} | ${VISIBILITY_PUBLIC} | ${false}
+ ${VISIBILITY_PRIVATE} | ${VISIBILITY_PRIVATE} | ${false}
+ ${VISIBILITY_INTERNAL} | ${VISIBILITY_PRIVATE} | ${true}
+ ${VISIBILITY_PUBLIC} | ${VISIBILITY_PRIVATE} | ${true}
+ `(
+ 'when item visibility is $itemVisibility and parent group visibility is $currentGroupVisibility',
+ ({ itemVisibility, currentGroupVisibility, isPopoverShown }) => {
+ beforeEach(() => {
+ wrapper = createComponent(
+ {
+ group: {
+ ...mockParentGroupItem,
+ visibility: itemVisibility,
+ type: ITEM_TYPE.PROJECT,
+ },
+ parentGroup: mockChildren[0],
+ action: 'shared',
+ },
+ {
+ currentGroupVisibility,
+ },
+ );
+ });
+
+ if (isPopoverShown) {
+ it('renders visibility warning popover', () => {
+ expect(findPopover().exists()).toBe(true);
+ });
+ } else {
+ itDoesNotRenderVisibilityWarningPopover();
+ }
+ },
+ );
+ });
+
+ it('sets up popover `target` prop correctly', () => {
+ wrapper = createComponent({
+ group: {
+ ...mockParentGroupItem,
+ visibility: VISIBILITY_PUBLIC,
+ type: ITEM_TYPE.PROJECT,
+ },
+ parentGroup: mockChildren[0],
+ action: 'shared',
+ });
+
+ expect(findPopover().props('target')()).toEqual(
+ wrapper.findByRole('button', { name: GroupItem.i18n.popoverTitle }).element,
+ );
+ });
+ });
});
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 590b4fb3d57..48a2319cf96 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -1,45 +1,55 @@
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import groupFolderComponent from '~/groups/components/group_folder.vue';
-import groupItemComponent from '~/groups/components/group_item.vue';
-import groupsComponent from '~/groups/components/groups.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import GroupFolderComponent from '~/groups/components/group_folder.vue';
+import GroupItemComponent from '~/groups/components/group_item.vue';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
+import GroupsComponent from '~/groups/components/groups.vue';
import eventHub from '~/groups/event_hub';
+import { VISIBILITY_PRIVATE } from '~/groups/constants';
import { mockGroups, mockPageInfo } from '../mock_data';
-const createComponent = (searchEmpty = false) => {
- const Component = Vue.extend(groupsComponent);
+describe('GroupsComponent', () => {
+ let wrapper;
- return mountComponent(Component, {
+ const defaultPropsData = {
groups: mockGroups,
pageInfo: mockPageInfo,
searchEmptyMessage: 'No matching results',
- searchEmpty,
- });
-};
+ searchEmpty: false,
+ };
-describe('GroupsComponent', () => {
- let vm;
-
- beforeEach(async () => {
- Vue.component('GroupFolder', groupFolderComponent);
- Vue.component('GroupItem', groupItemComponent);
+ const createComponent = ({ propsData } = {}) => {
+ wrapper = mountExtended(GroupsComponent, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ provide: {
+ currentGroupVisibility: VISIBILITY_PRIVATE,
+ },
+ });
+ };
- vm = createComponent();
+ const findPaginationLinks = () => wrapper.findComponent(PaginationLinks);
- await nextTick();
+ beforeEach(async () => {
+ Vue.component('GroupFolder', GroupFolderComponent);
+ Vue.component('GroupItem', GroupItemComponent);
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('methods', () => {
describe('change', () => {
it('should emit `fetchPage` event when page is changed via pagination', () => {
+ createComponent();
+
jest.spyOn(eventHub, '$emit').mockImplementation();
- vm.change(2);
+ findPaginationLinks().props('change')(2);
expect(eventHub.$emit).toHaveBeenCalledWith('fetchPage', {
page: 2,
@@ -52,18 +62,18 @@ describe('GroupsComponent', () => {
});
describe('template', () => {
- it('should render component template correctly', async () => {
- await nextTick();
- expect(vm.$el.querySelector('.groups-list-tree-container')).toBeDefined();
- expect(vm.$el.querySelector('.group-list-tree')).toBeDefined();
- expect(vm.$el.querySelector('.gl-pagination')).toBeDefined();
- expect(vm.$el.querySelectorAll('.has-no-search-results').length).toBe(0);
+ it('should render component template correctly', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GroupFolderComponent).exists()).toBe(true);
+ expect(findPaginationLinks().exists()).toBe(true);
+ expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(false);
});
- it('should render empty search message when `searchEmpty` is `true`', async () => {
- vm.searchEmpty = true;
- await nextTick();
- expect(vm.$el.querySelector('.has-no-search-results')).toBeDefined();
+ it('should render empty search message when `searchEmpty` is `true`', () => {
+ createComponent({ propsData: { searchEmpty: true } });
+
+ expect(wrapper.findByText(defaultPropsData.searchEmptyMessage).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/groups/mock_data.js b/spec/frontend/groups/mock_data.js
index 65a62876893..9a325776374 100644
--- a/spec/frontend/groups/mock_data.js
+++ b/spec/frontend/groups/mock_data.js
@@ -29,6 +29,7 @@ export const mockParentGroupItem = {
isChildrenLoading: false,
isBeingRemoved: false,
updatedAt: '2017-04-09T18:40:39.101Z',
+ lastActivityAt: '2017-04-09T18:40:39.101Z',
};
export const mockRawChildren = [
diff --git a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
index ae19ed9ab02..82ac390971d 100644
--- a/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
+++ b/spec/frontend/pipeline_editor/components/lint/ci_lint_results_spec.js
@@ -152,4 +152,26 @@ describe('CI Lint Results', () => {
expect(findAfterScripts()).toHaveLength(filterEmptyScripts('afterScript').length);
});
});
+
+ describe('Hide Alert', () => {
+ it('hides alert on success if hide-alert prop is true', async () => {
+ await createComponent({ dryRun: true, hideAlert: true }, mount);
+
+ expect(findStatus().exists()).toBe(false);
+ });
+
+ it('hides alert on error if hide-alert prop is true', async () => {
+ await createComponent(
+ {
+ hideAlert: true,
+ isValid: false,
+ errors: mockErrors,
+ warnings: mockWarnings,
+ },
+ mount,
+ );
+
+ expect(findStatus().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
index af5029b7c3b..87a7f07f7d4 100644
--- a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -1,6 +1,8 @@
import { GlAlert, GlBadge, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { createLocalVue, mount, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
+import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import CiConfigMergedPreview from '~/pipeline_editor/components/editor/ci_config_merged_preview.vue';
import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
@@ -19,7 +21,17 @@ import {
VALIDATE_TAB_BADGE_DISMISSED_KEY,
} from '~/pipeline_editor/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import { mockLintResponse, mockLintResponseWithoutMerged, mockCiYml } from '../mock_data';
+import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import {
+ mockBlobContentQueryResponse,
+ mockCiLintPath,
+ mockCiYml,
+ mockLintResponse,
+ mockLintResponseWithoutMerged,
+} from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
Vue.config.ignoredElements = ['gl-emoji'];
@@ -35,6 +47,7 @@ describe('Pipeline editor tabs component', () => {
provide = {},
appStatus = EDITOR_APP_STATUS_VALID,
mountFn = shallowMount,
+ options = {},
} = {}) => {
wrapper = mountFn(PipelineEditorTabs, {
propsData: {
@@ -50,12 +63,34 @@ describe('Pipeline editor tabs component', () => {
appStatus,
};
},
- provide: { ...provide },
+ provide: {
+ ciLintPath: mockCiLintPath,
+ ...provide,
+ },
stubs: {
TextEditor: MockTextEditor,
EditorTab,
},
listeners,
+ ...options,
+ });
+ };
+
+ let mockBlobContentData;
+ let mockApollo;
+
+ const createComponentWithApollo = ({ props, provide = {}, mountFn = shallowMount } = {}) => {
+ const handlers = [[getBlobContent, mockBlobContentData]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ props,
+ provide,
+ mountFn,
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ },
});
};
@@ -76,6 +111,10 @@ describe('Pipeline editor tabs component', () => {
const findMergedPreview = () => wrapper.findComponent(CiConfigMergedPreview);
const findWalkthroughPopover = () => wrapper.findComponent(WalkthroughPopover);
+ beforeEach(() => {
+ mockBlobContentData = jest.fn();
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -118,27 +157,6 @@ describe('Pipeline editor tabs component', () => {
describe('validate tab', () => {
describe('with simulatePipeline feature flag ON', () => {
- describe('while loading', () => {
- beforeEach(() => {
- createComponent({
- appStatus: EDITOR_APP_STATUS_LOADING,
- provide: {
- glFeatures: {
- simulatePipeline: true,
- },
- },
- });
- });
-
- it('displays a loading icon if the lint query is loading', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
-
- it('does not display the validate component', () => {
- expect(findCiValidate().exists()).toBe(false);
- });
- });
-
describe('after loading', () => {
beforeEach(() => {
createComponent({
@@ -155,13 +173,17 @@ describe('Pipeline editor tabs component', () => {
describe('NEW badge', () => {
describe('default', () => {
beforeEach(() => {
- createComponent({
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ createComponentWithApollo({
mountFn: mount,
props: {
currentTab: VALIDATE_TAB,
},
provide: {
glFeatures: { simulatePipeline: true },
+ ciConfigPath: '/path/to/ci-config',
+ currentBranch: 'main',
+ projectFullPath: '/path/to/project',
simulatePipelineHelpPagePath: 'path/to/help/page',
validateTabIllustrationPath: 'path/to/svg',
},
@@ -185,10 +207,14 @@ describe('Pipeline editor tabs component', () => {
describe('if badge has been dismissed before', () => {
beforeEach(() => {
localStorage.setItem(VALIDATE_TAB_BADGE_DISMISSED_KEY, 'true');
- createComponent({
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ createComponentWithApollo({
mountFn: mount,
provide: {
glFeatures: { simulatePipeline: true },
+ ciConfigPath: '/path/to/ci-config',
+ currentBranch: 'main',
+ projectFullPath: '/path/to/project',
simulatePipelineHelpPagePath: 'path/to/help/page',
validateTabIllustrationPath: 'path/to/svg',
},
diff --git a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
index e1fbcd34e7c..f5f01b675b2 100644
--- a/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
+++ b/spec/frontend/pipeline_editor/components/validate/ci_validate_spec.js
@@ -1,45 +1,132 @@
-import { GlButton, GlDropdown, GlIcon, GlPopover } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlDropdown, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
import CiValidate, { i18n } from '~/pipeline_editor/components/validate/ci_validate.vue';
import ValidatePipelinePopover from '~/pipeline_editor/components/popovers/validate_pipeline_popover.vue';
-import { mockSimulatePipelineHelpPagePath } from '../../mock_data';
+import getBlobContent from '~/pipeline_editor/graphql/queries/blob_content.query.graphql';
+import lintCIMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import {
+ mockBlobContentQueryResponse,
+ mockCiLintPath,
+ mockCiYml,
+ mockSimulatePipelineHelpPagePath,
+} from '../../mock_data';
+import { mockLintDataError, mockLintDataValid } from '../../../ci_lint/mock_data';
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
describe('Pipeline Editor Validate Tab', () => {
let wrapper;
+ let mockApollo;
+ let mockBlobContentData;
- const createComponent = ({ stubs } = {}) => {
- wrapper = shallowMount(CiValidate, {
+ const createComponent = ({
+ props,
+ stubs,
+ options,
+ isBlobLoading = false,
+ isSimulationLoading = false,
+ } = {}) => {
+ wrapper = shallowMountExtended(CiValidate, {
+ propsData: {
+ ciFileContent: mockCiYml,
+ ...props,
+ },
provide: {
+ ciConfigPath: '/path/to/ci-config',
+ ciLintPath: mockCiLintPath,
+ currentBranch: 'main',
+ projectFullPath: '/path/to/project',
validateTabIllustrationPath: '/path/to/img',
simulatePipelineHelpPagePath: mockSimulatePipelineHelpPagePath,
},
stubs,
+ mocks: {
+ $apollo: {
+ queries: {
+ initialBlobContent: {
+ loading: isBlobLoading,
+ },
+ },
+ mutations: {
+ lintCiMutation: {
+ loading: isSimulationLoading,
+ },
+ },
+ },
+ },
+ ...options,
+ });
+ };
+
+ const createComponentWithApollo = ({ props, stubs } = {}) => {
+ const handlers = [[getBlobContent, mockBlobContentData]];
+ mockApollo = createMockApollo(handlers);
+
+ createComponent({
+ props,
+ stubs,
+ options: {
+ localVue,
+ apolloProvider: mockApollo,
+ mocks: {},
+ },
});
};
- const findCta = () => wrapper.findComponent(GlButton);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findCancelBtn = () => wrapper.findByTestId('cancel-simulation');
+ const findContentChangeStatus = () => wrapper.findByTestId('content-status');
+ const findCta = () => wrapper.findByTestId('simulate-pipeline-button');
+ const findDisabledCtaTooltip = () => wrapper.findByTestId('cta-tooltip');
const findHelpIcon = () => wrapper.findComponent(GlIcon);
+ const findIllustration = () => wrapper.findByRole('img');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findPipelineSource = () => wrapper.findComponent(GlDropdown);
const findPopover = () => wrapper.findComponent(GlPopover);
+ const findCiLintResults = () => wrapper.findComponent(CiLintResults);
+ const findResultsCta = () => wrapper.findByTestId('resimulate-pipeline-button');
+
+ beforeEach(() => {
+ mockBlobContentData = jest.fn();
+ });
afterEach(() => {
wrapper.destroy();
});
- describe('template', () => {
+ describe('while initial CI content is loading', () => {
beforeEach(() => {
- createComponent({ stubs: { GlPopover, ValidatePipelinePopover } });
+ createComponent({ isBlobLoading: true });
+ });
+
+ it('renders disabled CTA with tooltip', () => {
+ expect(findCta().props('disabled')).toBe(true);
+ expect(findDisabledCtaTooltip().exists()).toBe(true);
+ });
+ });
+
+ describe('after initial CI content is loaded', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo({ stubs: { GlPopover, ValidatePipelinePopover } });
});
it('renders disabled pipeline source dropdown', () => {
expect(findPipelineSource().exists()).toBe(true);
expect(findPipelineSource().attributes('text')).toBe(i18n.pipelineSourceDefault);
- expect(findPipelineSource().attributes('disabled')).toBe('true');
+ expect(findPipelineSource().props('disabled')).toBe(true);
});
- it('renders CTA', () => {
+ it('renders enabled CTA without tooltip', () => {
expect(findCta().exists()).toBe(true);
- expect(findCta().text()).toBe(i18n.cta);
+ expect(findCta().props('disabled')).toBe(false);
+ expect(findDisabledCtaTooltip().exists()).toBe(false);
});
it('popover is set to render when hovering over help icon', () => {
@@ -47,4 +134,146 @@ describe('Pipeline Editor Validate Tab', () => {
expect(findPopover().props('triggers')).toBe('hover focus');
});
});
+
+ describe('simulating the pipeline', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ });
+
+ it('renders loading state while simulation is ongoing', async () => {
+ findCta().vm.$emit('click');
+ await nextTick();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findCancelBtn().exists()).toBe(true);
+ expect(findCta().props('loading')).toBe(true);
+ });
+
+ it('calls mutation with the correct input', async () => {
+ await findCta().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: lintCIMutation,
+ variables: {
+ dry_run: true,
+ content: mockCiYml,
+ endpoint: mockCiLintPath,
+ },
+ });
+ });
+
+ describe('when results are successful', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ await findCta().vm.$emit('click');
+ });
+
+ it('renders success alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().attributes('variant')).toBe('success');
+ expect(findAlert().attributes('title')).toBe(i18n.successAlertTitle);
+ });
+
+ it('does not render content change status or CTA for results page', () => {
+ expect(findContentChangeStatus().exists()).toBe(false);
+ expect(findResultsCta().exists()).toBe(false);
+ });
+
+ it('renders CI lint results with correct props', () => {
+ expect(findCiLintResults().exists()).toBe(true);
+ expect(findCiLintResults().props()).toMatchObject({
+ dryRun: true,
+ hideAlert: true,
+ isValid: true,
+ jobs: mockLintDataValid.data.lintCI.jobs,
+ });
+ });
+ });
+
+ describe('when results have errors', () => {
+ beforeEach(async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataError);
+ await findCta().vm.$emit('click');
+ });
+
+ it('renders error alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().attributes('variant')).toBe('danger');
+ expect(findAlert().attributes('title')).toBe(i18n.errorAlertTitle);
+ });
+
+ it('renders CI lint results with correct props', () => {
+ expect(findCiLintResults().exists()).toBe(true);
+ expect(findCiLintResults().props()).toMatchObject({
+ dryRun: true,
+ hideAlert: true,
+ isValid: false,
+ errors: mockLintDataError.data.lintCI.errors,
+ warnings: mockLintDataError.data.lintCI.warnings,
+ });
+ });
+ });
+ });
+
+ describe('when CI content has changed after a simulation', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ await findCta().vm.$emit('click');
+ });
+
+ it('renders content change status', async () => {
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+
+ expect(findContentChangeStatus().exists()).toBe(true);
+ expect(findResultsCta().exists()).toBe(true);
+ });
+
+ it('calls mutation with new content', async () => {
+ await wrapper.setProps({ ciFileContent: 'new yaml content' });
+ await findResultsCta().vm.$emit('click');
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(2);
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
+ mutation: lintCIMutation,
+ variables: {
+ dry_run: true,
+ content: 'new yaml content',
+ endpoint: mockCiLintPath,
+ },
+ });
+ });
+ });
+
+ describe('canceling a simulation', () => {
+ beforeEach(async () => {
+ mockBlobContentData.mockResolvedValue(mockBlobContentQueryResponse);
+ await createComponentWithApollo();
+ });
+
+ it('returns to init state', async () => {
+ // init state
+ expect(findIllustration().exists()).toBe(true);
+ expect(findCiLintResults().exists()).toBe(false);
+
+ // mutations should have successful results
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockLintDataValid);
+ findCta().vm.$emit('click');
+ await nextTick();
+
+ // cancel before simulation succeeds
+ expect(findCancelBtn().exists()).toBe(true);
+ await findCancelBtn().vm.$emit('click');
+
+ // should still render init state
+ expect(findIllustration().exists()).toBe(true);
+ expect(findCiLintResults().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/pipeline_editor/mock_data.js b/spec/frontend/pipeline_editor/mock_data.js
index f50ab1ab2a6..2ea580b7b53 100644
--- a/spec/frontend/pipeline_editor/mock_data.js
+++ b/spec/frontend/pipeline_editor/mock_data.js
@@ -7,6 +7,7 @@ export const mockProjectFullPath = `${mockProjectNamespace}/${mockProjectPath}`;
export const mockDefaultBranch = 'main';
export const mockNewBranch = 'new-branch';
export const mockNewMergeRequestPath = '/-/merge_requests/new';
+export const mockCiLintPath = '/-/ci/lint';
export const mockCommitSha = 'aabbccdd';
export const mockCommitNextSha = 'eeffgghh';
export const mockIncludesHelpPagePath = '/-/includes/help';
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index 6ab479a257c..f9b9da01a2b 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -49,7 +49,7 @@ describe('Mutations TestReports Store', () => {
describe('set suite error', () => {
it('should set the error message in state if provided', () => {
- const message = 'Test report artifacts have expired';
+ const message = 'Test report artifacts not found';
mutations[types.SET_SUITE_ERROR](mockState, {
response: { data: { errors: message } },
diff --git a/spec/helpers/ci/pipeline_editor_helper_spec.rb b/spec/helpers/ci/pipeline_editor_helper_spec.rb
index a895d3e4fd7..bc9e47a4ca1 100644
--- a/spec/helpers/ci/pipeline_editor_helper_spec.rb
+++ b/spec/helpers/ci/pipeline_editor_helper_spec.rb
@@ -48,6 +48,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
+ "ci-lint-path" => project_ci_lint_path(project),
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'illustrations/empty.svg',
"initial-branch-name" => nil,
@@ -78,6 +79,7 @@ RSpec.describe Ci::PipelineEditorHelper do
"ci-config-path": project.ci_config_path_or_default,
"ci-examples-help-page-path" => help_page_path('ci/examples/index'),
"ci-help-page-path" => help_page_path('ci/index'),
+ "ci-lint-path" => project_ci_lint_path(project),
"default-branch" => project.default_branch_or_main,
"empty-state-illustration-path" => 'illustrations/empty.svg',
"initial-branch-name" => nil,
diff --git a/spec/lib/gitlab/memory/watchdog_spec.rb b/spec/lib/gitlab/memory/watchdog_spec.rb
new file mode 100644
index 00000000000..8b82078bcb9
--- /dev/null
+++ b/spec/lib/gitlab/memory/watchdog_spec.rb
@@ -0,0 +1,308 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Memory::Watchdog, :aggregate_failures, :prometheus do
+ context 'watchdog' do
+ let(:logger) { instance_double(::Logger) }
+ let(:handler) { instance_double(described_class::NullHandler) }
+
+ let(:heap_frag_limit_gauge) { instance_double(::Prometheus::Client::Gauge) }
+ let(:heap_frag_violations_counter) { instance_double(::Prometheus::Client::Counter) }
+ let(:heap_frag_violations_handled_counter) { instance_double(::Prometheus::Client::Counter) }
+
+ let(:sleep_time) { 0.1 }
+ let(:max_heap_fragmentation) { 0.2 }
+
+ subject(:watchdog) do
+ described_class.new(handler: handler, logger: logger, sleep_time_seconds: sleep_time,
+ max_strikes: max_strikes, max_heap_fragmentation: max_heap_fragmentation)
+ end
+
+ before do
+ allow(handler).to receive(:on_high_heap_fragmentation).and_return(true)
+
+ allow(logger).to receive(:warn)
+ allow(logger).to receive(:info)
+
+ allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(fragmentation)
+ end
+
+ after do
+ watchdog.stop
+ end
+
+ context 'when starting up' do
+ let(:fragmentation) { 0 }
+ let(:max_strikes) { 0 }
+
+ it 'sets the heap fragmentation limit gauge' do
+ allow(Gitlab::Metrics).to receive(:gauge).and_return(heap_frag_limit_gauge)
+
+ expect(heap_frag_limit_gauge).to receive(:set).with({}, max_heap_fragmentation)
+ end
+
+ context 'when no settings are set in the environment' do
+ it 'initializes with defaults' do
+ watchdog = described_class.new(handler: handler, logger: logger)
+
+ expect(watchdog.max_heap_fragmentation).to eq(described_class::DEFAULT_HEAP_FRAG_THRESHOLD)
+ expect(watchdog.max_strikes).to eq(described_class::DEFAULT_MAX_STRIKES)
+ expect(watchdog.sleep_time_seconds).to eq(described_class::DEFAULT_SLEEP_TIME_SECONDS)
+ end
+ end
+
+ context 'when settings are passed through the environment' do
+ before do
+ stub_env('GITLAB_MEMWD_MAX_HEAP_FRAG', 1)
+ stub_env('GITLAB_MEMWD_MAX_STRIKES', 2)
+ stub_env('GITLAB_MEMWD_SLEEP_TIME_SEC', 3)
+ end
+
+ it 'initializes with these settings' do
+ watchdog = described_class.new(handler: handler, logger: logger)
+
+ expect(watchdog.max_heap_fragmentation).to eq(1)
+ expect(watchdog.max_strikes).to eq(2)
+ expect(watchdog.sleep_time_seconds).to eq(3)
+ end
+ end
+ end
+
+ context 'when process does not exceed heap fragmentation threshold' do
+ let(:fragmentation) { max_heap_fragmentation - 0.1 }
+ let(:max_strikes) { 0 } # To rule out that we were granting too many strikes.
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:on_high_heap_fragmentation)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+ end
+
+ context 'when process exceeds heap fragmentation threshold permanently' do
+ let(:fragmentation) { max_heap_fragmentation + 0.1 }
+
+ before do
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:gitlab_memwd_heap_frag_violations_total, anything, anything)
+ .and_return(heap_frag_violations_counter)
+ allow(Gitlab::Metrics).to receive(:counter)
+ .with(:gitlab_memwd_heap_frag_violations_handled_total, anything, anything)
+ .and_return(heap_frag_violations_handled_counter)
+ allow(heap_frag_violations_counter).to receive(:increment)
+ allow(heap_frag_violations_handled_counter).to receive(:increment)
+ end
+
+ context 'when process has not exceeded allowed number of strikes' do
+ let(:max_strikes) { 10 }
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:on_high_heap_fragmentation)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+
+ it 'does not log any events' do
+ expect(logger).not_to receive(:warn)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+
+ it 'increments the violations counter' do
+ expect(heap_frag_violations_counter).to receive(:increment)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+
+ it 'does not increment violations handled counter' do
+ expect(heap_frag_violations_handled_counter).not_to receive(:increment)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+ end
+
+ context 'when process exceeds the allowed number of strikes' do
+ let(:max_strikes) { 1 }
+
+ it 'signals the handler and resets strike counter' do
+ expect(handler).to receive(:on_high_heap_fragmentation).and_return(true)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+
+ expect(watchdog.strikes).to eq(0)
+ end
+
+ it 'logs the event' do
+ expect(::Prometheus::PidProvider).to receive(:worker_id).at_least(:once).and_return('worker_1')
+ expect(Gitlab::Metrics::System).to receive(:memory_usage_rss).at_least(:once).and_return(1024)
+ expect(logger).to receive(:warn).with({
+ message: 'heap fragmentation limit exceeded',
+ pid: Process.pid,
+ worker_id: 'worker_1',
+ memwd_handler_class: 'RSpec::Mocks::InstanceVerifyingDouble',
+ memwd_sleep_time_s: sleep_time,
+ memwd_max_heap_frag: max_heap_fragmentation,
+ memwd_cur_heap_frag: fragmentation,
+ memwd_max_strikes: max_strikes,
+ memwd_cur_strikes: max_strikes + 1,
+ memwd_rss_bytes: 1024
+ })
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+
+ it 'increments both the violations and violations handled counters' do
+ expect(heap_frag_violations_counter).to receive(:increment)
+ expect(heap_frag_violations_handled_counter).to receive(:increment)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+
+ context 'when enforce_memory_watchdog ops toggle is off' do
+ before do
+ stub_feature_flags(enforce_memory_watchdog: false)
+ end
+
+ it 'always uses the NullHandler' do
+ expect(handler).not_to receive(:on_high_heap_fragmentation)
+ expect(described_class::NullHandler.instance).to(
+ receive(:on_high_heap_fragmentation).with(fragmentation).and_return(true)
+ )
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+ end
+ end
+
+ context 'when handler result is true' do
+ let(:max_strikes) { 1 }
+
+ it 'considers the event handled and stops itself' do
+ expect(handler).to receive(:on_high_heap_fragmentation).once.and_return(true)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+ end
+
+ context 'when handler result is false' do
+ let(:max_strikes) { 1 }
+
+ it 'keeps running' do
+ # Return true the third time to terminate the daemon.
+ expect(handler).to receive(:on_high_heap_fragmentation).and_return(false, false, true)
+
+ watchdog.start
+
+ sleep sleep_time * 4
+ end
+ end
+ end
+
+ context 'when process exceeds heap fragmentation threshold temporarily' do
+ let(:fragmentation) { max_heap_fragmentation }
+ let(:max_strikes) { 1 }
+
+ before do
+ allow(Gitlab::Metrics::Memory).to receive(:gc_heap_fragmentation).and_return(
+ fragmentation - 0.1,
+ fragmentation + 0.2,
+ fragmentation - 0.1,
+ fragmentation + 0.1
+ )
+ end
+
+ it 'does not signal the handler' do
+ expect(handler).not_to receive(:on_high_heap_fragmentation)
+
+ watchdog.start
+
+ sleep sleep_time * 4
+ end
+ end
+
+ context 'when gitlab_memory_watchdog ops toggle is off' do
+ let(:fragmentation) { 0 }
+ let(:max_strikes) { 0 }
+
+ before do
+ stub_feature_flags(gitlab_memory_watchdog: false)
+ end
+
+ it 'does not monitor heap fragmentation' do
+ expect(Gitlab::Metrics::Memory).not_to receive(:gc_heap_fragmentation)
+
+ watchdog.start
+
+ sleep sleep_time * 3
+ end
+ end
+ end
+
+ context 'handlers' do
+ context 'NullHandler' do
+ subject(:handler) { described_class::NullHandler.instance }
+
+ describe '#on_high_heap_fragmentation' do
+ it 'does nothing' do
+ expect(handler.on_high_heap_fragmentation(1.0)).to be(false)
+ end
+ end
+ end
+
+ context 'TermProcessHandler' do
+ subject(:handler) { described_class::TermProcessHandler.new(42) }
+
+ describe '#on_high_heap_fragmentation' do
+ it 'sends SIGTERM to the current process' do
+ expect(Process).to receive(:kill).with(:TERM, 42)
+
+ expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
+ end
+ end
+ end
+
+ context 'PumaHandler' do
+ # rubocop: disable RSpec/VerifiedDoubles
+ # In tests, the Puma constant is not loaded so we cannot make this an instance_double.
+ let(:puma_worker_handle_class) { double('Puma::Cluster::WorkerHandle') }
+ let(:puma_worker_handle) { double('worker') }
+ # rubocop: enable RSpec/VerifiedDoubles
+
+ subject(:handler) { described_class::PumaHandler.new({}) }
+
+ before do
+ stub_const('::Puma::Cluster::WorkerHandle', puma_worker_handle_class)
+ end
+
+ describe '#on_high_heap_fragmentation' do
+ it 'invokes orderly termination via Puma API' do
+ expect(puma_worker_handle_class).to receive(:new).and_return(puma_worker_handle)
+ expect(puma_worker_handle).to receive(:term)
+
+ expect(handler.on_high_heap_fragmentation(1.0)).to be(true)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/models/project_setting_spec.rb b/spec/models/project_setting_spec.rb
index 867ad843406..fb1601a5f9c 100644
--- a/spec/models/project_setting_spec.rb
+++ b/spec/models/project_setting_spec.rb
@@ -6,6 +6,17 @@ RSpec.describe ProjectSetting, type: :model do
using RSpec::Parameterized::TableSyntax
it { is_expected.to belong_to(:project) }
+ describe 'scopes' do
+ let_it_be(:project_1) { create(:project) }
+ let_it_be(:project_2) { create(:project) }
+ let_it_be(:project_setting_1) { create(:project_setting, project: project_1) }
+ let_it_be(:project_setting_2) { create(:project_setting, project: project_2) }
+
+ it 'returns project setting for the given projects' do
+ expect(described_class.for_projects(project_1)).to contain_exactly(project_setting_1)
+ end
+ end
+
describe 'validations' do
it { is_expected.not_to allow_value(nil).for(:target_platforms) }
it { is_expected.to allow_value([]).for(:target_platforms) }
diff --git a/spec/requests/api/graphql/crm/contacts_spec.rb b/spec/requests/api/graphql/crm/contacts_spec.rb
new file mode 100644
index 00000000000..7e824140894
--- /dev/null
+++ b/spec/requests/api/graphql/crm/contacts_spec.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting CRM contacts' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group) { create(:group, :crm_enabled) }
+
+ let_it_be(:contact_a) do
+ create(
+ :contact,
+ group: group,
+ first_name: "ABC",
+ last_name: "DEF",
+ email: "ghi@test.com",
+ description: "LMNO",
+ state: "inactive"
+ )
+ end
+
+ let_it_be(:contact_b) do
+ create(
+ :contact,
+ group: group,
+ first_name: "ABC",
+ last_name: "DEF",
+ email: "vwx@test.com",
+ description: "YZ",
+ state: "active"
+ )
+ end
+
+ let_it_be(:contact_c) do
+ create(
+ :contact,
+ group: group,
+ first_name: "PQR",
+ last_name: "STU",
+ email: "aaa@test.com",
+ description: "YZ",
+ state: "active"
+ )
+ end
+
+ before do
+ group.add_reporter(current_user)
+ end
+
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_argument) { {} }
+ let(:first_param) { 2 }
+ let(:all_records) { [contact_a, contact_b, contact_c] }
+ let(:data_path) { [:group, :contacts] }
+
+ def pagination_query(params)
+ graphql_query_for(
+ :group,
+ { full_path: group.full_path },
+ query_graphql_field(:contacts, params, "#{page_info} nodes { id }")
+ )
+ end
+
+ def pagination_results_data(nodes)
+ nodes.map { |item| GlobalID::Locator.locate(item['id']) }
+ end
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 69e14eace66..596e023a027 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -223,6 +223,7 @@ RSpec.describe 'getting an issue list for a project' do
end
describe 'sorting and pagination' do
+ let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:data_path) { [:project, :issues] }
def pagination_query(params)
@@ -237,8 +238,38 @@ RSpec.describe 'getting an issue list for a project' do
data.map { |issue| issue['iid'].to_i }
end
+ context 'when sorting by severity' do
+ let_it_be(:severty_issue1) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue2) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue3) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue4) { create(:issue, project: sort_project) }
+ let_it_be(:severty_issue5) { create(:issue, project: sort_project) }
+
+ before(:all) do
+ create(:issuable_severity, issue: severty_issue1, severity: :unknown)
+ create(:issuable_severity, issue: severty_issue2, severity: :low)
+ create(:issuable_severity, issue: severty_issue4, severity: :critical)
+ create(:issuable_severity, issue: severty_issue5, severity: :high)
+ end
+
+ context 'when ascending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_ASC }
+ let(:first_param) { 2 }
+ let(:all_records) { [severty_issue3.iid, severty_issue1.iid, severty_issue2.iid, severty_issue5.iid, severty_issue4.iid] }
+ end
+ end
+
+ context 'when descending' do
+ it_behaves_like 'sorted paginated query' do
+ let(:sort_param) { :SEVERITY_DESC }
+ let(:first_param) { 2 }
+ let(:all_records) { [severty_issue4.iid, severty_issue5.iid, severty_issue2.iid, severty_issue1.iid, severty_issue3.iid] }
+ end
+ end
+ end
+
context 'when sorting by due date' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:due_issue1) { create(:issue, project: sort_project, due_date: 3.days.from_now) }
let_it_be(:due_issue2) { create(:issue, project: sort_project, due_date: nil) }
let_it_be(:due_issue3) { create(:issue, project: sort_project, due_date: 2.days.ago) }
@@ -263,7 +294,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by relative position' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:relative_issue1) { create(:issue, project: sort_project, relative_position: 2000) }
let_it_be(:relative_issue2) { create(:issue, project: sort_project, relative_position: nil) }
let_it_be(:relative_issue3) { create(:issue, project: sort_project, relative_position: 1000) }
@@ -285,7 +315,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by priority' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:on_project) { { project: sort_project } }
let_it_be(:early_milestone) { create(:milestone, **on_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, **on_project, due_date: 30.days.from_now) }
@@ -321,7 +350,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by label priority' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:label1) { create(:label, project: sort_project, priority: 1) }
let_it_be(:label2) { create(:label, project: sort_project, priority: 5) }
let_it_be(:label3) { create(:label, project: sort_project, priority: 10) }
@@ -348,7 +376,6 @@ RSpec.describe 'getting an issue list for a project' do
end
context 'when sorting by milestone due date' do
- let_it_be(:sort_project) { create(:project, :public) }
let_it_be(:early_milestone) { create(:milestone, project: sort_project, due_date: 10.days.from_now) }
let_it_be(:late_milestone) { create(:milestone, project: sort_project, due_date: 30.days.from_now) }
let_it_be(:milestone_issue1) { create(:issue, project: sort_project) }
diff --git a/spec/requests/projects/google_cloud/configuration_controller_spec.rb b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
new file mode 100644
index 00000000000..08d4ad2f9ba
--- /dev/null
+++ b/spec/requests/projects/google_cloud/configuration_controller_spec.rb
@@ -0,0 +1,141 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Mock Types
+MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+
+RSpec.describe Projects::GoogleCloud::ConfigurationController do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:url) { project_google_cloud_configuration_path(project) }
+
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer] }
+
+ before do
+ project.add_guest(user_guest)
+ project.add_developer(user_developer)
+ project.add_maintainer(user_maintainer)
+ end
+
+ context 'when accessed by unauthorized members' do
+ it 'returns not found on GET request' do
+ unauthorized_members.each do |unauthorized_member|
+ sign_in(unauthorized_member)
+
+ get url
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'admin_project_google_cloud!',
+ label: 'error_access_denied',
+ property: 'invalid_user',
+ project: project,
+ user: unauthorized_member
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when accessed by authorized members' do
+ it 'returns successful' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to be_successful
+ expect(response).to render_template('projects/google_cloud/configuration/index')
+ end
+ end
+
+ context 'but gitlab instance is not configured for google oauth2' do
+ it 'returns forbidden' do
+ unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(unconfigured_google_oauth2)
+
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'google_oauth2_enabled!',
+ label: 'error_access_denied',
+ extra: { reason: 'google_oauth2_not_configured',
+ config: unconfigured_google_oauth2 },
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+
+ context 'but feature flag is disabled' do
+ before do
+ stub_feature_flags(incubation_5mp_google_cloud: false)
+ end
+
+ it 'returns not found' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'feature_flag_enabled!',
+ label: 'error_access_denied',
+ property: 'feature_flag_not_enabled',
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+
+ context 'but google oauth2 token is not valid' do
+ it 'does not return revoke oauth url' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(false)
+ end
+
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to be_successful
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'configuration#index',
+ label: 'success',
+ extra: {
+ configurationUrl: project_google_cloud_configuration_path(project),
+ deploymentsUrl: project_google_cloud_deployments_path(project),
+ databasesUrl: project_google_cloud_databases_path(project),
+ serviceAccounts: [],
+ createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
+ emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
+ configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
+ gcpRegions: [],
+ revokeOauthUrl: nil
+ },
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/google_cloud/databases_controller_spec.rb b/spec/requests/projects/google_cloud/databases_controller_spec.rb
new file mode 100644
index 00000000000..c9335f8f317
--- /dev/null
+++ b/spec/requests/projects/google_cloud/databases_controller_spec.rb
@@ -0,0 +1,135 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+# Mock Types
+MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
+
+RSpec.describe Projects::GoogleCloud::DatabasesController do
+ let_it_be(:project) { create(:project, :public) }
+ let_it_be(:url) { project_google_cloud_databases_path(project) }
+
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer] }
+
+ before do
+ project.add_guest(user_guest)
+ project.add_developer(user_developer)
+ project.add_maintainer(user_maintainer)
+ end
+
+ context 'when accessed by unauthorized members' do
+ it 'returns not found on GET request' do
+ unauthorized_members.each do |unauthorized_member|
+ sign_in(unauthorized_member)
+
+ get url
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'admin_project_google_cloud!',
+ label: 'error_access_denied',
+ property: 'invalid_user',
+ project: project,
+ user: unauthorized_member
+ )
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when accessed by authorized members' do
+ it 'returns successful' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to be_successful
+ expect(response).to render_template('projects/google_cloud/databases/index')
+ end
+ end
+
+ context 'but gitlab instance is not configured for google oauth2' do
+ it 'returns forbidden' do
+ unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
+ .with('google_oauth2')
+ .and_return(unconfigured_google_oauth2)
+
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'google_oauth2_enabled!',
+ label: 'error_access_denied',
+ extra: { reason: 'google_oauth2_not_configured',
+ config: unconfigured_google_oauth2 },
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+
+ context 'but feature flag is disabled' do
+ before do
+ stub_feature_flags(incubation_5mp_google_cloud: false)
+ end
+
+ it 'returns not found' do
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'feature_flag_enabled!',
+ label: 'error_access_denied',
+ property: 'feature_flag_not_enabled',
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+
+ context 'but google oauth2 token is not valid' do
+ it 'does not return revoke oauth url' do
+ allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
+ allow(client).to receive(:validate_token).and_return(false)
+ end
+
+ authorized_members.each do |authorized_member|
+ sign_in(authorized_member)
+
+ get url
+
+ expect(response).to be_successful
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'databases#index',
+ label: 'success',
+ extra: {
+ configurationUrl: project_google_cloud_configuration_path(project),
+ deploymentsUrl: project_google_cloud_deployments_path(project),
+ databasesUrl: project_google_cloud_databases_path(project)
+ },
+ project: project,
+ user: authorized_member
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/projects/google_cloud/deployments_controller_spec.rb b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
index 7bd9609a7dc..9e854e01516 100644
--- a/spec/requests/projects/google_cloud/deployments_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/deployments_controller_spec.rb
@@ -9,10 +9,9 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
let_it_be(:user_guest) { create(:user) }
let_it_be(:user_developer) { create(:user) }
let_it_be(:user_maintainer) { create(:user) }
- let_it_be(:user_creator) { project.creator }
let_it_be(:unauthorized_members) { [user_guest, user_developer] }
- let_it_be(:authorized_members) { [user_maintainer, user_creator] }
+ let_it_be(:authorized_members) { [user_maintainer] }
let_it_be(:urls_list) { %W[#{project_google_cloud_deployments_cloud_run_path(project)} #{project_google_cloud_deployments_cloud_storage_path(project)}] }
@@ -32,7 +31,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: nil
@@ -51,7 +50,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: nil
@@ -87,15 +86,15 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
end
- it 'redirects to google_cloud home on enable service error' do
+ it 'redirects to google cloud deployments on enable service error' do
get url
- expect(response).to redirect_to(project_google_cloud_index_path(project))
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
# since GPC_PROJECT_ID is not set, enable cloud run service should return an error
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
- label: 'enable_cloud_run_error',
+ label: 'error_enable_cloud_run',
extra: { message: 'No GCP projects found. Configure a service account or GCP_PROJECT_ID ci variable.',
status: :error },
project: project,
@@ -103,7 +102,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
)
end
- it 'redirects to gcp_error' do
+ it 'redirects to google cloud deployments with error' do
mock_gcp_error = Google::Apis::ClientError.new('some_error')
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |service|
@@ -112,11 +111,11 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
get url
- expect(response).to render_template(:gcp_error)
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
- label: 'gcp_error',
+ label: 'error_gcp',
extra: mock_gcp_error,
project: project,
user: user_maintainer
@@ -124,7 +123,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
end
context 'GCP_PROJECT_IDs are defined' do
- it 'redirects to google_cloud home on generate pipeline error' do
+ it 'redirects to google_cloud deployments on generate pipeline error' do
allow_next_instance_of(GoogleCloud::EnableCloudRunService) do |enable_cloud_run_service|
allow(enable_cloud_run_service).to receive(:execute).and_return({ status: :success })
end
@@ -135,11 +134,11 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
get url
- expect(response).to redirect_to(project_google_cloud_index_path(project))
+ expect(response).to redirect_to(project_google_cloud_deployments_path(project))
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
- label: 'generate_pipeline_error',
+ label: 'error_generate_pipeline',
extra: { status: :error },
project: project,
user: user_maintainer
@@ -162,7 +161,7 @@ RSpec.describe Projects::GoogleCloud::DeploymentsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'deployments#cloud_run',
- label: 'cloud_run_success',
+ label: 'success',
extra: { "title": "Enable deployments to Cloud Run",
"description": "This merge request includes a Cloud Run deployment job in the pipeline definition (.gitlab-ci.yml).\n\nThe `deploy-to-cloud-run` job:\n* Requires the following environment variables\n * `GCP_PROJECT_ID`\n * `GCP_SERVICE_ACCOUNT_KEY`\n* Job definition can be found at: https://gitlab.com/gitlab-org/incubation-engineering/five-minute-production/library\n\nThis pipeline definition has been committed to the branch ``.\nYou may modify the pipeline definition further or accept the changes as-is if suitable.\n",
"source_project_id": project.id,
diff --git a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
index 56474b6520d..f88273080d5 100644
--- a/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/gcp_regions_controller_spec.rb
@@ -6,8 +6,8 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
let_it_be(:project) { create(:project, :public, :repository) }
let_it_be(:repository) { project.repository }
- let(:user_guest) { create(:user) }
- let(:user_maintainer) { create(:user) }
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
RSpec.shared_examples "should track not_found event" do
it "tracks event" do
@@ -15,7 +15,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: nil
@@ -29,7 +29,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: nil
@@ -43,7 +43,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'feature_flag_enabled!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'feature_flag_not_enabled',
project: project,
user: user_maintainer
@@ -57,7 +57,7 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'google_oauth2_enabled!',
- label: 'access_denied',
+ label: 'error_access_denied',
extra: { reason: 'google_oauth2_not_configured', config: config },
project: project,
user: user_maintainer
@@ -144,8 +144,8 @@ RSpec.describe Projects::GoogleCloud::GcpRegionsController do
sign_in(user_maintainer)
end
- it 'redirects to google cloud index' do
- is_expected.to redirect_to(project_google_cloud_index_path(project))
+ it 'redirects to google cloud configurations' do
+ is_expected.to redirect_to(project_google_cloud_configuration_path(project))
end
end
end
diff --git a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
index 07590d3710e..36441a184cb 100644
--- a/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/revoke_oauth_controller_spec.rb
@@ -47,13 +47,13 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do
post url
expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
- expect(response).to redirect_to(project_google_cloud_index_path(project))
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:notice]).to eq('Google OAuth2 token revocation requested')
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'revoke_oauth#create',
- label: 'create',
- property: 'success',
+ label: 'success',
+ property: '{}',
project: project,
user: user
)
@@ -70,13 +70,13 @@ RSpec.describe Projects::GoogleCloud::RevokeOauthController do
post url
expect(request.session[GoogleApi::CloudPlatform::Client.session_key_for_token]).to be_nil
- expect(response).to redirect_to(project_google_cloud_index_path(project))
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
expect(flash[:alert]).to eq('Google OAuth2 token revocation request failed')
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'revoke_oauth#create',
- label: 'create',
- property: 'failed',
+ label: 'error',
+ property: '{}',
project: project,
user: user
)
diff --git a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
index 4b32965e2b0..ae2519855db 100644
--- a/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
+++ b/spec/requests/projects/google_cloud/service_accounts_controller_spec.rb
@@ -8,13 +8,15 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
describe 'GET index', :snowplow do
let_it_be(:url) { "#{project_google_cloud_service_accounts_path(project)}" }
- let(:user_guest) { create(:user) }
- let(:user_developer) { create(:user) }
- let(:user_maintainer) { create(:user) }
- let(:user_creator) { project.creator }
+ let_it_be(:user_guest) { create(:user) }
+ let_it_be(:user_developer) { create(:user) }
+ let_it_be(:user_maintainer) { create(:user) }
+ let_it_be(:user_creator) { project.creator }
- let(:unauthorized_members) { [user_guest, user_developer] }
- let(:authorized_members) { [user_maintainer, user_creator] }
+ let_it_be(:unauthorized_members) { [user_guest, user_developer] }
+ let_it_be(:authorized_members) { [user_maintainer, user_creator] }
+
+ let_it_be(:google_client_error) { Google::Apis::ClientError.new('client-error') }
before do
project.add_guest(user_guest)
@@ -30,7 +32,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: nil
@@ -53,7 +55,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: unauthorized_member
@@ -71,7 +73,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
expect_snowplow_event(
category: 'Projects::GoogleCloud',
action: 'admin_project_google_cloud!',
- label: 'access_denied',
+ label: 'error_access_denied',
property: 'invalid_user',
project: project,
user: unauthorized_member
@@ -116,7 +118,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
end
end
- it 'renders no_gcp_projects' do
+ it 'flashes error and redirects to google cloud configurations' do
authorized_members.each do |authorized_member|
allow_next_instance_of(BranchesFinder) do |branches_finder|
allow(branches_finder).to receive(:execute).and_return([])
@@ -130,7 +132,16 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
get url
- expect(response).to render_template('projects/google_cloud/errors/no_gcp_projects')
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
+ expect(flash[:warning]).to eq('No Google Cloud projects - You need at least one Google Cloud project')
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'service_accounts#index',
+ label: 'error_form',
+ property: 'no_gcp_projects',
+ project: project,
+ user: authorized_member
+ )
end
end
end
@@ -171,7 +182,7 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
post url, params: { gcp_project: 'prj1', ref: 'env1' }
- expect(response).to redirect_to(project_google_cloud_index_path(project))
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
end
end
end
@@ -181,29 +192,47 @@ RSpec.describe Projects::GoogleCloud::ServiceAccountsController do
before do
allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
allow(client).to receive(:validate_token).and_return(true)
- allow(client).to receive(:list_projects).and_raise(Google::Apis::ClientError.new(''))
- allow(client).to receive(:create_service_account).and_raise(Google::Apis::ClientError.new(''))
- allow(client).to receive(:create_service_account_key).and_raise(Google::Apis::ClientError.new(''))
+ allow(client).to receive(:list_projects).and_raise(google_client_error)
+ allow(client).to receive(:create_service_account).and_raise(google_client_error)
+ allow(client).to receive(:create_service_account_key).and_raise(google_client_error)
end
end
- it 'renders gcp_error template on GET' do
+ it 'GET flashes error and redirects to -/google_cloud/configurations' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
get url
- expect(response).to render_template(:gcp_error)
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
+ expect(flash[:warning]).to eq('Google Cloud Error - client-error')
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'service_accounts#index',
+ label: 'error_gcp',
+ extra: google_client_error,
+ project: project,
+ user: authorized_member
+ )
end
end
- it 'renders gcp_error template on POST' do
+ it 'POST flashes error and redirects to -/google_cloud/configurations' do
authorized_members.each do |authorized_member|
sign_in(authorized_member)
post url, params: { gcp_project: 'prj1', environment: 'env1' }
- expect(response).to render_template(:gcp_error)
+ expect(response).to redirect_to(project_google_cloud_configuration_path(project))
+ expect(flash[:warning]).to eq('Google Cloud Error - client-error')
+ expect_snowplow_event(
+ category: 'Projects::GoogleCloud',
+ action: 'service_accounts#create',
+ label: 'error_gcp',
+ extra: google_client_error,
+ project: project,
+ user: authorized_member
+ )
end
end
end
diff --git a/spec/requests/projects/google_cloud_controller_spec.rb b/spec/requests/projects/google_cloud_controller_spec.rb
deleted file mode 100644
index d0814990989..00000000000
--- a/spec/requests/projects/google_cloud_controller_spec.rb
+++ /dev/null
@@ -1,178 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# Mock Types
-MockGoogleOAuth2Credentials = Struct.new(:app_id, :app_secret)
-
-RSpec.describe Projects::GoogleCloudController do
- let_it_be(:project) { create(:project, :public) }
-
- describe 'GET index', :snowplow do
- let_it_be(:url) { "#{project_google_cloud_index_path(project)}" }
-
- context 'when a public request is made' do
- it 'returns not found' do
- get url
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'access_denied',
- property: 'invalid_user',
- project: project,
- user: nil)
- end
- end
-
- context 'when a project.guest makes request' do
- let(:user) { create(:user) }
-
- it 'returns not found' do
- project.add_guest(user)
- sign_in(user)
-
- get url
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'access_denied',
- property: 'invalid_user',
- project: project,
- user: user
- )
- end
- end
-
- context 'when project.developer makes request' do
- let(:user) { create(:user) }
-
- it 'returns not found' do
- project.add_developer(user)
- sign_in(user)
-
- get url
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'admin_project_google_cloud!',
- label: 'access_denied',
- property: 'invalid_user',
- project: project,
- user: user
- )
- end
- end
-
- context 'when project.maintainer makes request' do
- let(:user) { create(:user) }
-
- it 'returns successful' do
- project.add_maintainer(user)
- sign_in(user)
-
- get url
-
- expect(response).to be_successful
- end
- end
-
- context 'when project.creator makes request' do
- let(:user) { project.creator }
-
- it 'returns successful' do
- sign_in(user)
-
- get url
-
- expect(response).to be_successful
- end
- end
-
- describe 'when authorized user makes request' do
- let(:user) { project.creator }
-
- context 'but gitlab instance is not configured for google oauth2' do
- it 'returns forbidden' do
- unconfigured_google_oauth2 = MockGoogleOAuth2Credentials.new('', '')
- allow(Gitlab::Auth::OAuth::Provider).to receive(:config_for)
- .with('google_oauth2')
- .and_return(unconfigured_google_oauth2)
-
- sign_in(user)
-
- get url
-
- expect(response).to have_gitlab_http_status(:forbidden)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_oauth2_enabled!',
- label: 'access_denied',
- extra: { reason: 'google_oauth2_not_configured',
- config: unconfigured_google_oauth2 },
- project: project,
- user: user
- )
- end
- end
-
- context 'but feature flag is disabled' do
- before do
- stub_feature_flags(incubation_5mp_google_cloud: false)
- end
-
- it 'returns not found' do
- sign_in(user)
-
- get url
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'feature_flag_enabled!',
- label: 'access_denied',
- property: 'feature_flag_not_enabled',
- project: project,
- user: user
- )
- end
- end
-
- context 'but google oauth2 token is not valid' do
- it 'does not return revoke oauth url' do
- allow_next_instance_of(GoogleApi::CloudPlatform::Client) do |client|
- allow(client).to receive(:validate_token).and_return(false)
- end
-
- sign_in(user)
-
- get url
-
- expect(response).to be_successful
- expect_snowplow_event(
- category: 'Projects::GoogleCloud',
- action: 'google_cloud#index',
- label: 'index',
- extra: {
- screen: 'home',
- serviceAccounts: [],
- createServiceAccountUrl: project_google_cloud_service_accounts_path(project),
- enableCloudRunUrl: project_google_cloud_deployments_cloud_run_path(project),
- enableCloudStorageUrl: project_google_cloud_deployments_cloud_storage_path(project),
- emptyIllustrationUrl: ActionController::Base.helpers.image_path('illustrations/pipelines_empty.svg'),
- configureGcpRegionsUrl: project_google_cloud_gcp_regions_path(project),
- gcpRegions: [],
- revokeOauthUrl: nil
- },
- project: project,
- user: user
- )
- end
- end
- end
- end
-end
diff --git a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
index e2f5a2e719e..b2cd5632be0 100644
--- a/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
+++ b/spec/services/google_cloud/gcp_region_add_or_replace_service_spec.rb
@@ -11,7 +11,7 @@ RSpec.describe GoogleCloud::GcpRegionAddOrReplaceService do
service.execute('env_2', 'loc_2')
service.execute('env_1', 'loc_3')
- list = project.variables.reload.filter { |variable| variable.key == Projects::GoogleCloudController::GCP_REGION_CI_VAR_KEY }
+ list = project.variables.reload.filter { |variable| variable.key == Projects::GoogleCloud::GcpRegionsController::GCP_REGION_CI_VAR_KEY }
list = list.sort_by(&:environment_scope)
aggregate_failures 'testing list of gcp regions' do
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index f0d7f570c19..fbcca215282 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -439,6 +439,7 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
before do
TestEnv.clean_test_path
create(:group_member, :owner, group: new_parent_group, user: user)
+ allow(transfer_service).to receive(:update_project_settings)
transfer_service.execute(new_parent_group)
end
@@ -478,6 +479,11 @@ RSpec.describe Groups::TransferService, :sidekiq_inline do
end
end
+ it 'invokes #update_project_settings' do
+ expect(transfer_service).to have_received(:update_project_settings)
+ .with(group.projects.pluck(:id))
+ end
+
it_behaves_like 'project namespace path is in sync with project path' do
let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
let(:projects_with_project_namespace) { [project1, project2] }
diff --git a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
index a479f3a82ff..e3c2466f807 100644
--- a/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
+++ b/spec/views/layouts/nav/sidebar/_project.html.haml_spec.rb
@@ -532,7 +532,7 @@ RSpec.describe 'layouts/nav/sidebar/_project' do
describe 'Google Cloud' do
it 'has a link to the google cloud page' do
render
- expect(rendered).to have_link('Google Cloud', href: project_google_cloud_index_path(project))
+ expect(rendered).to have_link('Google Cloud', href: project_google_cloud_configuration_path(project))
end
describe 'when the user does not have access' do