summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue2
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue2
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue18
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue16
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue4
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue2
-rw-r--r--app/assets/javascripts/ide/components/branches/search_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/file_templates/dropdown.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/preview/clientside.vue2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js15
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue1
-rw-r--r--app/assets/javascripts/monitoring/components/panel_type.vue4
-rw-r--r--app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql13
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js49
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js10
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue2
-rw-r--r--app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue2
-rw-r--r--app/assets/javascripts/serverless/components/functions.vue4
-rw-r--r--app/assets/javascripts/smart_interval.js27
-rw-r--r--app/assets/javascripts/snippets/components/show.vue33
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue2
-rw-r--r--app/assets/javascripts/snippets/mixins/snippets.js39
-rw-r--r--app/assets/javascripts/static_site_editor/components/static_site_editor.vue4
-rw-r--r--app/assets/javascripts/static_site_editor/store/getters.js2
-rw-r--r--app/assets/javascripts/static_site_editor/store/mutations.js1
-rw-r--r--app/assets/javascripts/static_site_editor/store/state.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue2
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb19
-rw-r--r--app/controllers/projects/merge_requests_controller.rb1
-rw-r--r--app/mailers/emails/pages_domains.rb11
-rw-r--r--app/models/application_setting_implementation.rb1
-rw-r--r--app/models/ci/job_artifact.rb15
-rw-r--r--app/models/diff_note_position.rb36
-rw-r--r--app/models/lfs_object.rb13
-rw-r--r--app/services/clusters/create_service.rb7
-rw-r--r--app/services/clusters/management/validate_management_project_permissions_service.rb54
-rw-r--r--app/services/clusters/update_service.rb41
-rw-r--r--app/services/environments/auto_stop_service.rb2
-rw-r--r--app/services/notification_service.rb6
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb2
-rw-r--r--app/uploaders/records_uploads.rb23
-rw-r--r--app/views/admin/application_settings/_issue_limits.html.haml9
-rw-r--r--app/views/admin/application_settings/network.html.haml11
-rw-r--r--app/views/admin/deploy_keys/index.html.haml2
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.html.haml11
-rw-r--r--app/views/notify/pages_domain_auto_ssl_failed_email.text.haml7
-rw-r--r--app/views/profiles/emails/index.html.haml2
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml4
-rw-r--r--app/workers/environments/auto_stop_cron_worker.rb2
-rw-r--r--changelogs/unreleased/211998-add-cluster-mangement-id-on-create.yml6
-rw-r--r--changelogs/unreleased/212561-fix-empty-edit-area.yml5
-rw-r--r--changelogs/unreleased/213299-env-autostop-bug.yml5
-rw-r--r--changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml6
-rw-r--r--changelogs/unreleased/213799-optimize-usage_activity_by_stage-projects_with_repositories_enable.yml5
-rw-r--r--changelogs/unreleased/55241-rate-limit-issue-creation.yml5
-rw-r--r--changelogs/unreleased/bvl-remove-sidekiq-deduplication-feature-flag.yml5
-rw-r--r--changelogs/unreleased/ph-210377-increaseMrPollTimes.yml5
-rw-r--r--changelogs/unreleased/vs-migrate-deprecated-size-in-loading-icon.yml5
-rw-r--r--db/migrate/20200325111432_add_issues_create_limit_to_application_settings.rb9
-rw-r--r--db/migrate/20200326122700_create_diff_note_positions.rb31
-rw-r--r--db/migrate/20200406165950_add_not_null_constraint_on_file_store_to_lfs_objects.rb24
-rw-r--r--db/migrate/20200406171857_add_not_null_constraint_on_file_store_to_ci_job_artifacts.rb24
-rw-r--r--db/migrate/20200406172135_add_not_null_constraint_on_file_store_to_uploads.rb24
-rw-r--r--db/migrate/20200408153842_add_index_on_creator_id_and_id_on_projects.rb17
-rw-r--r--db/structure.sql52
-rw-r--r--doc/api/group_clusters.md1
-rw-r--r--doc/api/project_clusters.md1
-rw-r--r--doc/user/project/repository/forking_workflow.md3
-rw-r--r--lib/api/group_clusters.rb1
-rw-r--r--lib/api/project_clusters.rb1
-rw-r--r--lib/gitlab/application_rate_limiter.rb5
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs.rb24
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb2
-rw-r--r--locale/gitlab.pot24
-rw-r--r--package.json2
-rw-r--r--rubocop/cop/performance/ar_count_each.rb45
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb42
-rw-r--r--spec/factories/ci/job_artifacts.rb2
-rw-r--r--spec/factories/diff_note_positions.rb10
-rw-r--r--spec/features/projects/environments/environments_spec.rb14
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js1
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js111
-rw-r--r--spec/frontend/smart_interval_spec.js197
-rw-r--r--spec/frontend/static_site_editor/components/static_site_editor_spec.js9
-rw-r--r--spec/frontend/static_site_editor/store/getters_spec.js12
-rw-r--r--spec/frontend/static_site_editor/store/mutations_spec.js1
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js19
-rw-r--r--spec/javascripts/smart_interval_spec.js234
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb20
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb31
-rw-r--r--spec/mailers/emails/pages_domains_spec.rb31
-rw-r--r--spec/models/ci/job_artifact_spec.rb21
-rw-r--r--spec/models/diff_note_position_spec.rb22
-rw-r--r--spec/requests/api/group_clusters_spec.rb17
-rw-r--r--spec/requests/api/project_clusters_spec.rb22
-rw-r--r--spec/rubocop/cop/performance/ar_count_each_spec.rb62
-rw-r--r--spec/services/application_settings/update_service_spec.rb16
-rw-r--r--spec/services/clusters/create_service_spec.rb88
-rw-r--r--spec/services/clusters/management/validate_management_project_permissions_service_spec.rb88
-rw-r--r--spec/services/environments/auto_stop_service_spec.rb12
-rw-r--r--spec/services/notification_service_spec.rb1
-rw-r--r--spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb8
-rw-r--r--spec/uploaders/records_uploads_spec.rb6
-rw-r--r--spec/views/projects/pages/show.html.haml_spec.rb2
-rw-r--r--yarn.lock8
119 files changed, 1482 insertions, 553 deletions
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index d2767dd6c64..04c2d4a7493 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -28,7 +28,7 @@ export default {
{{ s__('Badges|Your badges') }}
<span v-show="!isLoading" class="badge badge-pill">{{ badges.length }}</span>
</div>
- <gl-loading-icon v-show="isLoading" :size="2" class="card-body" />
+ <gl-loading-icon v-show="isLoading" size="lg" class="card-body" />
<div v-if="hasNoBadges" class="card-body">
<span v-if="isGroupBadge">{{ s__('Badges|This group has no badges') }}</span>
<span v-else>{{ s__('Badges|This project has no badges') }}</span>
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 19516a13d15..3de1b2f0707 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -197,7 +197,7 @@ export default {
<template>
<div>
<div v-if="loading" class="contributors-loader text-center">
- <gl-loading-icon :inline="true" :size="4" />
+ <gl-loading-icon :inline="true" size="xl" />
</div>
<div v-else-if="showChart" class="contributors-charts">
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 048f3a2485c..5505704f430 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -119,7 +119,7 @@ export default {
<gl-loading-icon
v-if="isLoading && !hasKeys"
:label="s__('DeployKeys|Loading deploy keys')"
- :size="2"
+ size="lg"
/>
<template v-else-if="hasKeys">
<div class="top-area scrolling-tabs-container inner-page-scroll-tabs">
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 305d860a692..335c668474e 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -58,12 +58,6 @@ export default {
required: true,
},
- shouldShowAutoStopDate: {
- type: Boolean,
- required: false,
- default: false,
- },
-
tableData: {
type: Object,
required: true,
@@ -638,12 +632,7 @@ export default {
</span>
</div>
- <div
- v-if="!isFolder && shouldShowAutoStopDate"
- class="table-section"
- :class="tableData.autoStop.spacing"
- role="gridcell"
- >
+ <div v-if="!isFolder" class="table-section" :class="tableData.autoStop.spacing" role="gridcell">
<div role="rowheader" class="table-mobile-header">{{ tableData.autoStop.title }}</div>
<span
v-if="canShowAutoStopDate"
@@ -662,10 +651,7 @@ export default {
role="gridcell"
>
<div class="btn-group table-action-buttons" role="group">
- <pin-component
- v-if="canShowAutoStopDate && shouldShowAutoStopDate"
- :auto-stop-url="autoStopUrl"
- />
+ <pin-component v-if="canShowAutoStopDate" :auto-stop-url="autoStopUrl" />
<external-url-component
v-if="externalURL && canReadEnvironment"
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index 01a00e03814..89e40faa23e 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -6,7 +6,6 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { flow, reverse, sortBy } from 'lodash/fp';
import environmentTableMixin from 'ee_else_ce/environments/mixins/environments_table_mixin';
import { s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import EnvironmentItem from './environment_item.vue';
export default {
@@ -17,7 +16,7 @@ export default {
CanaryDeploymentCallout: () =>
import('ee_component/environments/components/canary_deployment_callout.vue'),
},
- mixins: [environmentTableMixin, glFeatureFlagsMixin()],
+ mixins: [environmentTableMixin],
props: {
environments: {
type: Array,
@@ -43,9 +42,6 @@ export default {
: env,
);
},
- shouldShowAutoStopDate() {
- return this.glFeatures.autoStopEnvironments;
- },
tableData() {
return {
// percent spacing for cols, should add up to 100
@@ -74,7 +70,7 @@ export default {
spacing: 'section-5',
},
actions: {
- spacing: this.shouldShowAutoStopDate ? 'section-25' : 'section-30',
+ spacing: 'section-25',
},
};
},
@@ -131,12 +127,7 @@ export default {
<div class="table-section" :class="tableData.date.spacing" role="columnheader">
{{ tableData.date.title }}
</div>
- <div
- v-if="shouldShowAutoStopDate"
- class="table-section"
- :class="tableData.autoStop.spacing"
- role="columnheader"
- >
+ <div class="table-section" :class="tableData.autoStop.spacing" role="columnheader">
{{ tableData.autoStop.title }}
</div>
</div>
@@ -146,7 +137,6 @@ export default {
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
- :should-show-auto-stop-date="shouldShowAutoStopDate"
:table-data="tableData"
/>
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index a8103c80da0..148edfe3a51 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -225,7 +225,7 @@ export default {
<template>
<div>
<div v-if="errorLoading" class="py-3">
- <gl-loading-icon :size="3" />
+ <gl-loading-icon size="lg" />
</div>
<div v-else-if="error" class="error-details">
<gl-alert v-if="isAlertVisible" @dismiss="isAlertVisible = false">
@@ -405,7 +405,7 @@ export default {
</ul>
<div v-if="loadingStacktrace" class="py-3">
- <gl-loading-icon :size="3" />
+ <gl-loading-icon size="lg" />
</div>
<template v-else-if="showStacktrace">
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
index 2ffecce0a56..1f1776a5487 100644
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ b/app/assets/javascripts/frequent_items/components/app.vue
@@ -107,7 +107,7 @@ export default {
<gl-loading-icon
v-if="isLoadingItems"
:label="translations.loadingMessage"
- :size="2"
+ size="lg"
class="loading-animation prepend-top-20"
/>
<div v-if="!isLoadingItems && !hasSearchQuery" class="section-header">
diff --git a/app/assets/javascripts/ide/components/branches/search_list.vue b/app/assets/javascripts/ide/components/branches/search_list.vue
index 76821bcd986..dd2d726d525 100644
--- a/app/assets/javascripts/ide/components/branches/search_list.vue
+++ b/app/assets/javascripts/ide/components/branches/search_list.vue
@@ -72,7 +72,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
v-if="isLoading"
- :size="2"
+ size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/>
<ul v-else class="mb-0 w-100">
diff --git a/app/assets/javascripts/ide/components/file_templates/dropdown.vue b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
index 35e5f9bcf69..d80662f6ae1 100644
--- a/app/assets/javascripts/ide/components/file_templates/dropdown.vue
+++ b/app/assets/javascripts/ide/components/file_templates/dropdown.vue
@@ -88,7 +88,7 @@ export default {
<i aria-hidden="true" class="fa fa-search dropdown-input-search"></i>
</div>
<div class="dropdown-content">
- <gl-loading-icon v-if="showLoading" :size="2" />
+ <gl-loading-icon v-if="showLoading" size="lg" />
<ul v-else>
<li v-for="(item, index) in outputData" :key="index">
<button type="button" @click="clickItem(item)">{{ item.name }}</button>
diff --git a/app/assets/javascripts/ide/components/jobs/list.vue b/app/assets/javascripts/ide/components/jobs/list.vue
index 2cb5050c3f0..b97b7289886 100644
--- a/app/assets/javascripts/ide/components/jobs/list.vue
+++ b/app/assets/javascripts/ide/components/jobs/list.vue
@@ -26,7 +26,7 @@ export default {
<template>
<div>
- <gl-loading-icon v-if="loading && !stages.length" :size="2" class="prepend-top-default" />
+ <gl-loading-icon v-if="loading && !stages.length" size="lg" class="prepend-top-default" />
<template v-else>
<stage
v-for="stage in stages"
diff --git a/app/assets/javascripts/ide/components/merge_requests/list.vue b/app/assets/javascripts/ide/components/merge_requests/list.vue
index 15c08988977..bf2a33be653 100644
--- a/app/assets/javascripts/ide/components/merge_requests/list.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/list.vue
@@ -90,7 +90,7 @@ export default {
<div class="dropdown-content ide-merge-requests-dropdown-content d-flex">
<gl-loading-icon
v-if="isLoading"
- :size="2"
+ size="lg"
class="mt-3 mb-3 align-self-center ml-auto mr-auto"
/>
<template v-else>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 343b0b6e90c..d3e5add2e83 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -56,7 +56,7 @@ export default {
<template>
<div class="ide-pipeline">
- <gl-loading-icon v-if="showLoadingIcon" :size="2" class="prepend-top-default" />
+ <gl-loading-icon v-if="showLoadingIcon" size="lg" class="prepend-top-default" />
<template v-else-if="hasLoadedPipeline">
<header v-if="latestPipeline" class="ide-tree-header ide-pipeline-header">
<ci-icon :status="latestPipeline.details.status" :size="24" class="d-flex" />
diff --git a/app/assets/javascripts/ide/components/preview/clientside.vue b/app/assets/javascripts/ide/components/preview/clientside.vue
index 86a773499bc..3852f2fdfa4 100644
--- a/app/assets/javascripts/ide/components/preview/clientside.vue
+++ b/app/assets/javascripts/ide/components/preview/clientside.vue
@@ -176,6 +176,6 @@ export default {
{{ s__('IDE|Get started with Live Preview') }}
</a>
</div>
- <gl-loading-icon v-else :size="2" class="align-self-center mt-auto mb-auto" />
+ <gl-loading-icon v-else size="lg" class="align-self-center mt-auto mb-auto" />
</div>
</template>
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9b0ee40a30a..4a48852159a 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -910,3 +910,18 @@ export const setCookie = (name, value) => Cookies.set(name, value, { expires: 36
export const getCookie = name => Cookies.get(name);
export const removeCookie = name => Cookies.remove(name);
+
+/**
+ * Returns the status of a feature flag.
+ * Currently, there is no way to access feature
+ * flags in Vuex other than directly tapping into
+ * window.gon.
+ *
+ * This should only be used on Vuex. If feature flags
+ * need to be accessed in Vue components consider
+ * using the Vue feature flag mixin.
+ *
+ * @param {String} flag Feature flag
+ * @returns {Boolean} on/off
+ */
+export const isFeatureFlagEnabled = flag => window.gon.features?.[flag];
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 24aa8480ce4..9041b01088c 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -55,6 +55,11 @@ export default {
required: false,
default: () => [],
},
+ annotations: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
projectPath: {
type: String,
required: false,
@@ -143,6 +148,7 @@ export default {
return (this.option.series || []).concat(
generateAnnotationsSeries({
deployments: this.recentDeployments,
+ annotations: this.annotations,
}),
);
},
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 15b17f01daf..4586ce70ad6 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -213,7 +213,6 @@ export default {
'dashboard',
'emptyState',
'showEmptyState',
- 'deploymentData',
'useDashboardEndpoint',
'allDashboards',
'additionalPanelTypesEnabled',
diff --git a/app/assets/javascripts/monitoring/components/panel_type.vue b/app/assets/javascripts/monitoring/components/panel_type.vue
index d1394bca447..676fc0cca64 100644
--- a/app/assets/javascripts/monitoring/components/panel_type.vue
+++ b/app/assets/javascripts/monitoring/components/panel_type.vue
@@ -89,6 +89,9 @@ export default {
deploymentData(state) {
return state[this.namespace].deploymentData;
},
+ annotations(state) {
+ return state[this.namespace].annotations;
+ },
projectPath(state) {
return state[this.namespace].projectPath;
},
@@ -310,6 +313,7 @@ export default {
ref="timeChart"
:graph-data="graphData"
:deployment-data="deploymentData"
+ :annotations="annotations"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
diff --git a/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
new file mode 100644
index 00000000000..e2edaa707b2
--- /dev/null
+++ b/app/assets/javascripts/monitoring/queries/getAnnotations.query.graphql
@@ -0,0 +1,13 @@
+query getAnnotations($projectPath: ID!) {
+ environment(name: $environmentName) {
+ metricDashboard(id: $dashboardId) {
+ annotations: nodes {
+ id
+ description
+ from
+ to
+ panelId
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 06b99f572e7..5b2bd1f1493 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -6,8 +6,13 @@ import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { gqClient, parseEnvironmentsResponse, removeLeadingSlash } from './utils';
import trackDashboardLoad from '../monitoring_tracking_helper';
import getEnvironments from '../queries/getEnvironments.query.graphql';
+import getAnnotations from '../queries/getAnnotations.query.graphql';
import statusCodes from '../../lib/utils/http_status';
-import { backOff, convertObjectPropsToCamelCase } from '../../lib/utils/common_utils';
+import {
+ backOff,
+ convertObjectPropsToCamelCase,
+ isFeatureFlagEnabled,
+} from '../../lib/utils/common_utils';
import { s__, sprintf } from '../../locale';
import { PROMETHEUS_TIMEOUT, ENVIRONMENT_AVAILABLE_STATE } from '../constants';
@@ -80,6 +85,14 @@ export const setShowErrorBanner = ({ commit }, enabled) => {
export const fetchData = ({ dispatch }) => {
dispatch('fetchEnvironmentsData');
dispatch('fetchDashboard');
+ /**
+ * Annotations data is not yet fetched. This will be
+ * ready after the BE piece is implemented.
+ * https://gitlab.com/gitlab-org/gitlab/-/issues/211330
+ */
+ if (isFeatureFlagEnabled('metrics_dashboard_annotations')) {
+ dispatch('fetchAnnotations');
+ }
};
// Metrics dashboard
@@ -269,6 +282,40 @@ export const receiveEnvironmentsDataFailure = ({ commit }) => {
commit(types.RECEIVE_ENVIRONMENTS_DATA_FAILURE);
};
+export const fetchAnnotations = ({ state, dispatch }) => {
+ dispatch('requestAnnotations');
+
+ return gqClient
+ .mutate({
+ mutation: getAnnotations,
+ variables: {
+ projectPath: removeLeadingSlash(state.projectPath),
+ dashboardId: state.currentDashboard,
+ environmentName: state.currentEnvironmentName,
+ },
+ })
+ .then(resp => resp.data?.project?.environment?.metricDashboard?.annotations)
+ .then(annotations => {
+ if (!annotations) {
+ createFlash(s__('Metrics|There was an error fetching annotations. Please try again.'));
+ }
+
+ dispatch('receiveAnnotationsSuccess', annotations);
+ })
+ .catch(err => {
+ Sentry.captureException(err);
+ dispatch('receiveAnnotationsFailure');
+ createFlash(s__('Metrics|There was an error getting annotations information.'));
+ });
+};
+
+// While this commit does not update the state it will
+// eventually be useful to show a loading state
+export const requestAnnotations = ({ commit }) => commit(types.REQUEST_ANNOTATIONS);
+export const receiveAnnotationsSuccess = ({ commit }, data) =>
+ commit(types.RECEIVE_ANNOTATIONS_SUCCESS, data);
+export const receiveAnnotationsFailure = ({ commit }) => commit(types.RECEIVE_ANNOTATIONS_FAILURE);
+
// Dashboard manipulation
/**
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index 9a3489d53d7..2f9955da1b1 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -3,6 +3,11 @@ export const REQUEST_METRICS_DASHBOARD = 'REQUEST_METRICS_DASHBOARD';
export const RECEIVE_METRICS_DASHBOARD_SUCCESS = 'RECEIVE_METRICS_DASHBOARD_SUCCESS';
export const RECEIVE_METRICS_DASHBOARD_FAILURE = 'RECEIVE_METRICS_DASHBOARD_FAILURE';
+// Annotations
+export const REQUEST_ANNOTATIONS = 'REQUEST_ANNOTATIONS';
+export const RECEIVE_ANNOTATIONS_SUCCESS = 'RECEIVE_ANNOTATIONS_SUCCESS';
+export const RECEIVE_ANNOTATIONS_FAILURE = 'RECEIVE_ANNOTATIONS_FAILURE';
+
// Git project deployments
export const REQUEST_DEPLOYMENTS_DATA = 'REQUEST_DEPLOYMENTS_DATA';
export const RECEIVE_DEPLOYMENTS_DATA_SUCCESS = 'RECEIVE_DEPLOYMENTS_DATA_SUCCESS';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 38c1524d904..aa31b6642d7 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -93,6 +93,16 @@ export default {
},
/**
+ * Annotations
+ */
+ [types.RECEIVE_ANNOTATIONS_SUCCESS](state, annotations) {
+ state.annotations = annotations;
+ },
+ [types.RECEIVE_ANNOTATIONS_FAILURE](state) {
+ state.annotations = [];
+ },
+
+ /**
* Individual panel/metric results
*/
[types.REQUEST_METRIC_RESULT](state, { metricId }) {
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index 2b1907e8df7..e60510e747b 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -20,6 +20,7 @@ export default () => ({
allDashboards: [],
// Other project data
+ annotations: [],
deploymentData: [],
environments: [],
environmentsSearchTerm: '',
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index ef3f4d0e3f6..1ff5b662d18 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -135,7 +135,7 @@ export default {
paddingRight: `${graphRightPadding}px`,
}"
>
- <gl-loading-icon v-if="isLoading" class="m-auto" :size="3" />
+ <gl-loading-icon v-if="isLoading" class="m-auto" size="lg" />
<pipeline-graph
v-if="pipelineTypeUpstream"
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 2a3d022c5cd..e7777d0d3af 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -108,7 +108,7 @@ export default {
/>
</ci-header>
- <gl-loading-icon v-if="isLoading" :size="2" class="prepend-top-default append-bottom-default" />
+ <gl-loading-icon v-if="isLoading" size="lg" class="prepend-top-default append-bottom-default" />
<gl-modal
:modal-id="$options.DELETE_MODAL_ID"
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index accd6bf71f4..d4f23697e09 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -271,7 +271,7 @@ export default {
<gl-loading-icon
v-if="stateToRender === $options.stateMap.loading"
:label="s__('Pipelines|Loading Pipelines')"
- :size="3"
+ size="lg"
class="prepend-top-20"
/>
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index f1106dc6ae9..571d305a50c 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -94,7 +94,7 @@ export default {
</script>
<template>
<div class="ci-status-link">
- <gl-loading-icon v-if="isLoading" :size="3" label="Loading pipeline status" />
+ <gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
<ci-icon
v-tooltip
diff --git a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
index c90478db620..807f10bd9c6 100644
--- a/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
+++ b/app/assets/javascripts/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
@@ -36,7 +36,7 @@ export default {
</div>
</div>
<div v-if="loadingStacktrace" class="card">
- <gl-loading-icon class="py-2" label="Fetching stack trace" :size="1" />
+ <gl-loading-icon class="py-2" label="Fetching stack trace" size="sm" />
</div>
<stacktrace v-else :entries="stacktrace" />
</div>
diff --git a/app/assets/javascripts/serverless/components/functions.vue b/app/assets/javascripts/serverless/components/functions.vue
index e06149f2bcb..2b1291ac70f 100644
--- a/app/assets/javascripts/serverless/components/functions.vue
+++ b/app/assets/javascripts/serverless/components/functions.vue
@@ -77,7 +77,7 @@ export default {
<section id="serverless-functions" class="flex-grow">
<gl-loading-icon
v-if="checkingInstalled"
- :size="2"
+ size="lg"
class="prepend-top-default append-bottom-default"
/>
@@ -97,7 +97,7 @@ export default {
</template>
<gl-loading-icon
v-if="isLoading"
- :size="2"
+ size="lg"
class="prepend-top-default append-bottom-default js-functions-loader"
/>
</div>
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 8ca590123ae..0e52d2d8010 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -33,7 +33,7 @@ export default class SmartInterval {
this.state = {
intervalId: null,
currentInterval: this.cfg.startingInterval,
- pageVisibility: 'visible',
+ pagevisibile: true,
};
this.initInterval();
@@ -91,8 +91,10 @@ export default class SmartInterval {
}
destroy() {
+ document.removeEventListener('visibilitychange', this.onVisibilityChange);
+ window.removeEventListener('blur', this.onWindowVisibilityChange);
+ window.removeEventListener('focus', this.onWindowVisibilityChange);
this.cancel();
- document.removeEventListener('visibilitychange', this.handleVisibilityChange);
$(document)
.off('visibilitychange')
.off('beforeunload');
@@ -124,9 +126,21 @@ export default class SmartInterval {
});
}
+ onWindowVisibilityChange(e) {
+ this.state.pagevisibile = e.type === 'focus';
+ this.handleVisibilityChange();
+ }
+
+ onVisibilityChange(e) {
+ this.state.pagevisibile = e.target.visibilityState === 'visible';
+ this.handleVisibilityChange();
+ }
+
initVisibilityChangeHandling() {
- // cancel interval when tab no longer shown (prevents cached pages from polling)
- document.addEventListener('visibilitychange', this.handleVisibilityChange.bind(this));
+ // cancel interval when tab or window is no longer shown (prevents cached pages from polling)
+ document.addEventListener('visibilitychange', this.onVisibilityChange.bind(this));
+ window.addEventListener('blur', this.onWindowVisibilityChange.bind(this));
+ window.addEventListener('focus', this.onWindowVisibilityChange.bind(this));
}
initPageUnloadHandling() {
@@ -135,8 +149,7 @@ export default class SmartInterval {
$(document).on('beforeunload', () => this.cancel());
}
- handleVisibilityChange(e) {
- this.state.pageVisibility = e.target.visibilityState;
+ handleVisibilityChange() {
const intervalAction = this.isPageVisible()
? this.onVisibilityVisible
: this.onVisibilityHidden;
@@ -166,7 +179,7 @@ export default class SmartInterval {
}
isPageVisible() {
- return this.state.pageVisibility === 'visible';
+ return this.state.pagevisibile;
}
stopTimer() {
diff --git a/app/assets/javascripts/snippets/components/show.vue b/app/assets/javascripts/snippets/components/show.vue
index e98f56d87f5..bc0034d397e 100644
--- a/app/assets/javascripts/snippets/components/show.vue
+++ b/app/assets/javascripts/snippets/components/show.vue
@@ -1,10 +1,11 @@
<script>
-import GetSnippetQuery from '../queries/snippet.query.graphql';
import SnippetHeader from './snippet_header.vue';
import SnippetTitle from './snippet_title.vue';
import SnippetBlob from './snippet_blob_view.vue';
import { GlLoadingIcon } from '@gitlab/ui';
+import { getSnippetMixin } from '../mixins/snippets';
+
export default {
components: {
SnippetHeader,
@@ -12,33 +13,7 @@ export default {
GlLoadingIcon,
SnippetBlob,
},
- apollo: {
- snippet: {
- query: GetSnippetQuery,
- variables() {
- return {
- ids: this.snippetGid,
- };
- },
- update: data => data.snippets.edges[0].node,
- },
- },
- props: {
- snippetGid: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- snippet: {},
- };
- },
- computed: {
- isLoading() {
- return this.$apollo.queries.snippet.loading;
- },
- },
+ mixins: [getSnippetMixin],
};
</script>
<template>
@@ -46,7 +21,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
- :size="2"
+ size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<template v-else>
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index ae6f451df18..44b4607e5a9 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -37,7 +37,7 @@ export default {
<gl-loading-icon
v-if="isLoading"
:label="__('Loading snippet')"
- :size="2"
+ size="lg"
class="loading-animation prepend-top-20 append-bottom-20"
/>
<blob-content-edit
diff --git a/app/assets/javascripts/snippets/mixins/snippets.js b/app/assets/javascripts/snippets/mixins/snippets.js
new file mode 100644
index 00000000000..837c41cdf6b
--- /dev/null
+++ b/app/assets/javascripts/snippets/mixins/snippets.js
@@ -0,0 +1,39 @@
+import GetSnippetQuery from '../queries/snippet.query.graphql';
+
+export const getSnippetMixin = {
+ apollo: {
+ snippet: {
+ query: GetSnippetQuery,
+ variables() {
+ return {
+ ids: this.snippetGid,
+ };
+ },
+ update: data => data.snippets.edges[0]?.node,
+ result(res) {
+ if (this.onSnippetFetch) {
+ this.onSnippetFetch(res);
+ }
+ },
+ },
+ },
+ props: {
+ snippetGid: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ snippet: {},
+ newSnippet: false,
+ };
+ },
+ computed: {
+ isLoading() {
+ return this.$apollo.queries.snippet.loading;
+ },
+ },
+};
+
+export default () => {};
diff --git a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue
index e711510ba44..8deae2f2c8a 100644
--- a/app/assets/javascripts/static_site_editor/components/static_site_editor.vue
+++ b/app/assets/javascripts/static_site_editor/components/static_site_editor.vue
@@ -12,8 +12,8 @@ export default {
Toolbar,
},
computed: {
- ...mapState(['content', 'isLoadingContent', 'isSavingChanges']),
- ...mapGetters(['isContentLoaded', 'contentChanged']),
+ ...mapState(['content', 'isLoadingContent', 'isSavingChanges', 'isContentLoaded']),
+ ...mapGetters(['contentChanged']),
},
mounted() {
this.loadContent();
diff --git a/app/assets/javascripts/static_site_editor/store/getters.js b/app/assets/javascripts/static_site_editor/store/getters.js
index 41256201c26..ebc68f8e9e6 100644
--- a/app/assets/javascripts/static_site_editor/store/getters.js
+++ b/app/assets/javascripts/static_site_editor/store/getters.js
@@ -1,2 +1,2 @@
-export const isContentLoaded = ({ originalContent }) => Boolean(originalContent);
+// eslint-disable-next-line import/prefer-default-export
export const contentChanged = ({ originalContent, content }) => originalContent !== content;
diff --git a/app/assets/javascripts/static_site_editor/store/mutations.js b/app/assets/javascripts/static_site_editor/store/mutations.js
index f98177bbc18..4727d04439c 100644
--- a/app/assets/javascripts/static_site_editor/store/mutations.js
+++ b/app/assets/javascripts/static_site_editor/store/mutations.js
@@ -6,6 +6,7 @@ export default {
},
[types.RECEIVE_CONTENT_SUCCESS](state, { title, content }) {
state.isLoadingContent = false;
+ state.isContentLoaded = true;
state.title = title;
state.content = content;
state.originalContent = content;
diff --git a/app/assets/javascripts/static_site_editor/store/state.js b/app/assets/javascripts/static_site_editor/store/state.js
index 477ec540e02..d48cc8ed1a4 100644
--- a/app/assets/javascripts/static_site_editor/store/state.js
+++ b/app/assets/javascripts/static_site_editor/store/state.js
@@ -6,6 +6,8 @@ const createState = (initialState = {}) => ({
isLoadingContent: false,
isSavingChanges: false,
+ isContentLoaded: false,
+
originalContent: '',
content: '',
title: '',
diff --git a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
index 1bc28b15f74..05f73c4cdaf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -214,8 +214,6 @@ export default {
return new MRWidgetService(this.getServiceEndpoints(store));
},
checkStatus(cb, isRebased) {
- if (document.visibilityState !== 'visible') return Promise.resolve();
-
return this.service
.checkStatus()
.then(({ data }) => {
@@ -238,10 +236,10 @@ export default {
initPolling() {
this.pollingInterval = new SmartInterval({
callback: this.checkStatus,
- startingInterval: 10000,
- maxInterval: 30000,
- hiddenInterval: 120000,
- incrementByFactorOf: 5000,
+ startingInterval: 10 * 1000,
+ maxInterval: 240 * 1000,
+ hiddenInterval: window.gon?.features?.widgetVisibilityPolling && 360 * 1000,
+ incrementByFactorOf: 2,
});
},
initDeploymentsPolling() {
@@ -253,10 +251,9 @@ export default {
deploymentsPoll(callback) {
return new SmartInterval({
callback,
- startingInterval: 30000,
- maxInterval: 120000,
- hiddenInterval: 240000,
- incrementByFactorOf: 15000,
+ startingInterval: 30 * 1000,
+ maxInterval: 240 * 1000,
+ incrementByFactorOf: 4,
immediateExecution: true,
});
},
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 30a9633b6dc..fd45ac52647 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -80,7 +80,7 @@ export default {
@input="onInput"
/>
<div class="d-flex flex-column">
- <gl-loading-icon v-if="showLoadingIndicator" :size="1" class="py-2 px-4" />
+ <gl-loading-icon v-if="showLoadingIndicator" size="sm" class="py-2 px-4" />
<gl-infinite-scroll
:max-list-height="402"
:fetched-items="projectSearchResults.length"
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 210d488f5a3..16254c74ba4 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -219,6 +219,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:domain_blacklist_file,
:raw_blob_request_limit,
:namespace_storage_size_limit,
+ :issues_create_limit,
disabled_oauth_sign_in_sources: [],
import_sources: [],
repository_storages: [],
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index e51a5c7b84d..09dc4d118a1 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -14,9 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action :expire_etag_cache, only: [:index], unless: -> { request.format.json? }
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
push_frontend_feature_flag(:prometheus_computed_alerts)
- end
- before_action do
- push_frontend_feature_flag(:auto_stop_environments, default_enabled: true)
+ push_frontend_feature_flag(:metrics_dashboard_annotations)
end
after_action :expire_etag_cache, only: [:cancel_auto_stop]
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index f552c471eb2..96650e2cae9 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -42,6 +42,9 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_import_issues!, only: [:import_csv]
before_action :authorize_download_code!, only: [:related_branches]
+ # Limit the amount of issues created per minute
+ before_action :create_rate_limit, only: [:create]
+
before_action do
push_frontend_feature_flag(:vue_issuable_sidebar, project.group)
push_frontend_feature_flag(:save_issuable_health_status, project.group, default_enabled: true)
@@ -296,6 +299,22 @@ class Projects::IssuesController < Projects::ApplicationController
# 3. https://gitlab.com/gitlab-org/gitlab-foss/issues/42426
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42422')
end
+
+ private
+
+ def create_rate_limit
+ key = :issues_create
+
+ if rate_limiter.throttled?(key, scope: [@project, @current_user])
+ rate_limiter.log_request(request, "#{key}_request_limit".to_sym, current_user)
+
+ render plain: _('This endpoint has been requested too many times. Try again later.'), status: :too_many_requests
+ end
+ end
+
+ def rate_limiter
+ ::Gitlab::ApplicationRateLimiter
+ end
end
Projects::IssuesController.prepend_if_ee('EE::Projects::IssuesController')
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 26de200a1c1..038b6146bab 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -24,6 +24,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:single_mr_diff_view, @project, default_enabled: true)
push_frontend_feature_flag(:suggest_pipeline) if experiment_enabled?(:suggest_pipeline)
push_frontend_feature_flag(:code_navigation, @project)
+ push_frontend_feature_flag(:widget_visibility_polling, @project, default_enabled: true)
end
before_action do
diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb
index 1caca6b3e44..6c3dcf8746b 100644
--- a/app/mailers/emails/pages_domains.rb
+++ b/app/mailers/emails/pages_domains.rb
@@ -41,5 +41,16 @@ module Emails
subject: subject("ACTION REQUIRED: Verification failed for GitLab Pages domain '#{domain.domain}'")
)
end
+
+ def pages_domain_auto_ssl_failed_email(domain, recipient)
+ @domain = domain
+ @project = domain.project
+
+ subject_text = _("ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'") % { domain: domain.domain }
+ mail(
+ to: recipient.notification_email_for(@project.group),
+ subject: subject(subject_text)
+ )
+ end
end
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 920ad3286d1..c96f086684f 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -79,6 +79,7 @@ module ApplicationSettingImplementation
housekeeping_gc_period: 200,
housekeeping_incremental_repack_period: 10,
import_sources: Settings.gitlab['import_sources'],
+ issues_create_limit: 300,
local_markdown_version: 0,
max_artifacts_size: Settings.artifacts['max_size'],
max_attachment_size: Settings.gitlab['max_attachment_size'],
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index ef0701b3874..c4ac10814a9 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -73,12 +73,14 @@ module Ci
validates :file_format, presence: true, unless: :trace?, on: :create
validate :valid_file_format?, unless: :trace?, on: :create
- before_save :set_size, if: :file_changed?
- update_project_statistics project_statistics_name: :build_artifacts_size
+ before_save :set_size, if: :file_changed?
+ before_save :set_file_store, if: ->(job_artifact) { job_artifact.file_store.nil? }
after_save :update_file_store, if: :saved_change_to_file?
+ update_project_statistics project_statistics_name: :build_artifacts_size
+
scope :with_files_stored_locally, -> { where(file_store: [nil, ::JobArtifactUploader::Store::LOCAL]) }
scope :with_files_stored_remotely, -> { where(file_store: ::JobArtifactUploader::Store::REMOTE) }
scope :for_sha, ->(sha, project_id) { joins(job: :pipeline).where(ci_pipelines: { sha: sha, project_id: project_id }) }
@@ -226,6 +228,15 @@ module Ci
self.size = file.size
end
+ def set_file_store
+ self.file_store =
+ if JobArtifactUploader.object_store_enabled? && JobArtifactUploader.direct_upload_enabled?
+ JobArtifactUploader::Store::REMOTE
+ else
+ file.object_store
+ end
+ end
+
def project_destroyed?
# Use job.project to avoid extra DB query for project
job.project.pending_delete?
diff --git a/app/models/diff_note_position.rb b/app/models/diff_note_position.rb
new file mode 100644
index 00000000000..78e4fbc49eb
--- /dev/null
+++ b/app/models/diff_note_position.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+class DiffNotePosition < ApplicationRecord
+ belongs_to :note
+
+ enum diff_content_type: {
+ text: 0,
+ image: 1
+ }
+
+ enum diff_type: {
+ head: 0
+ }
+
+ def position
+ Gitlab::Diff::Position.new(
+ old_path: old_path,
+ new_path: new_path,
+ old_line: old_line,
+ new_line: new_line,
+ position_type: diff_content_type,
+ diff_refs: Gitlab::Diff::DiffRefs.new(
+ base_sha: base_sha,
+ start_sha: start_sha,
+ head_sha: head_sha
+ )
+ )
+ end
+
+ def position=(position)
+ position_attrs = position.to_h
+ position_attrs[:diff_content_type] = position_attrs.delete(:position_type)
+
+ assign_attributes(position_attrs)
+ end
+end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 6a86aebae39..c5233deaa96 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -17,6 +17,8 @@ class LfsObject < ApplicationRecord
mount_uploader :file, LfsObjectUploader
+ before_save :set_file_store, if: ->(lfs_object) { lfs_object.file_store.nil? }
+
after_save :update_file_store, if: :saved_change_to_file?
def self.not_linked_to_project(project)
@@ -55,6 +57,17 @@ class LfsObject < ApplicationRecord
def self.calculate_oid(path)
self.hexdigest(path)
end
+
+ private
+
+ def set_file_store
+ self.file_store =
+ if LfsObjectUploader.object_store_enabled? && LfsObjectUploader.direct_upload_enabled?
+ LfsObjectUploader::Store::REMOTE
+ else
+ file.object_store
+ end
+ end
end
LfsObject.prepend_if_ee('EE::LfsObject')
diff --git a/app/services/clusters/create_service.rb b/app/services/clusters/create_service.rb
index 5c26c611e00..7b5bf6b32c2 100644
--- a/app/services/clusters/create_service.rb
+++ b/app/services/clusters/create_service.rb
@@ -23,6 +23,8 @@ module Clusters
cluster.errors.add(:base, _('Instance does not support multiple Kubernetes clusters'))
end
+ validate_management_project_permissions(cluster)
+
return cluster if cluster.errors.present?
cluster.tap do |cluster|
@@ -57,6 +59,11 @@ module Clusters
def can_create_cluster?
clusterable.clusters.empty?
end
+
+ def validate_management_project_permissions(cluster)
+ Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
+ .execute(cluster, params[:management_project_id])
+ end
end
end
diff --git a/app/services/clusters/management/validate_management_project_permissions_service.rb b/app/services/clusters/management/validate_management_project_permissions_service.rb
new file mode 100644
index 00000000000..e89a0afe6d2
--- /dev/null
+++ b/app/services/clusters/management/validate_management_project_permissions_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Management
+ class ValidateManagementProjectPermissionsService
+ attr_reader :current_user
+
+ def initialize(user = nil)
+ @current_user = user
+ end
+
+ def execute(cluster, management_project_id)
+ if management_project_id.present?
+ management_project = management_project_scope(cluster).find_by_id(management_project_id)
+
+ unless management_project && can_admin_pipeline_for_project?(management_project)
+ cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
+
+ return false
+ end
+ end
+
+ true
+ end
+
+ private
+
+ def can_admin_pipeline_for_project?(project)
+ Ability.allowed?(current_user, :admin_pipeline, project)
+ end
+
+ def management_project_scope(cluster)
+ return ::Project.all if cluster.instance_type?
+
+ group =
+ if cluster.group_type?
+ cluster.first_group
+ elsif cluster.project_type?
+ cluster.first_project&.namespace
+ end
+
+ # Prevent users from selecting nested projects until
+ # https://gitlab.com/gitlab-org/gitlab/issues/34650 is resolved
+ include_subgroups = cluster.group_type?
+
+ ::GroupProjectsFinder.new(
+ group: group,
+ current_user: current_user,
+ options: { only_owned: true, include_subgroups: include_subgroups }
+ ).execute
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/update_service.rb b/app/services/clusters/update_service.rb
index 8cb77040b14..2315df612a1 100644
--- a/app/services/clusters/update_service.rb
+++ b/app/services/clusters/update_service.rb
@@ -18,46 +18,9 @@ module Clusters
private
- def can_admin_pipeline_for_project?(project)
- Ability.allowed?(current_user, :admin_pipeline, project)
- end
-
def validate_params(cluster)
- if params[:management_project_id].present?
- management_project = management_project_scope(cluster).find_by_id(params[:management_project_id])
-
- unless management_project
- cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
-
- return false
- end
-
- unless can_admin_pipeline_for_project?(management_project)
- # Use same message as not found to prevent enumeration
- cluster.errors.add(:management_project_id, _('Project does not exist or you don\'t have permission to perform this action'))
-
- return false
- end
- end
-
- true
- end
-
- def management_project_scope(cluster)
- return ::Project.all if cluster.instance_type?
-
- group =
- if cluster.group_type?
- cluster.first_group
- elsif cluster.project_type?
- cluster.first_project&.namespace
- end
-
- # Prevent users from selecting nested projects until
- # https://gitlab.com/gitlab-org/gitlab/issues/34650 is resolved
- include_subgroups = cluster.group_type?
-
- ::GroupProjectsFinder.new(group: group, current_user: current_user, options: { only_owned: true, include_subgroups: include_subgroups }).execute
+ ::Clusters::Management::ValidateManagementProjectPermissionsService.new(current_user)
+ .execute(cluster, params[:management_project_id])
end
end
end
diff --git a/app/services/environments/auto_stop_service.rb b/app/services/environments/auto_stop_service.rb
index ee7f25a4d76..bde598abf66 100644
--- a/app/services/environments/auto_stop_service.rb
+++ b/app/services/environments/auto_stop_service.rb
@@ -30,7 +30,7 @@ module Environments
def stop_in_batch
environments = Environment.auto_stoppable(BATCH_SIZE)
- return false unless environments.exists? && Feature.enabled?(:auto_stop_environments, default_enabled: true)
+ return false unless environments.exists?
Ci::StopEnvironmentsService.execute_in_batch(environments)
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 62827f20929..91e19d190bd 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -489,6 +489,12 @@ class NotificationService
end
end
+ def pages_domain_auto_ssl_failed(domain)
+ project_maintainers_recipients(domain, action: 'disabled').each do |recipient|
+ mailer.pages_domain_auto_ssl_failed_email(domain, recipient.user).deliver_later
+ end
+ end
+
def issue_due(issue)
recipients = NotificationRecipients::BuildService.build_recipients(
issue,
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index 93445dd4ddd..1c03641469e 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -57,6 +57,8 @@ module PagesDomains
pages_domain.save!(validate: false)
acme_order.destroy!
+
+ NotificationService.new.pages_domain_auto_ssl_failed(pages_domain)
end
def log_error(api_order)
diff --git a/app/uploaders/records_uploads.rb b/app/uploaders/records_uploads.rb
index 967fcdc704e..427314a87bb 100644
--- a/app/uploaders/records_uploads.rb
+++ b/app/uploaders/records_uploads.rb
@@ -56,10 +56,31 @@ module RecordsUploads
size: file.size,
path: upload_path,
model: model,
- mount_point: mounted_as
+ mount_point: mounted_as,
+ store: initial_store
)
end
+ def initial_store
+ if immediately_remote_stored?
+ ::ObjectStorage::Store::REMOTE
+ else
+ ::ObjectStorage::Store::LOCAL
+ end
+ end
+
+ def immediately_remote_stored?
+ object_storage_available? && direct_upload_enabled?
+ end
+
+ def object_storage_available?
+ self.class.ancestors.include?(ObjectStorage::Concern)
+ end
+
+ def direct_upload_enabled?
+ self.class.object_store_enabled? && self.class.direct_upload_enabled?
+ end
+
# Before removing an attachment, destroy any Upload records at the same path
#
# Called `before :remove`
diff --git a/app/views/admin/application_settings/_issue_limits.html.haml b/app/views/admin/application_settings/_issue_limits.html.haml
new file mode 100644
index 00000000000..5906358fbb1
--- /dev/null
+++ b/app/views/admin/application_settings/_issue_limits.html.haml
@@ -0,0 +1,9 @@
+= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-issue-limits-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :issues_create_limit, 'Max requests per second per user', class: 'label-bold'
+ = f.number_field :issues_create_limit, class: 'form-control'
+
+ = f.submit 'Save changes', class: "btn btn-success", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 8d88dedf832..db4611964b4 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -46,4 +46,15 @@
.settings-content
= render 'protected_paths'
+%section.settings.as-issue-limits.no-animate#js-issue-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Issues Rate Limits')
+ %button.btn.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure limit for issues created per minute by web and API requests.')
+ .settings-content
+ = render 'issue_limits'
+
= render_if_exists 'admin/application_settings/ee_network_settings'
diff --git a/app/views/admin/deploy_keys/index.html.haml b/app/views/admin/deploy_keys/index.html.haml
index 9fffa97f969..4e9cfc13af0 100644
--- a/app/views/admin/deploy_keys/index.html.haml
+++ b/app/views/admin/deploy_keys/index.html.haml
@@ -1,7 +1,7 @@
- page_title _('Deploy Keys')
%h3.page-title.deploy-keys-title
- = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.count }
+ = _('Public deploy keys (%{deploy_keys_count})') % { deploy_keys_count: @deploy_keys.load.size }
.float-right
= link_to _('New deploy key'), new_admin_deploy_key_path, class: 'btn btn-success btn-sm btn-inverted'
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
new file mode 100644
index 00000000000..1bc2cc15616
--- /dev/null
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.html.haml
@@ -0,0 +1,11 @@
+%p
+ = _("Something went wrong while obtaining the Let's Encrypt certificate.")
+%p
+ #{_('Project')}: #{link_to @project.human_name, project_url(@project)}
+%p
+ #{_('Domain')}: #{link_to @domain.domain, project_pages_domain_url(@project, @domain)}
+%p
+ - docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+ - link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: docs_url }
+ - link_end = '</a>'.html_safe
+ = _("Please follow the %{link_start}Let\'s Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate.").html_safe % { link_start: link_start, link_end: link_end }
diff --git a/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
new file mode 100644
index 00000000000..6f20d11c966
--- /dev/null
+++ b/app/views/notify/pages_domain_auto_ssl_failed_email.text.haml
@@ -0,0 +1,7 @@
+= _("Something went wrong while obtaining the Let's Encrypt certificate.").html_safe
+
+#{_('Project')}: #{project_url(@project)}
+#{_('Domain')}: #{project_pages_domain_url(@project, @domain)}
+
+- docs_url = help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
+= _("Please follow the Let\'s Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}.").html_safe % { docs_url: docs_url }
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 6ea4eeb66c5..e28c74dd650 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -18,7 +18,7 @@
= f.submit _('Add email address'), class: 'btn btn-success', data: { qa_selector: 'add_email_address_button' }
%hr
%h4.prepend-top-0
- = _('Linked emails (%{email_count})') % { email_count: @emails.count + 1 }
+ = _('Linked emails (%{email_count})') % { email_count: @emails.load.size + 1 }
.account-well.append-bottom-default
%ul
%li
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 6da4956a036..69b030ed76a 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -1,6 +1,6 @@
- if @related_branches.any?
%h2.related-branches-title
- = pluralize(@related_branches.count, 'Related Branch')
+ = pluralize(@related_branches.size, 'Related Branch')
%ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
%li
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 0d40f375926..a9e2cbac890 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -1,9 +1,9 @@
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
-- if can?(current_user, :update_pages, @project) && @domains.any?
+- if can?(current_user, :update_pages, @project) && @domains.load.any?
.card
.card-header
- Domains (#{@domains.count})
+ Domains (#{@domains.size})
%ul.list-group.list-group-flush.pages-domain-list{ class: ("has-verification-status" if verification_enabled) }
- @domains.each do |domain|
- domain = Gitlab::View::Presenter::Factory.new(domain, current_user: current_user).fabricate!
diff --git a/app/workers/environments/auto_stop_cron_worker.rb b/app/workers/environments/auto_stop_cron_worker.rb
index de5e10a0976..ada52d3402d 100644
--- a/app/workers/environments/auto_stop_cron_worker.rb
+++ b/app/workers/environments/auto_stop_cron_worker.rb
@@ -8,8 +8,6 @@ module Environments
feature_category :continuous_delivery
def perform
- return unless Feature.enabled?(:auto_stop_environments, default_enabled: true)
-
AutoStopService.new.execute
end
end
diff --git a/changelogs/unreleased/211998-add-cluster-mangement-id-on-create.yml b/changelogs/unreleased/211998-add-cluster-mangement-id-on-create.yml
new file mode 100644
index 00000000000..cca81a37179
--- /dev/null
+++ b/changelogs/unreleased/211998-add-cluster-mangement-id-on-create.yml
@@ -0,0 +1,6 @@
+---
+title: Add management_project_id to group and project cluster creation, clarifies
+ docs.
+merge_request: 28289
+author:
+type: fixed
diff --git a/changelogs/unreleased/212561-fix-empty-edit-area.yml b/changelogs/unreleased/212561-fix-empty-edit-area.yml
new file mode 100644
index 00000000000..0e45110265e
--- /dev/null
+++ b/changelogs/unreleased/212561-fix-empty-edit-area.yml
@@ -0,0 +1,5 @@
+---
+title: 'fix: Publish toolbar dissappears when submitting empty content'
+merge_request: 29410
+author:
+type: fixed
diff --git a/changelogs/unreleased/213299-env-autostop-bug.yml b/changelogs/unreleased/213299-env-autostop-bug.yml
new file mode 100644
index 00000000000..f4d3198f8bd
--- /dev/null
+++ b/changelogs/unreleased/213299-env-autostop-bug.yml
@@ -0,0 +1,5 @@
+---
+title: Add autostop check to folder table
+merge_request: 28937
+author:
+type: fixed
diff --git a/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml b/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml
new file mode 100644
index 00000000000..066c8e2c45c
--- /dev/null
+++ b/changelogs/unreleased/213382-use-not-valid-to-immediately-enforce-a-not-null-constraint.yml
@@ -0,0 +1,6 @@
+---
+title: Use NOT VALID to enforce a NOT NULL constraint on file_store to ci_job_artifacts,
+ lfs_objects and uploads tables
+merge_request: 28946
+author:
+type: fixed
diff --git a/changelogs/unreleased/213799-optimize-usage_activity_by_stage-projects_with_repositories_enable.yml b/changelogs/unreleased/213799-optimize-usage_activity_by_stage-projects_with_repositories_enable.yml
new file mode 100644
index 00000000000..af6a79a59b5
--- /dev/null
+++ b/changelogs/unreleased/213799-optimize-usage_activity_by_stage-projects_with_repositories_enable.yml
@@ -0,0 +1,5 @@
+---
+title: Optimize projects with repositories enabled usage data
+merge_request: 29117
+author:
+type: performance
diff --git a/changelogs/unreleased/55241-rate-limit-issue-creation.yml b/changelogs/unreleased/55241-rate-limit-issue-creation.yml
new file mode 100644
index 00000000000..76b3269ecb0
--- /dev/null
+++ b/changelogs/unreleased/55241-rate-limit-issue-creation.yml
@@ -0,0 +1,5 @@
+---
+title: Introduce rate limit for creating issues via web UI
+merge_request: 28129
+author:
+type: performance
diff --git a/changelogs/unreleased/bvl-remove-sidekiq-deduplication-feature-flag.yml b/changelogs/unreleased/bvl-remove-sidekiq-deduplication-feature-flag.yml
new file mode 100644
index 00000000000..cc8da28f6ca
--- /dev/null
+++ b/changelogs/unreleased/bvl-remove-sidekiq-deduplication-feature-flag.yml
@@ -0,0 +1,5 @@
+---
+title: Avoid scheduling duplicate sidekiq jobs
+merge_request: 29116
+author:
+type: performance
diff --git a/changelogs/unreleased/ph-210377-increaseMrPollTimes.yml b/changelogs/unreleased/ph-210377-increaseMrPollTimes.yml
new file mode 100644
index 00000000000..fb953f03e60
--- /dev/null
+++ b/changelogs/unreleased/ph-210377-increaseMrPollTimes.yml
@@ -0,0 +1,5 @@
+---
+title: Increase the timing of polling for the merge request widget
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/vs-migrate-deprecated-size-in-loading-icon.yml b/changelogs/unreleased/vs-migrate-deprecated-size-in-loading-icon.yml
new file mode 100644
index 00000000000..f78fa371c22
--- /dev/null
+++ b/changelogs/unreleased/vs-migrate-deprecated-size-in-loading-icon.yml
@@ -0,0 +1,5 @@
+---
+title: Replace deprecated GlLoadingIcon sizes
+merge_request: 29417
+author:
+type: fixed
diff --git a/db/migrate/20200325111432_add_issues_create_limit_to_application_settings.rb b/db/migrate/20200325111432_add_issues_create_limit_to_application_settings.rb
new file mode 100644
index 00000000000..60da96ccf33
--- /dev/null
+++ b/db/migrate/20200325111432_add_issues_create_limit_to_application_settings.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class AddIssuesCreateLimitToApplicationSettings < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :issues_create_limit, :integer, default: 300, null: false
+ end
+end
diff --git a/db/migrate/20200326122700_create_diff_note_positions.rb b/db/migrate/20200326122700_create_diff_note_positions.rb
new file mode 100644
index 00000000000..87159e666b5
--- /dev/null
+++ b/db/migrate/20200326122700_create_diff_note_positions.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+class CreateDiffNotePositions < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ create_table :diff_note_positions do |t|
+ t.references :note, foreign_key: { on_delete: :cascade }, null: false, index: false
+ t.integer :old_line
+ t.integer :new_line
+ t.integer :diff_content_type, limit: 2, null: false
+ t.integer :diff_type, limit: 2, null: false
+ t.string :line_code, limit: 255, null: false
+ t.binary :base_sha, null: false
+ t.binary :start_sha, null: false
+ t.binary :head_sha, null: false
+ t.text :old_path, null: false
+ t.text :new_path, null: false
+
+ t.index [:note_id, :diff_type], unique: true
+ end
+ end
+ end
+
+ def down
+ drop_table :diff_note_positions
+ end
+end
diff --git a/db/migrate/20200406165950_add_not_null_constraint_on_file_store_to_lfs_objects.rb b/db/migrate/20200406165950_add_not_null_constraint_on_file_store_to_lfs_objects.rb
new file mode 100644
index 00000000000..78b5832fea4
--- /dev/null
+++ b/db/migrate/20200406165950_add_not_null_constraint_on_file_store_to_lfs_objects.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnFileStoreToLfsObjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ CONSTRAINT_NAME = 'lfs_objects_file_store_not_null'
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE lfs_objects ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (file_store IS NOT NULL) NOT VALID;
+ SQL
+ end
+ end
+
+ def down
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE lfs_objects DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
+ SQL
+ end
+ end
+end
diff --git a/db/migrate/20200406171857_add_not_null_constraint_on_file_store_to_ci_job_artifacts.rb b/db/migrate/20200406171857_add_not_null_constraint_on_file_store_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..1d44e5c17b3
--- /dev/null
+++ b/db/migrate/20200406171857_add_not_null_constraint_on_file_store_to_ci_job_artifacts.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnFileStoreToCiJobArtifacts < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ CONSTRAINT_NAME = 'ci_job_artifacts_file_store_not_null'
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE ci_job_artifacts ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (file_store IS NOT NULL) NOT VALID;
+ SQL
+ end
+ end
+
+ def down
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE ci_job_artifacts DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
+ SQL
+ end
+ end
+end
diff --git a/db/migrate/20200406172135_add_not_null_constraint_on_file_store_to_uploads.rb b/db/migrate/20200406172135_add_not_null_constraint_on_file_store_to_uploads.rb
new file mode 100644
index 00000000000..aa498ba9c89
--- /dev/null
+++ b/db/migrate/20200406172135_add_not_null_constraint_on_file_store_to_uploads.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+class AddNotNullConstraintOnFileStoreToUploads < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ CONSTRAINT_NAME = 'uploads_store_not_null'
+ DOWNTIME = false
+
+ def up
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE uploads ADD CONSTRAINT #{CONSTRAINT_NAME} CHECK (store IS NOT NULL) NOT VALID;
+ SQL
+ end
+ end
+
+ def down
+ with_lock_retries do
+ execute <<~SQL
+ ALTER TABLE uploads DROP CONSTRAINT IF EXISTS #{CONSTRAINT_NAME};
+ SQL
+ end
+ end
+end
diff --git a/db/migrate/20200408153842_add_index_on_creator_id_and_id_on_projects.rb b/db/migrate/20200408153842_add_index_on_creator_id_and_id_on_projects.rb
new file mode 100644
index 00000000000..2cc91efcc36
--- /dev/null
+++ b/db/migrate/20200408153842_add_index_on_creator_id_and_id_on_projects.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddIndexOnCreatorIdAndIdOnProjects < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :projects, [:creator_id, :id]
+ end
+
+ def down
+ remove_concurrent_index :projects, [:creator_id, :id]
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 895afed92e6..cfa0f3c405a 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -397,6 +397,7 @@ CREATE TABLE public.application_settings (
email_restrictions text,
npm_package_requests_forwarding boolean DEFAULT true NOT NULL,
namespace_storage_size_limit bigint DEFAULT 0 NOT NULL,
+ issues_create_limit integer DEFAULT 300 NOT NULL,
seat_link_enabled boolean DEFAULT true NOT NULL,
container_expiration_policies_enable_historic_entries boolean DEFAULT false NOT NULL
);
@@ -2138,6 +2139,30 @@ CREATE SEQUENCE public.design_user_mentions_id_seq
ALTER SEQUENCE public.design_user_mentions_id_seq OWNED BY public.design_user_mentions.id;
+CREATE TABLE public.diff_note_positions (
+ id bigint NOT NULL,
+ note_id bigint NOT NULL,
+ old_line integer,
+ new_line integer,
+ diff_content_type smallint NOT NULL,
+ diff_type smallint NOT NULL,
+ line_code character varying(255) NOT NULL,
+ base_sha bytea NOT NULL,
+ start_sha bytea NOT NULL,
+ head_sha bytea NOT NULL,
+ old_path text NOT NULL,
+ new_path text NOT NULL
+);
+
+CREATE SEQUENCE public.diff_note_positions_id_seq
+ START WITH 1
+ INCREMENT BY 1
+ NO MINVALUE
+ NO MAXVALUE
+ CACHE 1;
+
+ALTER SEQUENCE public.diff_note_positions_id_seq OWNED BY public.diff_note_positions.id;
+
CREATE TABLE public.draft_notes (
id bigint NOT NULL,
merge_request_id integer NOT NULL,
@@ -7124,6 +7149,8 @@ ALTER TABLE ONLY public.design_management_versions ALTER COLUMN id SET DEFAULT n
ALTER TABLE ONLY public.design_user_mentions ALTER COLUMN id SET DEFAULT nextval('public.design_user_mentions_id_seq'::regclass);
+ALTER TABLE ONLY public.diff_note_positions ALTER COLUMN id SET DEFAULT nextval('public.diff_note_positions_id_seq'::regclass);
+
ALTER TABLE ONLY public.draft_notes ALTER COLUMN id SET DEFAULT nextval('public.draft_notes_id_seq'::regclass);
ALTER TABLE ONLY public.emails ALTER COLUMN id SET DEFAULT nextval('public.emails_id_seq'::regclass);
@@ -7670,6 +7697,9 @@ ALTER TABLE ONLY public.ci_daily_report_results
ALTER TABLE ONLY public.ci_group_variables
ADD CONSTRAINT ci_group_variables_pkey PRIMARY KEY (id);
+ALTER TABLE public.ci_job_artifacts
+ ADD CONSTRAINT ci_job_artifacts_file_store_not_null CHECK ((file_store IS NOT NULL)) NOT VALID;
+
ALTER TABLE ONLY public.ci_job_artifacts
ADD CONSTRAINT ci_job_artifacts_pkey PRIMARY KEY (id);
@@ -7829,6 +7859,9 @@ ALTER TABLE ONLY public.design_management_versions
ALTER TABLE ONLY public.design_user_mentions
ADD CONSTRAINT design_user_mentions_pkey PRIMARY KEY (id);
+ALTER TABLE ONLY public.diff_note_positions
+ ADD CONSTRAINT diff_note_positions_pkey PRIMARY KEY (id);
+
ALTER TABLE ONLY public.draft_notes
ADD CONSTRAINT draft_notes_pkey PRIMARY KEY (id);
@@ -8024,6 +8057,9 @@ ALTER TABLE ONLY public.ldap_group_links
ALTER TABLE ONLY public.lfs_file_locks
ADD CONSTRAINT lfs_file_locks_pkey PRIMARY KEY (id);
+ALTER TABLE public.lfs_objects
+ ADD CONSTRAINT lfs_objects_file_store_not_null CHECK ((file_store IS NOT NULL)) NOT VALID;
+
ALTER TABLE ONLY public.lfs_objects
ADD CONSTRAINT lfs_objects_pkey PRIMARY KEY (id);
@@ -8417,6 +8453,9 @@ ALTER TABLE ONLY public.u2f_registrations
ALTER TABLE ONLY public.uploads
ADD CONSTRAINT uploads_pkey PRIMARY KEY (id);
+ALTER TABLE public.uploads
+ ADD CONSTRAINT uploads_store_not_null CHECK ((store IS NOT NULL)) NOT VALID;
+
ALTER TABLE ONLY public.user_agent_details
ADD CONSTRAINT user_agent_details_pkey PRIMARY KEY (id);
@@ -9086,6 +9125,8 @@ CREATE UNIQUE INDEX index_design_management_versions_on_sha_and_issue_id ON publ
CREATE UNIQUE INDEX index_design_user_mentions_on_note_id ON public.design_user_mentions USING btree (note_id);
+CREATE UNIQUE INDEX index_diff_note_positions_on_note_id_and_diff_type ON public.diff_note_positions USING btree (note_id, diff_type);
+
CREATE INDEX index_draft_notes_on_author_id ON public.draft_notes USING btree (author_id);
CREATE INDEX index_draft_notes_on_discussion_id ON public.draft_notes USING btree (discussion_id);
@@ -9886,6 +9927,8 @@ CREATE INDEX index_projects_on_creator_id_and_created_at ON public.projects USIN
CREATE INDEX index_projects_on_creator_id_and_created_at_and_id ON public.projects USING btree (creator_id, created_at, id);
+CREATE INDEX index_projects_on_creator_id_and_id ON public.projects USING btree (creator_id, id);
+
CREATE INDEX index_projects_on_description_trigram ON public.projects USING gin (description public.gin_trgm_ops);
CREATE INDEX index_projects_on_id_and_archived_and_pending_delete ON public.projects USING btree (id) WHERE ((archived = false) AND (pending_delete = false));
@@ -11068,6 +11111,9 @@ ALTER TABLE ONLY public.project_statistics
ALTER TABLE ONLY public.user_details
ADD CONSTRAINT fk_rails_12e0b3043d FOREIGN KEY (user_id) REFERENCES public.users(id) ON DELETE CASCADE;
+ALTER TABLE ONLY public.diff_note_positions
+ ADD CONSTRAINT fk_rails_13c7212859 FOREIGN KEY (note_id) REFERENCES public.notes(id) ON DELETE CASCADE;
+
ALTER TABLE ONLY public.users_security_dashboard_projects
ADD CONSTRAINT fk_rails_150cd5682c FOREIGN KEY (project_id) REFERENCES public.projects(id) ON DELETE CASCADE;
@@ -13064,10 +13110,12 @@ COPY "schema_migrations" (version) FROM STDIN;
20200323134519
20200324093258
20200324115359
+20200325111432
20200325152327
20200325160952
20200325183636
20200326114443
+20200326122700
20200326124443
20200326134443
20200326135443
@@ -13090,10 +13138,14 @@ COPY "schema_migrations" (version) FROM STDIN;
20200403185127
20200403185422
20200406135648
+20200406165950
+20200406171857
+20200406172135
20200406192059
20200407094005
20200407094923
20200408110856
+20200408153842
20200408175424
\.
diff --git a/doc/api/group_clusters.md b/doc/api/group_clusters.md
index e9b4b2b92ab..01c6d59f60d 100644
--- a/doc/api/group_clusters.md
+++ b/doc/api/group_clusters.md
@@ -224,6 +224,7 @@ Parameters:
| `cluster_id` | integer | yes | The ID of the cluster |
| `name` | string | no | The name of the cluster |
| `domain` | string | no | The [base domain](../user/group/clusters/index.md#base-domain) of the cluster |
+| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `platform_kubernetes_attributes[api_url]` | string | no | The URL to access the Kubernetes API |
| `platform_kubernetes_attributes[token]` | string | no | The token to authenticate against Kubernetes |
| `platform_kubernetes_attributes[ca_cert]` | string | no | TLS certificate. Required if API is using a self-signed TLS certificate. |
diff --git a/doc/api/project_clusters.md b/doc/api/project_clusters.md
index 2ed57eceb85..79800af2f59 100644
--- a/doc/api/project_clusters.md
+++ b/doc/api/project_clusters.md
@@ -179,6 +179,7 @@ Parameters:
| `id` | integer | yes | The ID of the project owned by the authenticated user |
| `name` | string | yes | The name of the cluster |
| `domain` | string | no | The [base domain](../user/project/clusters/index.md#base-domain) of the cluster |
+| `management_project_id` | integer | no | The ID of the [management project](../user/clusters/management_project.md) for the cluster |
| `enabled` | boolean | no | Determines if cluster is active or not, defaults to true |
| `managed` | boolean | no | Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true |
| `platform_kubernetes_attributes[api_url]` | string | yes | The URL to access the Kubernetes API |
diff --git a/doc/user/project/repository/forking_workflow.md b/doc/user/project/repository/forking_workflow.md
index 927c1db804a..c26f2bd6b1d 100644
--- a/doc/user/project/repository/forking_workflow.md
+++ b/doc/user/project/repository/forking_workflow.md
@@ -54,6 +54,9 @@ When you are ready to send your code back to the upstream project,
[create a merge request](../merge_requests/creating_merge_requests.md). For **Source branch**,
choose your forked project's branch. For **Target branch**, choose the original project's branch.
+NOTE: **Note:**
+When creating a merge request, if the forked project's visibility is more restrictive than the parent project (for example the fork is private, parent is public), the target branch will default to the forked project's default branch. This prevents potentially exposing private code of the forked project.
+
![Selecting branches](img/forking_workflow_branch_select.png)
Then you can add labels, a milestone, and assign the merge request to someone who can review
diff --git a/lib/api/group_clusters.rb b/lib/api/group_clusters.rb
index 0108f6feae3..2c12c6387fb 100644
--- a/lib/api/group_clusters.rb
+++ b/lib/api/group_clusters.rb
@@ -53,6 +53,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
diff --git a/lib/api/project_clusters.rb b/lib/api/project_clusters.rb
index b482980b88a..299301aabc4 100644
--- a/lib/api/project_clusters.rb
+++ b/lib/api/project_clusters.rb
@@ -56,6 +56,7 @@ module API
requires :name, type: String, desc: 'Cluster name'
optional :enabled, type: Boolean, default: true, desc: 'Determines if cluster is active or not, defaults to true'
optional :domain, type: String, desc: 'Cluster base domain'
+ optional :management_project_id, type: Integer, desc: 'The ID of the management project'
optional :managed, type: Boolean, default: true, desc: 'Determines if GitLab will manage namespaces and service accounts for this cluster, defaults to true'
requires :platform_kubernetes_attributes, type: Hash, desc: %q(Platform Kubernetes data) do
requires :api_url, type: String, allow_blank: false, desc: 'URL to access the Kubernetes API'
diff --git a/lib/gitlab/application_rate_limiter.rb b/lib/gitlab/application_rate_limiter.rb
index c1066d8fa62..2defbd26b98 100644
--- a/lib/gitlab/application_rate_limiter.rb
+++ b/lib/gitlab/application_rate_limiter.rb
@@ -19,8 +19,9 @@ module Gitlab
# and only do that when it's needed.
def rate_limits
{
- project_export: { threshold: 1, interval: 5.minutes },
- project_download_export: { threshold: 10, interval: 10.minutes },
+ issues_create: { threshold: -> { Gitlab::CurrentSettings.current_application_settings.issues_create_limit }, interval: 1.minute },
+ project_export: { threshold: 1, interval: 5.minutes },
+ project_download_export: { threshold: 10, interval: 10.minutes },
project_repositories_archive: { threshold: 5, interval: 1.minute },
project_generate_new_export: { threshold: 1, interval: 5.minutes },
project_import: { threshold: 30, interval: 5.minutes },
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs.rb
deleted file mode 100644
index 7a77a56d642..00000000000
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-# frozen_string_literal: true
-
-require 'digest'
-
-module Gitlab
- module SidekiqMiddleware
- module DuplicateJobs
- DROPPABLE_QUEUES = Set.new([
- Namespaces::RootStatisticsWorker.queue,
- Namespaces::ScheduleAggregationWorker.queue
- ]).freeze
-
- def self.drop_duplicates?(queue_name)
- Feature.enabled?(:drop_duplicate_sidekiq_jobs) ||
- drop_duplicates_for_queue?(queue_name)
- end
-
- private_class_method def self.drop_duplicates_for_queue?(queue_name)
- DROPPABLE_QUEUES.include?(queue_name) &&
- Feature.enabled?(:drop_duplicate_sidekiq_jobs_for_queue)
- end
- end
- end
-end
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index a9007039334..79bbb99752e 100644
--- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
+++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
@@ -67,7 +67,7 @@ module Gitlab
end
def droppable?
- idempotent? && duplicate? && DuplicateJobs.drop_duplicates?(queue_name)
+ idempotent? && duplicate?
end
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d45e3763a32..220c41fa318 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -894,6 +894,9 @@ msgstr ""
msgid "A user with write access to the source branch selected this option"
msgstr ""
+msgid "ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '%{domain}'"
+msgstr ""
+
msgid "API Help"
msgstr ""
@@ -5334,6 +5337,9 @@ msgstr ""
msgid "Configure existing installation"
msgstr ""
+msgid "Configure limit for issues created per minute by web and API requests."
+msgstr ""
+
msgid "Configure limits for web and API requests."
msgstr ""
@@ -11385,6 +11391,9 @@ msgstr ""
msgid "Issues Analytics"
msgstr ""
+msgid "Issues Rate Limits"
+msgstr ""
+
msgid "Issues can be bugs, tasks or ideas to be discussed. Also, issues are searchable and filterable."
msgstr ""
@@ -12941,9 +12950,15 @@ msgstr ""
msgid "Metrics|There was an error creating the dashboard. %{error}"
msgstr ""
+msgid "Metrics|There was an error fetching annotations. Please try again."
+msgstr ""
+
msgid "Metrics|There was an error fetching the environments data, please try again"
msgstr ""
+msgid "Metrics|There was an error getting annotations information."
+msgstr ""
+
msgid "Metrics|There was an error getting deployment information."
msgstr ""
@@ -14900,6 +14915,12 @@ msgstr ""
msgid "Please fill in a descriptive name for your group."
msgstr ""
+msgid "Please follow the %{link_start}Let's Encrypt troubleshooting instructions%{link_end} to re-obtain your Let's Encrypt certificate."
+msgstr ""
+
+msgid "Please follow the Let's Encrypt troubleshooting instructions to re-obtain your Let's Encrypt certificate: %{docs_url}."
+msgstr ""
+
msgid "Please migrate all existing projects to hashed storage to avoid security issues and ensure data integrity. %{migrate_link}"
msgstr ""
@@ -18939,6 +18960,9 @@ msgstr ""
msgid "Something went wrong while moving issues."
msgstr ""
+msgid "Something went wrong while obtaining the Let's Encrypt certificate."
+msgstr ""
+
msgid "Something went wrong while performing the action."
msgstr ""
diff --git a/package.json b/package.json
index a0abec5643f..a48b2468ea8 100644
--- a/package.json
+++ b/package.json
@@ -43,7 +43,7 @@
"@gitlab/ui": "11.2.1",
"@gitlab/visual-review-tools": "1.5.1",
"@sentry/browser": "^5.10.2",
- "@sourcegraph/code-host-integration": "0.0.34",
+ "@sourcegraph/code-host-integration": "0.0.36",
"apollo-cache-inmemory": "^1.6.3",
"apollo-client": "^2.6.4",
"apollo-link": "^1.2.11",
diff --git a/rubocop/cop/performance/ar_count_each.rb b/rubocop/cop/performance/ar_count_each.rb
new file mode 100644
index 00000000000..2fe8e549872
--- /dev/null
+++ b/rubocop/cop/performance/ar_count_each.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ module Performance
+ class ARCountEach < RuboCop::Cop::Cop
+ def message(ivar)
+ "If #{ivar} is AR relation, avoid `#{ivar}.count ...; #{ivar}.each... `, this will trigger two queries. " \
+ "Use `#{ivar}.load.size ...; #{ivar}.each... ` instead. If #{ivar} is an array, try to use #{ivar}.size."
+ end
+
+ def_node_matcher :count_match, <<~PATTERN
+ (send (ivar $_) :count)
+ PATTERN
+
+ def_node_matcher :each_match, <<~PATTERN
+ (send (ivar $_) :each)
+ PATTERN
+
+ def file_name(node)
+ node.location.expression.source_buffer.name
+ end
+
+ def in_haml_file?(node)
+ file_name(node).end_with?('.haml.rb')
+ end
+
+ def on_send(node)
+ return unless in_haml_file?(node)
+
+ ivar_count = count_match(node)
+ return unless ivar_count
+
+ node.each_ancestor(:begin) do |begin_node|
+ begin_node.each_descendant do |n|
+ ivar_each = each_match(n)
+
+ add_offense(node, location: :expression, message: message(ivar_count)) if ivar_each == ivar_count
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 74ed4a0f991..fdc8fe5f082 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -1085,6 +1085,48 @@ describe Projects::IssuesController do
expect { subject }.to change(SentryIssue, :count)
end
end
+
+ context 'when the endpoint receives requests above the limit' do
+ before do
+ stub_application_setting(issues_create_limit: 5)
+ end
+
+ it 'prevents from creating more issues', :request_store do
+ 5.times { post_new_issue }
+
+ expect { post_new_issue }
+ .to change { Gitlab::GitalyClient.get_request_count }.by(1) # creates 1 projects and 0 issues
+
+ post_new_issue
+ expect(response.body).to eq(_('This endpoint has been requested too many times. Try again later.'))
+ expect(response).to have_gitlab_http_status(:too_many_requests)
+ end
+
+ it 'logs the event on auth.log' do
+ attributes = {
+ message: 'Application_Rate_Limiter_Request',
+ env: :issues_create_request_limit,
+ remote_ip: '0.0.0.0',
+ request_method: 'POST',
+ path: "/#{project.full_path}/-/issues",
+ user_id: user.id,
+ username: user.username
+ }
+
+ expect(Gitlab::AuthLogger).to receive(:error).with(attributes).once
+
+ project.add_developer(user)
+ sign_in(user)
+
+ 6.times do
+ post :create, params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ issue: { title: 'Title', description: 'Description' }
+ }
+ end
+ end
+ end
end
describe 'POST #mark_as_spam' do
diff --git a/spec/factories/ci/job_artifacts.rb b/spec/factories/ci/job_artifacts.rb
index 82383cfa2b0..a259c5142fc 100644
--- a/spec/factories/ci/job_artifacts.rb
+++ b/spec/factories/ci/job_artifacts.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
end
trait :remote_store do
- file_store { JobArtifactUploader::Store::REMOTE}
+ file_store { JobArtifactUploader::Store::REMOTE }
end
after :build do |artifact|
diff --git a/spec/factories/diff_note_positions.rb b/spec/factories/diff_note_positions.rb
new file mode 100644
index 00000000000..6e95e306d50
--- /dev/null
+++ b/spec/factories/diff_note_positions.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+FactoryBot.define do
+ factory :diff_note_position do
+ association :note, factory: :diff_note_on_merge_request
+ line_code { note.line_code }
+ position { note.position }
+ diff_type { :head }
+ end
+end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index d7f12411a93..cee9b6d50ba 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -399,10 +399,12 @@ describe 'Environments page', :js do
describe 'environments folders' do
before do
- create(:environment, project: project,
+ create(:environment, :will_auto_stop,
+ project: project,
name: 'staging/review-1',
state: :available)
- create(:environment, project: project,
+ create(:environment, :will_auto_stop,
+ project: project,
name: 'staging/review-2',
state: :available)
end
@@ -420,6 +422,14 @@ describe 'Environments page', :js do
expect(page).to have_content 'review-1'
expect(page).to have_content 'review-2'
+ within('.ci-table') do
+ within('.gl-responsive-table-row:nth-child(3)') do
+ expect(find('.js-auto-stop').text).not_to be_empty
+ end
+ within('.gl-responsive-table-row:nth-child(4)') do
+ expect(find('.js-auto-stop').text).not_to be_empty
+ end
+ end
end
end
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 3aad4c87237..870e47edde0 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -50,6 +50,7 @@ describe('Time series component', () => {
propsData: {
graphData: { ...graphData, type },
deploymentData: store.state.monitoringDashboard.deploymentData,
+ annotations: store.state.monitoringDashboard.annotations,
projectPath: `${mockHost}${mockProjectDir}`,
},
store,
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index d6faec29b65..c34a5afceb0 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -16,6 +16,7 @@ import {
fetchDeploymentsData,
fetchEnvironmentsData,
fetchDashboardData,
+ fetchAnnotations,
fetchPrometheusMetric,
setInitialState,
filterEnvironments,
@@ -24,10 +25,12 @@ import {
} from '~/monitoring/stores/actions';
import { gqClient, parseEnvironmentsResponse } from '~/monitoring/stores/utils';
import getEnvironments from '~/monitoring/queries/getEnvironments.query.graphql';
+import getAnnotations from '~/monitoring/queries/getAnnotations.query.graphql';
import storeState from '~/monitoring/stores/state';
import {
deploymentData,
environmentData,
+ annotationsData,
metricsDashboardResponse,
metricsDashboardViewModel,
dashboardGitResponse,
@@ -120,17 +123,15 @@ describe('Monitoring store actions', () => {
});
it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(
- Promise.resolve({
- data: {
- project: {
- data: {
- environments: [],
- },
+ jest.spyOn(gqClient, 'mutate').mockReturnValue({
+ data: {
+ project: {
+ data: {
+ environments: [],
},
},
- }),
- );
+ },
+ });
return testAction(
filterEnvironments,
@@ -180,17 +181,15 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataSuccess on success', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(
- Promise.resolve({
- data: {
- project: {
- data: {
- environments: environmentData,
- },
+ jest.spyOn(gqClient, 'mutate').mockResolvedValue({
+ data: {
+ project: {
+ data: {
+ environments: environmentData,
},
},
- }),
- );
+ },
+ });
return testAction(
fetchEnvironmentsData,
@@ -208,7 +207,7 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveEnvironmentsDataFailure on error', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue(Promise.reject());
+ jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
return testAction(
fetchEnvironmentsData,
@@ -220,6 +219,80 @@ describe('Monitoring store actions', () => {
});
});
+ describe('fetchAnnotations', () => {
+ const { state } = store;
+ state.projectPath = 'gitlab-org/gitlab-test';
+ state.currentEnvironmentName = 'production';
+ state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
+
+ afterEach(() => {
+ resetStore(store);
+ });
+
+ it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardId: state.currentDashboard,
+ },
+ };
+
+ mockMutate.mockResolvedValue({
+ data: {
+ project: {
+ environment: {
+ metricDashboard: {
+ annotations: annotationsData,
+ },
+ },
+ },
+ },
+ });
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [
+ { type: 'requestAnnotations' },
+ { type: 'receiveAnnotationsSuccess', payload: annotationsData },
+ ],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+
+ it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
+ const mockMutate = jest.spyOn(gqClient, 'mutate');
+ const mutationVariables = {
+ mutation: getAnnotations,
+ variables: {
+ projectPath: state.projectPath,
+ environmentName: state.currentEnvironmentName,
+ dashboardId: state.currentDashboard,
+ },
+ };
+
+ mockMutate.mockRejectedValue({});
+
+ return testAction(
+ fetchAnnotations,
+ null,
+ state,
+ [],
+ [{ type: 'requestAnnotations' }, { type: 'receiveAnnotationsFailure' }],
+ () => {
+ expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
+ },
+ );
+ });
+ });
+
describe('Set initial state', () => {
let mockedState;
beforeEach(() => {
diff --git a/spec/frontend/smart_interval_spec.js b/spec/frontend/smart_interval_spec.js
new file mode 100644
index 00000000000..b32ac99e4e4
--- /dev/null
+++ b/spec/frontend/smart_interval_spec.js
@@ -0,0 +1,197 @@
+import $ from 'jquery';
+import { assignIn } from 'lodash';
+import waitForPromises from 'helpers/wait_for_promises';
+import SmartInterval from '~/smart_interval';
+
+jest.useFakeTimers();
+
+let interval;
+
+describe('SmartInterval', () => {
+ const DEFAULT_MAX_INTERVAL = 100;
+ const DEFAULT_STARTING_INTERVAL = 5;
+ const DEFAULT_INCREMENT_FACTOR = 2;
+
+ function createDefaultSmartInterval(config) {
+ const defaultParams = {
+ callback: () => Promise.resolve(),
+ startingInterval: DEFAULT_STARTING_INTERVAL,
+ maxInterval: DEFAULT_MAX_INTERVAL,
+ incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
+ lazyStart: false,
+ immediateExecution: false,
+ hiddenInterval: null,
+ };
+
+ if (config) {
+ assignIn(defaultParams, config);
+ }
+
+ return new SmartInterval(defaultParams);
+ }
+
+ afterEach(() => {
+ interval.destroy();
+ });
+
+ describe('Increment Interval', () => {
+ it('should increment the interval delay', () => {
+ interval = createDefaultSmartInterval();
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const intervalConfig = interval.cfg;
+ const iterationCount = 4;
+ const maxIntervalAfterIterations =
+ intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
+ const currentInterval = interval.getCurrentInterval();
+
+ // Provide some flexibility for performance of testing environment
+ expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
+ expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
+ });
+ });
+
+ it('should not increment past maxInterval', () => {
+ interval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const currentInterval = interval.getCurrentInterval();
+
+ expect(currentInterval).toBe(interval.cfg.maxInterval);
+ });
+ });
+
+ it('does not increment while waiting for callback', () => {
+ interval = createDefaultSmartInterval({
+ callback: () => new Promise($.noop),
+ });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ const oneInterval = interval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
+
+ expect(interval.getCurrentInterval()).toEqual(oneInterval);
+ });
+ });
+ });
+
+ describe('Public methods', () => {
+ beforeEach(() => {
+ interval = createDefaultSmartInterval();
+ });
+
+ it('should cancel an interval', () => {
+ jest.runOnlyPendingTimers();
+
+ interval.cancel();
+
+ return waitForPromises().then(() => {
+ const { intervalId } = interval.state;
+ const currentInterval = interval.getCurrentInterval();
+ const intervalLowerLimit = interval.cfg.startingInterval;
+
+ expect(intervalId).toBeUndefined();
+ expect(currentInterval).toBe(intervalLowerLimit);
+ });
+ });
+
+ it('should resume an interval', () => {
+ jest.runOnlyPendingTimers();
+
+ interval.cancel();
+
+ interval.resume();
+
+ return waitForPromises().then(() => {
+ const { intervalId } = interval.state;
+
+ expect(intervalId).toBeTruthy();
+ });
+ });
+ });
+
+ describe('DOM Events', () => {
+ beforeEach(() => {
+ // This ensures DOM and DOM events are initialized for these specs.
+ setFixtures('<div></div>');
+
+ interval = createDefaultSmartInterval();
+ });
+
+ it('should pause when page is not visible', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+ });
+ });
+
+ it('should change to the hidden interval when page is not visible', () => {
+ interval.destroy();
+
+ const HIDDEN_INTERVAL = 1500;
+ interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
+
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(
+ interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
+ interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
+ ).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
+ });
+ });
+
+ it('should resume when page is becomes visible at the previous interval', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ expect(interval.state.intervalId).toBeTruthy();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'hidden' } });
+
+ expect(interval.state.intervalId).toBeUndefined();
+
+ // simulates triggering of visibilitychange event
+ interval.onVisibilityChange({ target: { visibilityState: 'visible' } });
+
+ expect(interval.state.intervalId).toBeTruthy();
+ });
+ });
+
+ it('should cancel on page unload', () => {
+ jest.runOnlyPendingTimers();
+
+ return waitForPromises().then(() => {
+ $(document).triggerHandler('beforeunload');
+
+ expect(interval.state.intervalId).toBeUndefined();
+ expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
+ });
+ });
+
+ it('should execute callback before first interval', () => {
+ interval = createDefaultSmartInterval({ immediateExecution: true });
+
+ expect(interval.cfg.immediateExecution).toBeFalsy();
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/static_site_editor_spec.js b/spec/frontend/static_site_editor/components/static_site_editor_spec.js
index a40f8edbeb2..2c4fa0e061a 100644
--- a/spec/frontend/static_site_editor/components/static_site_editor_spec.js
+++ b/spec/frontend/static_site_editor/components/static_site_editor_spec.js
@@ -30,7 +30,6 @@ describe('StaticSiteEditor', () => {
store = new Vuex.Store({
state: createState(initialState),
getters: {
- isContentLoaded: () => false,
contentChanged: () => false,
...getters,
},
@@ -43,9 +42,11 @@ describe('StaticSiteEditor', () => {
};
const buildContentLoadedStore = ({ initialState, getters } = {}) => {
buildStore({
- initialState,
+ initialState: {
+ isContentLoaded: true,
+ ...initialState,
+ },
getters: {
- isContentLoaded: () => true,
...getters,
},
});
@@ -85,7 +86,7 @@ describe('StaticSiteEditor', () => {
const content = 'edit area content';
beforeEach(() => {
- buildStore({ initialState: { content }, getters: { isContentLoaded: () => true } });
+ buildContentLoadedStore({ initialState: { content } });
buildWrapper();
});
diff --git a/spec/frontend/static_site_editor/store/getters_spec.js b/spec/frontend/static_site_editor/store/getters_spec.js
index 1b482db9366..5793e344784 100644
--- a/spec/frontend/static_site_editor/store/getters_spec.js
+++ b/spec/frontend/static_site_editor/store/getters_spec.js
@@ -1,18 +1,8 @@
import createState from '~/static_site_editor/store/state';
-import { isContentLoaded, contentChanged } from '~/static_site_editor/store/getters';
+import { contentChanged } from '~/static_site_editor/store/getters';
import { sourceContent as content } from '../mock_data';
describe('Static Site Editor Store getters', () => {
- describe('isContentLoaded', () => {
- it('returns true when originalContent is not empty', () => {
- expect(isContentLoaded(createState({ originalContent: content }))).toBe(true);
- });
-
- it('returns false when originalContent is empty', () => {
- expect(isContentLoaded(createState({ originalContent: '' }))).toBe(false);
- });
- });
-
describe('contentChanged', () => {
it('returns true when content and originalContent are different', () => {
const state = createState({ content, originalContent: 'something else' });
diff --git a/spec/frontend/static_site_editor/store/mutations_spec.js b/spec/frontend/static_site_editor/store/mutations_spec.js
index 1fd687eed4a..0b213c11a04 100644
--- a/spec/frontend/static_site_editor/store/mutations_spec.js
+++ b/spec/frontend/static_site_editor/store/mutations_spec.js
@@ -19,6 +19,7 @@ describe('Static Site Editor Store mutations', () => {
mutation | stateProperty | payload | expectedValue
${types.LOAD_CONTENT} | ${'isLoadingContent'} | ${undefined} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'isLoadingContent'} | ${contentLoadedPayload} | ${false}
+ ${types.RECEIVE_CONTENT_SUCCESS} | ${'isContentLoaded'} | ${contentLoadedPayload} | ${true}
${types.RECEIVE_CONTENT_SUCCESS} | ${'title'} | ${contentLoadedPayload} | ${title}
${types.RECEIVE_CONTENT_SUCCESS} | ${'content'} | ${contentLoadedPayload} | ${content}
${types.RECEIVE_CONTENT_SUCCESS} | ${'originalContent'} | ${contentLoadedPayload} | ${content}
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index ef95cb1b8f2..e022f68fdec 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -273,25 +273,6 @@ describe('mrWidgetOptions', () => {
};
});
- it('should not tell service to check status if document is not visible', () => {
- Object.defineProperty(document, 'visibilityState', {
- value: 'hidden',
- configurable: true,
- });
- vm.checkStatus(cb);
-
- return vm.$nextTick().then(() => {
- expect(vm.service.checkStatus).not.toHaveBeenCalled();
- expect(vm.mr.setData).not.toHaveBeenCalled();
- expect(vm.handleNotification).not.toHaveBeenCalled();
- expect(isCbExecuted).toBeFalsy();
- Object.defineProperty(document, 'visibilityState', {
- value: 'visible',
- configurable: true,
- });
- });
- });
-
it('should tell service to check status if document is visible', () => {
vm.checkStatus(cb);
diff --git a/spec/javascripts/smart_interval_spec.js b/spec/javascripts/smart_interval_spec.js
deleted file mode 100644
index 0dc9ee9d79a..00000000000
--- a/spec/javascripts/smart_interval_spec.js
+++ /dev/null
@@ -1,234 +0,0 @@
-import $ from 'jquery';
-import { assignIn } from 'lodash';
-import waitForPromises from 'spec/helpers/wait_for_promises';
-import SmartInterval from '~/smart_interval';
-
-describe('SmartInterval', function() {
- const DEFAULT_MAX_INTERVAL = 100;
- const DEFAULT_STARTING_INTERVAL = 5;
- const DEFAULT_SHORT_TIMEOUT = 75;
- const DEFAULT_INCREMENT_FACTOR = 2;
-
- function createDefaultSmartInterval(config) {
- const defaultParams = {
- callback: () => Promise.resolve(),
- startingInterval: DEFAULT_STARTING_INTERVAL,
- maxInterval: DEFAULT_MAX_INTERVAL,
- incrementByFactorOf: DEFAULT_INCREMENT_FACTOR,
- lazyStart: false,
- immediateExecution: false,
- hiddenInterval: null,
- };
-
- if (config) {
- assignIn(defaultParams, config);
- }
-
- return new SmartInterval(defaultParams);
- }
-
- beforeEach(() => {
- jasmine.clock().install();
- });
-
- afterEach(() => {
- jasmine.clock().uninstall();
- });
-
- describe('Increment Interval', function() {
- it('should increment the interval delay', done => {
- const smartInterval = createDefaultSmartInterval();
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- const intervalConfig = smartInterval.cfg;
- const iterationCount = 4;
- const maxIntervalAfterIterations =
- intervalConfig.startingInterval * intervalConfig.incrementByFactorOf ** iterationCount;
- const currentInterval = smartInterval.getCurrentInterval();
-
- // Provide some flexibility for performance of testing environment
- expect(currentInterval).toBeGreaterThan(intervalConfig.startingInterval);
- expect(currentInterval).toBeLessThanOrEqual(maxIntervalAfterIterations);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should not increment past maxInterval', done => {
- const smartInterval = createDefaultSmartInterval({ maxInterval: DEFAULT_STARTING_INTERVAL });
-
- jasmine.clock().tick(DEFAULT_STARTING_INTERVAL);
- jasmine.clock().tick(DEFAULT_STARTING_INTERVAL * DEFAULT_INCREMENT_FACTOR);
-
- waitForPromises()
- .then(() => {
- const currentInterval = smartInterval.getCurrentInterval();
-
- expect(currentInterval).toBe(smartInterval.cfg.maxInterval);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('does not increment while waiting for callback', done => {
- const smartInterval = createDefaultSmartInterval({
- callback: () => new Promise($.noop),
- });
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- const oneInterval = smartInterval.cfg.startingInterval * DEFAULT_INCREMENT_FACTOR;
-
- expect(smartInterval.getCurrentInterval()).toEqual(oneInterval);
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('Public methods', function() {
- beforeEach(function() {
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should cancel an interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- interval.cancel();
-
- waitForPromises()
- .then(() => {
- const { intervalId } = interval.state;
- const currentInterval = interval.getCurrentInterval();
- const intervalLowerLimit = interval.cfg.startingInterval;
-
- expect(intervalId).toBeUndefined();
- expect(currentInterval).toBe(intervalLowerLimit);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should resume an interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- interval.cancel();
-
- interval.resume();
-
- waitForPromises()
- .then(() => {
- const { intervalId } = interval.state;
-
- expect(intervalId).toBeTruthy();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-
- describe('DOM Events', function() {
- beforeEach(function() {
- // This ensures DOM and DOM events are initialized for these specs.
- setFixtures('<div></div>');
-
- this.smartInterval = createDefaultSmartInterval();
- });
-
- it('should pause when page is not visible', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should change to the hidden interval when page is not visible', done => {
- const HIDDEN_INTERVAL = 1500;
- const interval = createDefaultSmartInterval({ hiddenInterval: HIDDEN_INTERVAL });
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
- expect(
- interval.getCurrentInterval() >= DEFAULT_STARTING_INTERVAL &&
- interval.getCurrentInterval() <= DEFAULT_MAX_INTERVAL,
- ).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeTruthy();
- expect(interval.getCurrentInterval()).toBe(HIDDEN_INTERVAL);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should resume when page is becomes visible at the previous interval', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- expect(interval.state.intervalId).toBeTruthy();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'hidden' } });
-
- expect(interval.state.intervalId).toBeUndefined();
-
- // simulates triggering of visibilitychange event
- interval.handleVisibilityChange({ target: { visibilityState: 'visible' } });
-
- expect(interval.state.intervalId).toBeTruthy();
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should cancel on page unload', function(done) {
- const interval = this.smartInterval;
-
- jasmine.clock().tick(DEFAULT_SHORT_TIMEOUT);
-
- waitForPromises()
- .then(() => {
- $(document).triggerHandler('beforeunload');
-
- expect(interval.state.intervalId).toBeUndefined();
- expect(interval.getCurrentInterval()).toBe(interval.cfg.startingInterval);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('should execute callback before first interval', function() {
- const interval = createDefaultSmartInterval({ immediateExecution: true });
-
- expect(interval.cfg.immediateExecution).toBeFalsy();
- });
- });
-});
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
index e11613b202d..6e8a8c03aad 100644
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
+++ b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb
@@ -113,28 +113,22 @@ describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_r
end
describe 'droppable?' do
- where(:idempotent, :duplicate, :feature_enabled) do
- # [true, false].repeated_permutation(3)
- [[true, true, true],
- [true, true, false],
- [true, false, true],
- [true, false, false],
- [false, true, true],
- [false, true, false],
- [false, false, true],
- [false, false, false]]
+ where(:idempotent, :duplicate) do
+ # [true, false].repeated_permutation(2)
+ [[true, true],
+ [true, false],
+ [false, true],
+ [false, false]]
end
with_them do
before do
allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent)
allow(duplicate_job).to receive(:duplicate?).and_return(duplicate)
- allow(Gitlab::SidekiqMiddleware::DuplicateJobs)
- .to receive(:drop_duplicates?).with(queue).and_return(feature_enabled)
end
it 'is droppable when all conditions are met' do
- if idempotent && duplicate && feature_enabled
+ if idempotent && duplicate
expect(duplicate_job).to be_droppable
else
expect(duplicate_job).not_to be_droppable
diff --git a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb b/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb
deleted file mode 100644
index fa5938f470b..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/duplicate_jobs_spec.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-describe Gitlab::SidekiqMiddleware::DuplicateJobs do
- using RSpec::Parameterized::TableSyntax
-
- describe '.drop_duplicates?' do
- where(:global_feature_enabled, :selected_queue_enabled, :queue, :expected) do
- true | true | described_class::DROPPABLE_QUEUES.first | true
- true | true | "other_queue" | true
- true | false | described_class::DROPPABLE_QUEUES.first | true
- true | false | "other_queue" | true
- false | true | described_class::DROPPABLE_QUEUES.first | true
- false | true | "other_queue" | false
- false | false | described_class::DROPPABLE_QUEUES.first | false
- false | false | "other_queue" | false
- end
-
- with_them do
- before do
- stub_feature_flags(drop_duplicate_sidekiq_jobs: global_feature_enabled,
- drop_duplicate_sidekiq_jobs_for_queue: selected_queue_enabled)
- end
-
- it "allows dropping jobs when expected" do
- expect(described_class.drop_duplicates?(queue)).to be(expected)
- end
- end
- end
-end
diff --git a/spec/mailers/emails/pages_domains_spec.rb b/spec/mailers/emails/pages_domains_spec.rb
index 78887cef7ab..5029a17e4e5 100644
--- a/spec/mailers/emails/pages_domains_spec.rb
+++ b/spec/mailers/emails/pages_domains_spec.rb
@@ -23,13 +23,20 @@ describe Emails::PagesDomains do
is_expected.to have_subject(email_subject)
is_expected.to have_body_text(project.human_name)
is_expected.to have_body_text(domain.domain)
- is_expected.to have_body_text domain.url
is_expected.to have_body_text project_pages_domain_url(project, domain)
- is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
end
end
end
+ shared_examples 'a pages domain verification email' do
+ it_behaves_like 'a pages domain email'
+
+ it 'has the expected content' do
+ is_expected.to have_body_text domain.url
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: link_anchor)
+ end
+ end
+
shared_examples 'notification about upcoming domain removal' do
context 'when domain is not scheduled for removal' do
it 'asks user to remove it' do
@@ -56,7 +63,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_enabled_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it { is_expected.to have_body_text 'has been enabled' }
end
@@ -67,7 +74,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_disabled_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it_behaves_like 'notification about upcoming domain removal'
@@ -80,7 +87,7 @@ describe Emails::PagesDomains do
subject { Notify.pages_domain_verification_succeeded_email(domain, user) }
- it_behaves_like 'a pages domain email'
+ it_behaves_like 'a pages domain verification email'
it { is_expected.to have_body_text 'successfully verified' }
end
@@ -94,10 +101,18 @@ describe Emails::PagesDomains do
it_behaves_like 'a pages domain email'
it_behaves_like 'notification about upcoming domain removal'
+ end
+
+ describe '#pages_domain_auto_ssl_failed_email' do
+ let(:email_subject) { "#{project.path} | ACTION REQUIRED: Something went wrong while obtaining the Let's Encrypt certificate for GitLab Pages domain '#{domain.domain}'" }
+
+ subject { Notify.pages_domain_auto_ssl_failed_email(domain, user) }
+
+ it_behaves_like 'a pages domain email'
- it 'says verification has failed and when the domain is enabled until' do
- is_expected.to have_body_text 'Verification has failed'
- is_expected.to have_body_text domain.enabled_until.strftime('%F %T')
+ it 'says that we failed to obtain certificate' do
+ is_expected.to have_body_text "Something went wrong while obtaining the Let's Encrypt certificate."
+ is_expected.to have_body_text help_page_url('user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md', anchor: 'troubleshooting')
end
end
end
diff --git a/spec/models/ci/job_artifact_spec.rb b/spec/models/ci/job_artifact_spec.rb
index 6f6ff3704b4..80b619ed2b1 100644
--- a/spec/models/ci/job_artifact_spec.rb
+++ b/spec/models/ci/job_artifact_spec.rb
@@ -349,16 +349,13 @@ describe Ci::JobArtifact do
end
describe 'file is being stored' do
- subject { create(:ci_job_artifact, :archive) }
-
context 'when object has nil store' do
- before do
- subject.update_column(:file_store, nil)
- subject.reload
- end
-
it 'is stored locally' do
- expect(subject.file_store).to be(nil)
+ subject = build(:ci_job_artifact, :archive, file_store: nil)
+
+ subject.save
+
+ expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
expect(subject.file).to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
end
@@ -366,6 +363,10 @@ describe Ci::JobArtifact do
context 'when existing object has local store' do
it 'is stored locally' do
+ subject = build(:ci_job_artifact, :archive)
+
+ subject.save
+
expect(subject.file_store).to be(ObjectStorage::Store::LOCAL)
expect(subject.file).to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::LOCAL)
@@ -379,6 +380,10 @@ describe Ci::JobArtifact do
context 'when file is stored' do
it 'is stored remotely' do
+ subject = build(:ci_job_artifact, :archive)
+
+ subject.save
+
expect(subject.file_store).to eq(ObjectStorage::Store::REMOTE)
expect(subject.file).not_to be_file_storage
expect(subject.file.object_store).to eq(ObjectStorage::Store::REMOTE)
diff --git a/spec/models/diff_note_position_spec.rb b/spec/models/diff_note_position_spec.rb
new file mode 100644
index 00000000000..a00ba35feef
--- /dev/null
+++ b/spec/models/diff_note_position_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe DiffNotePosition, type: :model do
+ it 'has a position attribute' do
+ diff_position = build(:diff_position)
+ line_code = 'bd4b7bfff3a247ccf6e3371c41ec018a55230bcc_534_521'
+ diff_note_position = build(:diff_note_position, line_code: line_code, position: diff_position)
+
+ expect(diff_note_position.position).to eq(diff_position)
+ expect(diff_note_position.line_code).to eq(line_code)
+ expect(diff_note_position.diff_content_type).to eq('text')
+ end
+
+ it 'unique by note_id and diff type' do
+ existing_diff_note_position = create(:diff_note_position)
+ diff_note_position = build(:diff_note_position, note: existing_diff_note_position.note)
+
+ expect { diff_note_position.save! }.to raise_error(ActiveRecord::RecordNotUnique)
+ end
+end
diff --git a/spec/requests/api/group_clusters_spec.rb b/spec/requests/api/group_clusters_spec.rb
index d3bd84f1604..fade54f6b11 100644
--- a/spec/requests/api/group_clusters_spec.rb
+++ b/spec/requests/api/group_clusters_spec.rb
@@ -157,6 +157,7 @@ describe API::GroupClusters do
let(:api_url) { 'https://kubernetes.example.com' }
let(:authorization_type) { 'rbac' }
+ let(:management_project_id) { create(:project, group: group).id }
let(:platform_kubernetes_attributes) do
{
@@ -171,7 +172,8 @@ describe API::GroupClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
@@ -203,6 +205,7 @@ describe API::GroupClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.management_project_id).to eq management_project_id
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.token).to eq('sample-token')
@@ -234,6 +237,18 @@ describe API::GroupClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with invalid params' do
let(:api_url) { 'invalid_api_url' }
diff --git a/spec/requests/api/project_clusters_spec.rb b/spec/requests/api/project_clusters_spec.rb
index 648577dce8d..ed899e830e1 100644
--- a/spec/requests/api/project_clusters_spec.rb
+++ b/spec/requests/api/project_clusters_spec.rb
@@ -150,6 +150,12 @@ describe API::ProjectClusters do
let(:api_url) { 'https://kubernetes.example.com' }
let(:namespace) { project.path }
let(:authorization_type) { 'rbac' }
+ let(:management_project) { create(:project, namespace: project.namespace) }
+ let(:management_project_id) { management_project.id }
+
+ before do
+ management_project.add_maintainer(current_user)
+ end
let(:platform_kubernetes_attributes) do
{
@@ -165,7 +171,8 @@ describe API::ProjectClusters do
name: 'test-cluster',
domain: 'domain.example.com',
managed: false,
- platform_kubernetes_attributes: platform_kubernetes_attributes
+ platform_kubernetes_attributes: platform_kubernetes_attributes,
+ management_project_id: management_project_id
}
end
@@ -194,6 +201,7 @@ describe API::ProjectClusters do
expect(cluster_result.name).to eq('test-cluster')
expect(cluster_result.domain).to eq('domain.example.com')
expect(cluster_result.managed).to be_falsy
+ expect(cluster_result.management_project_id).to eq management_project_id
expect(platform_kubernetes.rbac?).to be_truthy
expect(platform_kubernetes.api_url).to eq(api_url)
expect(platform_kubernetes.namespace).to eq(namespace)
@@ -227,6 +235,18 @@ describe API::ProjectClusters do
end
end
+ context 'current user does not have access to management_project_id' do
+ let(:management_project_id) { create(:project).id }
+
+ it 'responds with 400' do
+ expect(response).to have_gitlab_http_status(:bad_request)
+ end
+
+ it 'returns validation errors' do
+ expect(json_response['message']['management_project_id'].first).to match('don\'t have permission')
+ end
+ end
+
context 'with invalid params' do
let(:namespace) { 'invalid_namespace' }
diff --git a/spec/rubocop/cop/performance/ar_count_each_spec.rb b/spec/rubocop/cop/performance/ar_count_each_spec.rb
new file mode 100644
index 00000000000..f934a1fde48
--- /dev/null
+++ b/spec/rubocop/cop/performance/ar_count_each_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require_relative '../../../support/helpers/expect_offense'
+require_relative '../../../../rubocop/cop/performance/ar_count_each.rb'
+
+describe RuboCop::Cop::Performance::ARCountEach do
+ include CopHelper
+ include ExpectOffense
+
+ subject(:cop) { described_class.new }
+
+ context 'when it is not haml file' do
+ it 'does not flag it as an offense' do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(false)
+
+ expect_no_offenses <<~SOURCE
+ show(@users.count)
+ @users.each { |user| display(user) }
+ SOURCE
+ end
+ end
+
+ context 'when it is haml file' do
+ before do
+ expect(subject).to receive(:in_haml_file?).with(anything).at_least(:once).and_return(true)
+ end
+
+ context 'when the same object uses count and each' do
+ it 'flags it as an offense' do
+ expect_offense <<~SOURCE
+ show(@users.count)
+ ^^^^^^^^^^^^ If @users is AR relation, avoid `@users.count ...; @users.each... `, this will trigger two queries. Use `@users.load.size ...; @users.each... ` instead. If @users is an array, try to use @users.size.
+ @users.each { |user| display(user) }
+ SOURCE
+
+ expect(cop.offenses.map(&:cop_name)).to contain_exactly('Performance/ARCountEach')
+ end
+ end
+
+ context 'when different object uses count and each' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses <<~SOURCE
+ show(@emails.count)
+ @users.each { |user| display(user) }
+ SOURCE
+ end
+ end
+
+ context 'when just using count without each' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.count'
+ end
+ end
+
+ context 'when just using each without count' do
+ it 'does not flag it as an offense' do
+ expect_no_offenses '@users.each { |user| display(user) }'
+ end
+ end
+ end
+end
diff --git a/spec/services/application_settings/update_service_spec.rb b/spec/services/application_settings/update_service_spec.rb
index 6e1fdb7aad0..069572e4dff 100644
--- a/spec/services/application_settings/update_service_spec.rb
+++ b/spec/services/application_settings/update_service_spec.rb
@@ -334,4 +334,20 @@ describe ApplicationSettings::UpdateService do
expect(application_settings.protected_paths).to eq(['/users/password', '/users/sign_in'])
end
end
+
+ context 'when issues_create_limit is passsed' do
+ let(:params) do
+ {
+ issues_create_limit: 600
+ }
+ end
+
+ it 'updates issues_create_limit value' do
+ subject.execute
+
+ application_settings.reload
+
+ expect(application_settings.issues_create_limit).to eq(600)
+ end
+ end
end
diff --git a/spec/services/clusters/create_service_spec.rb b/spec/services/clusters/create_service_spec.rb
index ecf0a9c9dce..3dd25be2a3d 100644
--- a/spec/services/clusters/create_service_spec.rb
+++ b/spec/services/clusters/create_service_spec.rb
@@ -59,4 +59,92 @@ describe Clusters::CreateService do
end
end
end
+
+ context 'when params includes :management_project_id' do
+ subject(:cluster) { described_class.new(user, params).execute(access_token: access_token) }
+
+ let(:params) do
+ {
+ name: 'test-cluster',
+ provider_type: :gcp,
+ provider_gcp_attributes: {
+ gcp_project_id: 'gcp-project',
+ zone: 'us-central1-a',
+ num_nodes: 1,
+ machine_type: 'machine_type-a',
+ legacy_abac: 'true'
+ },
+ clusterable: clusterable,
+ management_project_id: management_project_id
+ }
+ end
+
+ let(:clusterable) { project }
+ let(:management_project_id) { management_project.id }
+ let(:management_project_namespace) { project.namespace }
+ let(:management_project) { create(:project, namespace: management_project_namespace) }
+
+ shared_examples 'invalid project or cluster permissions' do
+ it 'does not persist the cluster and adds errors' do
+ expect(cluster).not_to be_persisted
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+
+ shared_examples 'setting a management project' do
+ context 'when user is authorized to adminster manangement_project' do
+ before do
+ management_project.add_maintainer(user)
+ end
+
+ it 'persists the cluster' do
+ expect(cluster).to be_persisted
+
+ expect(cluster.management_project).to eq(management_project)
+ end
+ end
+
+ context 'when user is not authorized to adminster manangement_project' do
+ include_examples 'invalid project or cluster permissions'
+ end
+ end
+
+ shared_examples 'setting a management project outside of scope' do
+ context 'when manangement_project is outside of the namespace scope' do
+ let(:management_project_namespace) { create(:group) }
+
+ it 'does not persist the cluster' do
+ expect(cluster).not_to be_persisted
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ context 'management_project is non-existent' do
+ let(:management_project_id) { 0 }
+
+ include_examples 'invalid project or cluster permissions'
+ end
+
+ context 'project cluster' do
+ include_examples 'setting a management project'
+ include_examples 'setting a management project outside of scope'
+ end
+
+ context 'group cluster' do
+ let(:management_project_namespace) { create(:group) }
+ let(:clusterable) { management_project_namespace }
+
+ include_examples 'setting a management project'
+ include_examples 'setting a management project outside of scope'
+ end
+
+ context 'instance cluster' do
+ let(:clusterable) { Clusters::Instance.new }
+
+ include_examples 'setting a management project'
+ end
+ end
end
diff --git a/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
new file mode 100644
index 00000000000..1bcebe2e2ac
--- /dev/null
+++ b/spec/services/clusters/management/validate_management_project_permissions_service_spec.rb
@@ -0,0 +1,88 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Clusters::Management::ValidateManagementProjectPermissionsService do
+ describe '#execute' do
+ subject { described_class.new(user).execute(cluster, management_project_id) }
+
+ let(:cluster) { build(:cluster, :project, projects: [create(:project)]) }
+ let(:user) { create(:user) }
+
+ context 'when management_project_id is nil' do
+ let(:management_project_id) { nil }
+
+ it { is_expected.to be true }
+ end
+
+ context 'when management_project_id is not nil' do
+ let(:management_project_id) { management_project.id }
+ let(:management_project_namespace) { create(:group) }
+ let(:management_project) { create(:project, namespace: management_project_namespace) }
+
+ context 'when management_project does not exist' do
+ let(:management_project_id) { 0 }
+
+ it 'adds errors to the cluster and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+
+ shared_examples 'management project is in scope' do
+ context 'when user is authorized to administer manangement_project' do
+ before do
+ management_project.add_maintainer(user)
+ end
+
+ it 'adds no error and returns true' do
+ is_expected.to eq true
+
+ expect(cluster.errors).to be_empty
+ end
+ end
+
+ context 'when user is not authorized to adminster manangement_project' do
+ it 'adds an error and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ shared_examples 'management project is out of scope' do
+ context 'when manangement_project is outside of the namespace scope' do
+ let(:management_project_namespace) { create(:group) }
+
+ it 'adds an error and returns false' do
+ is_expected.to eq false
+
+ expect(cluster.errors[:management_project_id]).to include('Project does not exist or you don\'t have permission to perform this action')
+ end
+ end
+ end
+
+ context 'project cluster' do
+ let(:cluster) { build(:cluster, :project, projects: [create(:project, namespace: management_project_namespace)]) }
+
+ include_examples 'management project is in scope'
+ include_examples 'management project is out of scope'
+ end
+
+ context 'group cluster' do
+ let(:cluster) { build(:cluster, :group, groups: [management_project_namespace]) }
+
+ include_examples 'management project is in scope'
+ include_examples 'management project is out of scope'
+ end
+
+ context 'instance cluster' do
+ let(:cluster) { build(:cluster, :instance) }
+
+ include_examples 'management project is in scope'
+ end
+ end
+ end
+end
diff --git a/spec/services/environments/auto_stop_service_spec.rb b/spec/services/environments/auto_stop_service_spec.rb
index 3620bf8fe87..b34d15889d3 100644
--- a/spec/services/environments/auto_stop_service_spec.rb
+++ b/spec/services/environments/auto_stop_service_spec.rb
@@ -40,18 +40,6 @@ describe Environments::AutoStopService, :clean_gitlab_redis_shared_state do
expect(Ci::Build.where(name: 'stop_review_app').map(&:status).uniq).to eq(['pending'])
end
- context 'when auto_stop_environments feature flag is disabled' do
- before do
- stub_feature_flags(auto_stop_environments: false)
- end
-
- it 'does not execute Ci::StopEnvironmentsService' do
- expect(Ci::StopEnvironmentsService).not_to receive(:execute_in_batch)
-
- subject
- end
- end
-
context 'when the other sidekiq worker has already been running' do
before do
stub_exclusive_lease_taken(described_class::EXCLUSIVE_LOCK_KEY)
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 86f37e9204c..163ca0b9bc3 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -2604,6 +2604,7 @@ describe NotificationService, :mailer do
pages_domain_disabled
pages_domain_verification_succeeded
pages_domain_verification_failed
+ pages_domain_auto_ssl_failed
].each do |sym|
describe "##{sym}" do
subject(:notify!) { notification.send(sym, domain) }
diff --git a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
index 163276db7e6..63fd0978c97 100644
--- a/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
+++ b/spec/services/pages_domains/obtain_lets_encrypt_certificate_service_spec.rb
@@ -180,5 +180,13 @@ describe PagesDomains::ObtainLetsEncryptCertificateService do
expect(PagesDomainAcmeOrder.find_by_id(existing_order.id)).to be_nil
end
+
+ it 'sends notification' do
+ expect_next_instance_of(NotificationService) do |notification_service|
+ expect(notification_service).to receive(:pages_domain_auto_ssl_failed).with(pages_domain)
+ end
+
+ service.execute
+ end
end
end
diff --git a/spec/uploaders/records_uploads_spec.rb b/spec/uploaders/records_uploads_spec.rb
index 71eff23c77c..140595e58ad 100644
--- a/spec/uploaders/records_uploads_spec.rb
+++ b/spec/uploaders/records_uploads_spec.rb
@@ -78,7 +78,8 @@ describe RecordsUploads do
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
model: build_stubbed(:user),
- uploader: uploader.class.to_s
+ uploader: uploader.class.to_s,
+ store: ::ObjectStorage::Store::LOCAL
)
uploader.upload = existing
@@ -98,7 +99,8 @@ describe RecordsUploads do
path: File.join('uploads', 'rails_sample.jpg'),
size: 512.kilobytes,
model: project,
- uploader: uploader.class.to_s
+ uploader: uploader.class.to_s,
+ store: ::ObjectStorage::Store::LOCAL
)
uploader.store!(upload_fixture('rails_sample.jpg'))
diff --git a/spec/views/projects/pages/show.html.haml_spec.rb b/spec/views/projects/pages/show.html.haml_spec.rb
index 80410e7bc32..39384484279 100644
--- a/spec/views/projects/pages/show.html.haml_spec.rb
+++ b/spec/views/projects/pages/show.html.haml_spec.rb
@@ -17,7 +17,7 @@ describe 'projects/pages/show' do
assign(:project, project)
allow(view).to receive(:current_user).and_return(user)
- assign(:domains, [domain])
+ assign(:domains, project.pages_domains)
end
describe 'validation warning' do
diff --git a/yarn.lock b/yarn.lock
index 42a5c0d29c3..81bbf8a59e3 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1036,10 +1036,10 @@
"@sentry/types" "5.10.0"
tslib "^1.9.3"
-"@sourcegraph/code-host-integration@0.0.34":
- version "0.0.34"
- resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.34.tgz#c8f94854d64fe035926bbda7bed3a538a7259d03"
- integrity sha512-TAa5kU/zPb9PfB4HIhaEDhKKdW5Fx9YVx9WWMOwz9elD0y9FZoAXDO1o4Pz1cm1IP/VZwd8csypAWgfxsAmfzw==
+"@sourcegraph/code-host-integration@0.0.36":
+ version "0.0.36"
+ resolved "https://registry.yarnpkg.com/@sourcegraph/code-host-integration/-/code-host-integration-0.0.36.tgz#2f4d287840ac2944c78ef92f10f0db0ef8a077fa"
+ integrity sha512-Hpj1xiVhPxMsjLNre9MrYYAM1SPOWPE9yG9SPtz4dqYzc6/ycaPGyr+ljcaWEclS9hZCvkk4+qVC5WONpYVjyA==
"@types/anymatch@*":
version "1.3.0"