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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 03:08:20 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-20 03:08:20 +0300
commit59accb4c4780f194554b86c7be3c4a916fb70737 (patch)
treedac53b413bbac9ba1bc2a523ec64bed2c94d6f7a
parent680d18802596089dc407b7011bcf682d24846aec (diff)
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop.yml1
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue3
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue21
-rw-r--r--app/assets/stylesheets/bootstrap_migration.scss2
-rw-r--r--app/assets/stylesheets/components/dashboard_skeleton.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss2
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss6
-rw-r--r--app/assets/stylesheets/snippets.scss6
-rw-r--r--app/finders/alert_management/alerts_finder.rb6
-rw-r--r--app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb13
-rw-r--r--app/graphql/resolvers/alert_management_alert_resolver.rb4
-rw-r--r--app/graphql/types/alert_management/alert_status_counts_type.rb30
-rw-r--r--app/graphql/types/project_type.rb6
-rw-r--r--app/models/alert_management/alert.rb2
-rw-r--r--app/views/shared/milestones/_merge_requests_tab.haml8
-rw-r--r--changelogs/unreleased/207934-snippet-embed-scrolling.yml5
-rw-r--r--changelogs/unreleased/214710-rename-configure-to-enable.yml5
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml5
-rw-r--r--changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml5
-rw-r--r--changelogs/unreleased/leaky-constant-fix-26.yml5
-rw-r--r--changelogs/unreleased/sy-status-counts.yml5
-rw-r--r--config/initializers/7_prometheus_metrics.rb4
-rw-r--r--doc/administration/pages/index.md93
-rw-r--r--doc/api/graphql/reference/gitlab_schema.graphql40
-rw-r--r--doc/api/graphql/reference/gitlab_schema.json111
-rw-r--r--doc/api/graphql/reference/index.md14
-rw-r--r--doc/development/sidekiq_style_guide.md18
-rw-r--r--doc/user/application_security/configuration/index.md2
-rw-r--r--doc/user/application_security/img/security_configuration_page_v12_9.pngbin51545 -> 0 bytes
-rw-r--r--doc/user/application_security/img/security_configuration_page_v13_1.pngbin0 -> 63337 bytes
-rw-r--r--doc/user/compliance/license_compliance/index.md26
-rw-r--r--doc/user/group/roadmap/index.md3
-rw-r--r--lib/gitlab/alert_management/alert_status_counts.rb53
-rw-r--r--lib/gitlab/kubernetes/network_policy.rb2
-rw-r--r--lib/gitlab/metrics/samplers/database_sampler.rb58
-rw-r--r--lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb2
-rw-r--r--locale/gitlab.pot42
-rw-r--r--spec/finders/alert_management/alerts_finder_spec.rb328
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js (renamed from spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js)0
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js (renamed from spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js)19
-rw-r--r--spec/frontend/pages/admin/users/new/index_spec.js (renamed from spec/javascripts/pages/admin/users/new/index_spec.js)0
-rw-r--r--spec/frontend/pages/labels/components/promote_label_modal_spec.js (renamed from spec/javascripts/pages/labels/components/promote_label_modal_spec.js)8
-rw-r--r--spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js (renamed from spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js)25
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js (renamed from spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js)8
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js (renamed from spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js)50
-rw-r--r--spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js (renamed from spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js)0
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js2
-rw-r--r--spec/frontend/prometheus_metrics/mock_data.js44
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js (renamed from spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js)12
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb24
-rw-r--r--spec/graphql/types/alert_management/alert_status_count_type_spec.rb20
-rw-r--r--spec/graphql/types/project_type_spec.rb1
-rw-r--r--spec/javascripts/prometheus_metrics/mock_data.js41
-rw-r--r--spec/lib/gitlab/alert_management/alert_status_counts_spec.rb55
-rw-r--r--spec/lib/gitlab/kubernetes/network_policy_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb49
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job_spec.rb19
-rw-r--r--spec/lib/gitlab/view/presenter/factory_spec.rb6
-rw-r--r--spec/models/alert_management/alert_spec.rb42
-rw-r--r--spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb61
66 files changed, 1044 insertions, 398 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 707e1a3ea36..3d013a650e7 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -367,7 +367,6 @@ RSpec/LeakyConstantDeclaration:
- 'spec/lib/gitlab/quick_actions/dsl_spec.rb'
- 'spec/lib/gitlab/sidekiq_middleware/client_metrics_spec.rb'
- 'spec/lib/gitlab/sidekiq_middleware/server_metrics_spec.rb'
- - 'spec/lib/gitlab/view/presenter/factory_spec.rb'
- 'spec/lib/marginalia_spec.rb'
- 'spec/mailers/notify_spec.rb'
- 'spec/migrations/20191125114345_add_admin_mode_protected_path_spec.rb'
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
index 22512a6f12a..da96e6f36b4 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue
@@ -2,7 +2,8 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import Translate from '../../../../../vue_shared/translate';
-import illustrationSvg from '../icons/intro_illustration.svg';
+// Full path is needed for Jest to be able to correctly mock this file
+import illustrationSvg from '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
import { parseBoolean } from '~/lib/utils/common_utils';
Vue.use(Translate);
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index df239427224..c0967e9093c 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -127,7 +127,7 @@ export default {
},
methods: {
redirectToSnippets() {
- window.location.pathname = this.snippet.project?.fullPath || 'dashboard/snippets';
+ window.location.pathname = `${this.snippet.project?.fullPath || 'dashboard'}/snippets`;
},
closeDeleteModal() {
this.$refs.deleteModal.hide();
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
index 01a195049ba..f6bfb178437 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_tour.vue
@@ -1,5 +1,4 @@
<script>
-import { s__, sprintf } from '~/locale';
import { GlPopover, GlDeprecatedButton } from '@gitlab/ui';
import Icon from '~/vue_shared/components/icon.vue';
import Cookies from 'js-cookie';
@@ -15,18 +14,6 @@ export default {
dismissTrackValue: 20,
showTrackValue: 10,
trackEvent: 'click_button',
- popoverContent: sprintf(
- '%{messageText1}%{lineBreak}%{messageText2}%{lineBreak}%{messageText3}%{lineBreak}%{messageText4}%{lineBreak}%{messageText5}',
- {
- messageText1: s__('mrWidget|Detect issues before deployment with a CI pipeline'),
- messageText2: s__('mrWidget|that continuously tests your code. We created'),
- messageText3: s__("mrWidget|a quick guide that'll show you how to create"),
- messageText4: s__('mrWidget|one. Make your code more secure and more'),
- messageText5: s__('mrWidget|robust in just a minute.'),
- lineBreak: '<br/>',
- },
- false,
- ),
components: {
GlPopover,
GlDeprecatedButton,
@@ -110,7 +97,13 @@ export default {
<div class="svg-content svg-150 pt-1">
<img :src="pipelineSvgPath" />
</div>
- <p v-html="$options.popoverContent"></p>
+ <p>
+ {{
+ s__(
+ 'mrWidget|Detect issues before deployment with a CI pipeline that continuously tests your code. We created a quick guide that will show you how to create one. Make your code more secure and more robust in just a minute.',
+ )
+ }}
+ </p>
<gl-deprecated-button
ref="ok"
category="primary"
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index ed5c133950d..1c15400542a 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -211,7 +211,7 @@ h3.popover-header {
}
.info-well {
- background: $gray-50;
+ background: $gray-10;
color: $gl-text-color;
border: 1px solid $border-color;
border-radius: 4px;
diff --git a/app/assets/stylesheets/components/dashboard_skeleton.scss b/app/assets/stylesheets/components/dashboard_skeleton.scss
index 2e2c1fefc79..ce33aa94df3 100644
--- a/app/assets/stylesheets/components/dashboard_skeleton.scss
+++ b/app/assets/stylesheets/components/dashboard_skeleton.scss
@@ -68,7 +68,7 @@
background-size: cover;
background-image: linear-gradient(to right,
$gray-100 0%,
- $gray-50 20%,
+ $gray-10 20%,
$gray-100 40%,
$gray-100 100%);
border-radius: $gl-padding;
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index d222fc4aefe..13174687e5d 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -193,7 +193,7 @@ a {
background-size: cover;
background-image: linear-gradient(to right,
$gray-100 0%,
- $gray-50 20%,
+ $gray-10 20%,
$gray-100 40%,
$gray-100 100%);
height: 10px;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index aaad640b7f0..1afcbc6d514 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -86,7 +86,7 @@
line-height: 10px;
color: $gl-gray-700;
vertical-align: middle;
- background-color: $gray-50;
+ background-color: $gray-10;
border-width: 1px;
border-style: solid;
border-color: $gray-200 $gray-200 $gray-400;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 38f5ebdc4fa..ac4d431ea57 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -163,7 +163,8 @@ $red-800: #8b2615;
$red-900: #711e11;
$red-950: #4b140b;
-$gray-50: #fafafa;
+$gray-10: #fafafa;
+$gray-50: #f0f0f0;
$gray-100: #f2f2f2;
$gray-200: #dfdfdf;
$gray-300: #ccc;
@@ -232,6 +233,7 @@ $reds: (
);
$grays: (
+ '10': $gray-10,
'50': $gray-50,
'100': $gray-100,
'200': $gray-200,
@@ -699,7 +701,7 @@ $logs-p-color: #333;
*/
$input-height: 34px;
$input-danger-bg: #f2dede;
-$input-group-addon-bg: $gray-50;
+$input-group-addon-bg: $gray-10;
$gl-field-focus-shadow: rgba(0, 0, 0, 0.075);
$gl-field-focus-shadow-error: rgba($red-500, 0.6);
$input-short-width: 200px;
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
index 49175a244de..5675835a622 100644
--- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -147,7 +147,7 @@
.monaco-editor,
.monaco-editor-background,
.monaco-editor .inputarea.ime-input {
- background-color: $gray-50;
+ background-color: $gray-10;
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 37a176adf84..61914740ac0 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1124,7 +1124,7 @@ $ide-commit-header-height: 48px;
.ide-commit-editor-header {
height: 65px;
padding: 8px 16px;
- background-color: var(--ide-background, $gray-50);
+ background-color: var(--ide-background, $gray-10);
box-shadow: inset 0 -1px var(--ide-border-color, $white-dark);
}
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index d86bf92eac4..0f56b98a78d 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -151,7 +151,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-left: 4px solid $gray-50;
+ border-left: 4px solid $gray-10;
}
.arrow-shadow {
@@ -173,7 +173,7 @@
> .arrow::after {
border-top: 6px solid transparent;
border-bottom: 6px solid transparent;
- border-right: 4px solid $gray-50;
+ border-right: 4px solid $gray-10;
}
.arrow-shadow {
@@ -207,7 +207,7 @@
}
> .popover-title {
- background-color: $gray-50;
+ background-color: $gray-10;
border-radius: $border-radius-default $border-radius-default 0 0;
}
}
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index 93a12cf28a2..d410a16a1d9 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -40,10 +40,9 @@
margin: 0;
padding: 0;
table-layout: fixed;
+ overflow-x: auto;
.blob-content {
- overflow-x: auto;
-
pre {
height: 100%;
padding: 10px;
@@ -61,6 +60,7 @@
font-family: $monospace-font;
font-size: $code-font-size;
line-height: $code-line-height;
+ display: inline-block;
}
}
@@ -73,7 +73,7 @@
font-family: $monospace-font;
display: block;
font-size: $code-font-size;
- min-height: $code-line-height;
+ line-height: $code-line-height;
white-space: nowrap;
color: $black-transparent;
min-width: 30px;
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index e5fde50849e..cb35be43c15 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -2,6 +2,12 @@
module AlertManagement
class AlertsFinder
+ # @return [Hash<Integer,Integer>] Mapping of status id to count
+ # ex) { 0: 6, ...etc }
+ def self.counts_by_status(current_user, project, params = {})
+ new(current_user, project, params).execute.counts_by_status
+ end
+
def initialize(current_user, project, params)
@current_user = current_user
@project = project
diff --git a/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
new file mode 100644
index 00000000000..7f4346632ca
--- /dev/null
+++ b/app/graphql/resolvers/alert_management/alert_status_counts_resolver.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module AlertManagement
+ class AlertStatusCountsResolver < BaseResolver
+ type Types::AlertManagement::AlertStatusCountsType, null: true
+
+ def resolve(**args)
+ ::Gitlab::AlertManagement::AlertStatusCounts.new(context[:current_user], object, args)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/alert_management_alert_resolver.rb b/app/graphql/resolvers/alert_management_alert_resolver.rb
index 8e7fe566d32..51ebbb96476 100644
--- a/app/graphql/resolvers/alert_management_alert_resolver.rb
+++ b/app/graphql/resolvers/alert_management_alert_resolver.rb
@@ -23,9 +23,9 @@ module Resolvers
def resolve(**args)
parent = object.respond_to?(:sync) ? object.sync : object
- return AlertManagement::Alert.none if parent.nil?
+ return ::AlertManagement::Alert.none if parent.nil?
- AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
+ ::AlertManagement::AlertsFinder.new(context[:current_user], parent, args).execute
end
end
end
diff --git a/app/graphql/types/alert_management/alert_status_counts_type.rb b/app/graphql/types/alert_management/alert_status_counts_type.rb
new file mode 100644
index 00000000000..f80b289eabc
--- /dev/null
+++ b/app/graphql/types/alert_management/alert_status_counts_type.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+# Service for managing alert counts and cache updates.
+module Types
+ module AlertManagement
+ class AlertStatusCountsType < BaseObject
+ graphql_name 'AlertManagementAlertStatusCountsType'
+ description "Represents total number of alerts for the represented categories"
+
+ authorize :read_alert_management_alert
+
+ ::Gitlab::AlertManagement::AlertStatusCounts::STATUSES.each_key do |status|
+ field status,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: "Number of alerts with status #{status.upcase} for the project"
+ end
+
+ field :open,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: 'Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project'
+
+ field :all,
+ GraphQL::INT_TYPE,
+ null: true,
+ description: 'Total number of alerts for the project'
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index e7a83446610..4e438ed2576 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -218,6 +218,12 @@ module Types
description: 'A single Alert Management alert of the project',
resolver: Resolvers::AlertManagementAlertResolver.single
+ field :alert_management_alert_status_counts,
+ Types::AlertManagement::AlertStatusCountsType,
+ null: true,
+ description: 'Counts of alerts by status for the project',
+ resolver: Resolvers::AlertManagement::AlertStatusCountsResolver
+
field :releases,
Types::ReleaseType.connection_type,
null: true,
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index c030987e770..acaf474ecc2 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -106,6 +106,8 @@ module AlertManagement
scope :order_severity, -> (sort_order) { order(severity: sort_order) }
scope :order_status, -> (sort_order) { order(status: sort_order) }
+ scope :counts_by_status, -> { group(:status).count }
+
def self.sort_by_attribute(method)
case method.to_s
when 'start_time_asc' then order_start_time(:asc)
diff --git a/app/views/shared/milestones/_merge_requests_tab.haml b/app/views/shared/milestones/_merge_requests_tab.haml
index 9c193f901e2..4dba2473efc 100644
--- a/app/views/shared/milestones/_merge_requests_tab.haml
+++ b/app/views/shared/milestones/_merge_requests_tab.haml
@@ -3,10 +3,10 @@
.row.prepend-top-default
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Work in progress (open and unassigned)', issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Work in progress (open and unassigned)'), issuables: merge_requests.opened.unassigned, id: 'unassigned', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Waiting for merge (open and assigned)', issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Waiting for merge (open and assigned)'), issuables: merge_requests.opened.assigned, id: 'ongoing', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Rejected (closed)', issuables: merge_requests.closed, id: 'closed', show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Rejected (closed)'), issuables: merge_requests.closed, id: 'closed', show_counter: true)
.col-md-3
- = render 'shared/milestones/issuables', args.merge(title: 'Merged', issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
+ = render 'shared/milestones/issuables', args.merge(title: _('Merged'), issuables: merge_requests.merged, id: 'merged', primary: true, show_counter: true)
diff --git a/changelogs/unreleased/207934-snippet-embed-scrolling.yml b/changelogs/unreleased/207934-snippet-embed-scrolling.yml
new file mode 100644
index 00000000000..583b6067651
--- /dev/null
+++ b/changelogs/unreleased/207934-snippet-embed-scrolling.yml
@@ -0,0 +1,5 @@
+---
+title: Fix display of embedded snippets
+merge_request: 32411
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/214710-rename-configure-to-enable.yml b/changelogs/unreleased/214710-rename-configure-to-enable.yml
new file mode 100644
index 00000000000..b68bd130323
--- /dev/null
+++ b/changelogs/unreleased/214710-rename-configure-to-enable.yml
@@ -0,0 +1,5 @@
+---
+title: Changed terminology of security scanner status from configure to enable
+merge_request: 31503
+author:
+type: changed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml
new file mode 100644
index 00000000000..d85793902fa
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-milestones-_merge_.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/milestones/_merge_requests_tab.haml
+merge_request: 32158
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml b/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml
new file mode 100644
index 00000000000..58c089f2eb0
--- /dev/null
+++ b/changelogs/unreleased/dmishunov-fix-project-snippet-redirect.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed redirection to project snippets
+merge_request: 32530
+author:
+type: fixed
diff --git a/changelogs/unreleased/leaky-constant-fix-26.yml b/changelogs/unreleased/leaky-constant-fix-26.yml
new file mode 100644
index 00000000000..c567b5dc36a
--- /dev/null
+++ b/changelogs/unreleased/leaky-constant-fix-26.yml
@@ -0,0 +1,5 @@
+---
+title: Fix leaky constant issue in factory spec
+merge_request: 32099
+author: Rajendra Kadam
+type: fixed
diff --git a/changelogs/unreleased/sy-status-counts.yml b/changelogs/unreleased/sy-status-counts.yml
new file mode 100644
index 00000000000..a2a6c54df71
--- /dev/null
+++ b/changelogs/unreleased/sy-status-counts.yml
@@ -0,0 +1,5 @@
+---
+title: Add alert counts by status to GraphQL API
+merge_request: 31818
+author:
+type: changed
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index addb2dd4f65..267a1f0b1a5 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -44,6 +44,10 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics::Samplers::RubySampler.initialize_instance(Settings.monitoring.ruby_sampler_interval).start
+ if Gitlab::Utils.to_boolean(ENV['ENABLE_DATABASE_CONNECTION_POOL_METRICS'])
+ Gitlab::Metrics::Samplers::DatabaseSampler.initialize_instance(Gitlab::Metrics::Samplers::DatabaseSampler::SAMPLING_INTERVAL_SECONDS).start
+ end
+
if Gitlab.ee? && Gitlab::Runtime.sidekiq?
Gitlab::Metrics::Samplers::GlobalSearchSampler.instance(Settings.monitoring.global_search_sampler_interval).start
end
diff --git a/doc/administration/pages/index.md b/doc/administration/pages/index.md
index 15d7567ad5c..21d13be47bd 100644
--- a/doc/administration/pages/index.md
+++ b/doc/administration/pages/index.md
@@ -455,9 +455,36 @@ You can run the GitLab Pages daemon on a separate server in order to decrease th
To configure GitLab Pages on a separate server:
+DANGER: **Danger:**
+The following procedure includes steps to back up and edit the
+`gitlab-secrets.json` file. This file contains secrets that control
+database encryption. Proceed with caution.
+
+1. On the **GitLab server**, to enable Pages, add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_pages['enable'] = true
+ ```
+
+1. Optionally, to enable [access control](#access-control), add the following to `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ gitlab_pages['access_control'] = true
+ ```
+
+1. [Reconfigure the **GitLab server**](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the
+ changes to take effect. The `gitlab-secrets.json` file is now updated with the
+ new configuration.
+
+1. Create a backup of the secrets file on the **GitLab server**:
+
+ ```shell
+ cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
+ ```
+
1. Set up a new server. This will become the **Pages server**.
-1. Create an NFS share on the new server and configure this share to
+1. Create an [NFS share](../high_availability/nfs_host_client_setup.md) on the new server and configure this share to
allow access from your main **GitLab server**. For this example, we use the
default GitLab Pages folder `/var/opt/gitlab/gitlab-rails/shared/pages`
as the shared folder on the new server and we will mount it to `/mnt/pages`
@@ -481,6 +508,15 @@ To configure GitLab Pages on a separate server:
gitlab_rails['auto_migrate'] = false
```
+1. Create a backup of the secrets file on the **Pages server**:
+
+ ```shell
+ cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
+ ```
+
+1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the **GitLab server**
+ to the **Pages server**.
+
1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
1. On the **GitLab server**, make the following changes to `/etc/gitlab/gitlab.rb`:
@@ -500,61 +536,6 @@ configuring a load balancer to work at the IP level, and so on. If you wish to
set up GitLab Pages on multiple servers, perform the above procedure for each
Pages server.
-### Access control when running GitLab Pages on a separate server
-
-If you are [running GitLab Pages on a separate server](#running-gitlab-pages-on-a-separate-server),
-then you must use the following procedure to configure [access control](#access-control):
-
-1. On the **GitLab server**, add the following to `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['enable'] = true
- gitlab_pages['access_control'] = true
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the
- changes to take effect. The `gitlab-secrets.json` file is now updated with the
- new configuration.
-
- DANGER: **Danger:**
- The `gitlab-secrets.json` file contains secrets that control database encryption.
- Do not edit or replace this file on the **GitLab server** or you might
- experience permanent data loss. Make a backup copy of this file before proceeding,
- as explained in the following steps.
-
-1. Create a backup of the secrets file on the **GitLab server**:
-
- ```shell
- cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
- ```
-
-1. Create a backup of the secrets file on the **Pages server**:
-
- ```shell
- cp /etc/gitlab/gitlab-secrets.json /etc/gitlab/gitlab-secrets.json.bak
- ```
-
-1. Disable Pages on the **GitLab server** by setting the following in
- `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['enable'] = false
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-
-1. Copy the `/etc/gitlab/gitlab-secrets.json` file from the **GitLab server**
- to the **Pages server**.
-
-1. On your **Pages server**, add the following to `/etc/gitlab/gitlab.rb`:
-
- ```ruby
- gitlab_pages['gitlab_server'] = "https://<your-gitlab-server-URL>"
- gitlab_pages['access_control'] = true
- ```
-
-1. [Reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure) for the changes to take effect.
-
## Backup
GitLab Pages are part of the [regular backup](../../raketasks/backup_restore.md), so there is no separate backup to configure.
diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql
index 3d2d2b47b7e..91f1413943c 100644
--- a/doc/api/graphql/reference/gitlab_schema.graphql
+++ b/doc/api/graphql/reference/gitlab_schema.graphql
@@ -349,6 +349,41 @@ enum AlertManagementAlertSort {
}
"""
+Represents total number of alerts for the represented categories
+"""
+type AlertManagementAlertStatusCountsType {
+ """
+ Number of alerts with status ACKNOWLEDGED for the project
+ """
+ acknowledged: Int
+
+ """
+ Total number of alerts for the project
+ """
+ all: Int
+
+ """
+ Number of alerts with status IGNORED for the project
+ """
+ ignored: Int
+
+ """
+ Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project
+ """
+ open: Int
+
+ """
+ Number of alerts with status RESOLVED for the project
+ """
+ resolved: Int
+
+ """
+ Number of alerts with status TRIGGERED for the project
+ """
+ triggered: Int
+}
+
+"""
Alert severity values
"""
enum AlertManagementSeverity {
@@ -7385,6 +7420,11 @@ type Project {
): AlertManagementAlert
"""
+ Counts of alerts by status for the project
+ """
+ alertManagementAlertStatusCounts: AlertManagementAlertStatusCountsType
+
+ """
Alert Management alerts of the project
"""
alertManagementAlerts(
diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json
index 3d7c701dbea..40bfa08cff3 100644
--- a/doc/api/graphql/reference/gitlab_schema.json
+++ b/doc/api/graphql/reference/gitlab_schema.json
@@ -856,6 +856,103 @@
"possibleTypes": null
},
{
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertStatusCountsType",
+ "description": "Represents total number of alerts for the represented categories",
+ "fields": [
+ {
+ "name": "acknowledged",
+ "description": "Number of alerts with status ACKNOWLEDGED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "all",
+ "description": "Total number of alerts for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "ignored",
+ "description": "Number of alerts with status IGNORED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "open",
+ "description": "Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "resolved",
+ "description": "Number of alerts with status RESOLVED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
+ "name": "triggered",
+ "description": "Number of alerts with status TRIGGERED for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "SCALAR",
+ "name": "Int",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ }
+ ],
+ "inputFields": null,
+ "interfaces": [
+
+ ],
+ "enumValues": null,
+ "possibleTypes": null
+ },
+ {
"kind": "ENUM",
"name": "AlertManagementSeverity",
"description": "Alert severity values",
@@ -22091,6 +22188,20 @@
"deprecationReason": null
},
{
+ "name": "alertManagementAlertStatusCounts",
+ "description": "Counts of alerts by status for the project",
+ "args": [
+
+ ],
+ "type": {
+ "kind": "OBJECT",
+ "name": "AlertManagementAlertStatusCountsType",
+ "ofType": null
+ },
+ "isDeprecated": false,
+ "deprecationReason": null
+ },
+ {
"name": "alertManagementAlerts",
"description": "Alert Management alerts of the project",
"args": [
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index e2d18ee3f14..4164c26e751 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -68,6 +68,19 @@ Describes an alert from the project's Alert Management
| `title` | String | Title of the alert |
| `updatedAt` | Time | Timestamp the alert was last updated |
+## AlertManagementAlertStatusCountsType
+
+Represents total number of alerts for the represented categories
+
+| Name | Type | Description |
+| --- | ---- | ---------- |
+| `acknowledged` | Int | Number of alerts with status ACKNOWLEDGED for the project |
+| `all` | Int | Total number of alerts for the project |
+| `ignored` | Int | Number of alerts with status IGNORED for the project |
+| `open` | Int | Number of alerts with status TRIGGERED or ACKNOWLEDGED for the project |
+| `resolved` | Int | Number of alerts with status RESOLVED for the project |
+| `triggered` | Int | Number of alerts with status TRIGGERED for the project |
+
## AwardEmoji
An emoji awarded by a user.
@@ -1115,6 +1128,7 @@ Information about pagination in a connection.
| Name | Type | Description |
| --- | ---- | ---------- |
| `alertManagementAlert` | AlertManagementAlert | A single Alert Management alert of the project |
+| `alertManagementAlertStatusCounts` | AlertManagementAlertStatusCountsType | Counts of alerts by status for the project |
| `archived` | Boolean | Indicates the archived status of the project |
| `autocloseReferencedIssues` | Boolean | Indicates if issues referenced by merge requests and commits within the default branch are closed automatically |
| `avatarUrl` | String | URL to avatar image file of the project |
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index a4e6ef27687..a5d0eecdc7b 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -150,6 +150,24 @@ execute.
More [deduplication strategies have been suggested](https://gitlab.com/gitlab-com/gl-infra/scalability/issues/195). If you are implementing a worker that
could benefit from a different strategy, please comment in the issue.
+If the automatic deduplication were to cause issues in certain
+queues. This can be temporarily disabled by enabling a feature flag
+named `disable_<queue name>_deduplication`. For example to disable
+deduplication for the `AuthorizedProjectsWorker`, we would enable the
+feature flag `disable_authorized_projects_deduplication`.
+
+From chatops:
+
+```shell
+/chatops run feature set disable_authorized_projects_deduplication true
+```
+
+From the rails console:
+
+```ruby
+Feature.enable!(:disable_authorized_projects_deduplication)
+```
+
## Job urgency
Jobs can have an `urgency` attribute set, which can be `:high`,
diff --git a/doc/user/application_security/configuration/index.md b/doc/user/application_security/configuration/index.md
index 131247910ab..dfddbde379d 100644
--- a/doc/user/application_security/configuration/index.md
+++ b/doc/user/application_security/configuration/index.md
@@ -11,7 +11,7 @@ type: reference, howto
The security configuration page displays the configuration state of each of the security
features and can be accessed through a project's sidebar nav.
-![Screenshot of security configuration page](../img/security_configuration_page_v12_9.png)
+![Screenshot of security configuration page](../img/security_configuration_page_v13_1.png)
The page uses the project's latest default branch [CI pipeline](../../../ci/pipelines/index.md) to determine the configuration
state of each feature. If a job with the expected security report artifact exists in the pipeline,
diff --git a/doc/user/application_security/img/security_configuration_page_v12_9.png b/doc/user/application_security/img/security_configuration_page_v12_9.png
deleted file mode 100644
index a81d82e03c3..00000000000
--- a/doc/user/application_security/img/security_configuration_page_v12_9.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/application_security/img/security_configuration_page_v13_1.png b/doc/user/application_security/img/security_configuration_page_v13_1.png
new file mode 100644
index 00000000000..0147084f705
--- /dev/null
+++ b/doc/user/application_security/img/security_configuration_page_v13_1.png
Binary files differ
diff --git a/doc/user/compliance/license_compliance/index.md b/doc/user/compliance/license_compliance/index.md
index 97c2b900b61..010eea8f45e 100644
--- a/doc/user/compliance/license_compliance/index.md
+++ b/doc/user/compliance/license_compliance/index.md
@@ -136,7 +136,7 @@ License Compliance can be configured using environment variables.
| Environment variable | Required | Description |
|-----------------------------|----------|-------------|
| `SECURE_ANALYZERS_PREFIX` | no | Set the Docker registry base address to download the analyzer from. |
-| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, and NPM projects). |
+| `ADDITIONAL_CA_CERT_BUNDLE` | no | Bundle of trusted CA certificates (currently supported in Pip, Pipenv, Maven, Gradle, Yarn, and NPM projects). |
| `GRADLE_CLI_OPTS` | no | Additional arguments for the gradle executable. If not supplied, defaults to `--exclude-task=test`. |
| `LICENSE_FINDER_CLI_OPTS` | no | Additional arguments for the `license_finder` executable. For example, if your project has both Golang and Ruby code stored in different directories and you want to only scan the Ruby code, you can update your `.gitlab-ci-yml` template to specify which project directories to scan, like `LICENSE_FINDER_CLI_OPTS: '--debug --aggregate-paths=. ruby'`. |
| `LM_JAVA_VERSION` | no | Version of Java. If set to `11`, Maven and Gradle use Java 11 instead of Java 8. |
@@ -326,6 +326,28 @@ For example:
strict-ssl = false
```
+### Configuring Yarn projects
+
+You can configure Yarn projects by using a [`.yarnrc.yml`](https://yarnpkg.com/configuration/yarnrc)
+file.
+
+#### Using private Yarn registries
+
+If you have a private Yarn registry you can use the
+[`npmRegistryServer`](https://yarnpkg.com/configuration/yarnrc#npmRegistryServer)
+setting to specify its location.
+
+For example:
+
+```text
+npmRegistryServer: "https://npm.example.com"
+```
+
+#### Custom root certificates for Yarn
+
+You can supply a custom root certificate to complete TLS verification by using the
+`ADDITIONAL_CA_CERT_BUNDLE` [environment variable](#available-variables).
+
### Migration from `license_management` to `license_scanning`
In GitLab 12.8 a new name for `license_management` job was introduced. This change was made to improve clarity around the purpose of the scan, which is to scan and collect the types of licenses present in a projects dependencies.
@@ -429,7 +451,7 @@ The License Compliance job should now use local copies of the License Compliance
your code and generate security reports, without requiring internet access.
Additional configuration may be needed for connecting to [private Maven repositories](#using-private-maven-repos),
-[private NPM registries](#using-private-npm-registries), and [private Python repositories](#using-private-python-repos).
+[private NPM registries](#using-private-npm-registries), [private Yarn registries](#using-private-yarn-registries), and [private Python repositories](#using-private-python-repos).
Exact name matches are required for [project policies](#project-policies-for-license-compliance)
when running in an offline environment ([see related issue](https://gitlab.com/gitlab-org/gitlab/-/issues/212388)).
diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md
index 18b94328f9d..6bee552d433 100644
--- a/doc/user/group/roadmap/index.md
+++ b/doc/user/group/roadmap/index.md
@@ -7,7 +7,8 @@ type: reference
> - Introduced in [GitLab Ultimate](https://about.gitlab.com/pricing/) 10.5.
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/198062), Roadmaps were moved to the Premium tier.
> - In [GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/5164) and later, the epic bars show epics' title, progress, and completed weight percentage.
-> - In [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/6802), and later, milestones appear in Roadmaps.
+> - Milestones appear in Roadmaps in [GitLab 12.10](https://gitlab.com/gitlab-org/gitlab/-/issues/6802), and later.
+> - Feature flag removed in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/29641).
Epics and milestones within a group containing **Start date** and/or **Due date**
can be visualized in a form of a timeline (that is, a Gantt chart). The Roadmap page
diff --git a/lib/gitlab/alert_management/alert_status_counts.rb b/lib/gitlab/alert_management/alert_status_counts.rb
new file mode 100644
index 00000000000..382026236e0
--- /dev/null
+++ b/lib/gitlab/alert_management/alert_status_counts.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module AlertManagement
+ # Represents counts of each status or category of statuses
+ class AlertStatusCounts
+ include Gitlab::Utils::StrongMemoize
+
+ STATUSES = ::AlertManagement::Alert::STATUSES
+
+ attr_reader :project
+
+ def self.declarative_policy_class
+ 'AlertManagement::AlertPolicy'
+ end
+
+ def initialize(current_user, project, params)
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ # Define method for each status
+ STATUSES.each_key do |status|
+ define_method(status) { counts[status] }
+ end
+
+ def open
+ counts[:triggered] + counts[:acknowledged]
+ end
+
+ def all
+ counts.values.sum # rubocop:disable CodeReuse/ActiveRecord
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def counts
+ strong_memoize(:counts) do
+ Hash.new(0).merge(counts_by_status)
+ end
+ end
+
+ def counts_by_status
+ ::AlertManagement::AlertsFinder
+ .counts_by_status(current_user, project, params)
+ .transform_keys { |status_id| STATUSES.key(status_id) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/network_policy.rb b/lib/gitlab/kubernetes/network_policy.rb
index 8a93cd100d7..ea25d81cbd2 100644
--- a/lib/gitlab/kubernetes/network_policy.rb
+++ b/lib/gitlab/kubernetes/network_policy.rb
@@ -84,7 +84,7 @@ module Gitlab
end
def manifest
- YAML.dump(metadata: metadata, spec: spec)
+ YAML.dump({ metadata: metadata, spec: spec }.deep_stringify_keys)
end
end
end
diff --git a/lib/gitlab/metrics/samplers/database_sampler.rb b/lib/gitlab/metrics/samplers/database_sampler.rb
new file mode 100644
index 00000000000..9ee4b0960c5
--- /dev/null
+++ b/lib/gitlab/metrics/samplers/database_sampler.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Samplers
+ class DatabaseSampler < BaseSampler
+ SAMPLING_INTERVAL_SECONDS = 5
+
+ METRIC_PREFIX = 'gitlab_database_connection_pool_'
+
+ METRIC_DESCRIPTIONS = {
+ size: 'Total connection pool capacity',
+ connections: 'Current connections in the pool',
+ busy: 'Connections in use where the owner is still alive',
+ dead: 'Connections in use where the owner is not alive',
+ idle: 'Connections not in use',
+ waiting: 'Threads currently waiting on this queue'
+ }.freeze
+
+ def metrics
+ @metrics ||= init_metrics
+ end
+
+ def sample
+ host_stats.each do |host_stat|
+ METRIC_DESCRIPTIONS.each_key do |metric|
+ metrics[metric].set(host_stat[:labels], host_stat[:stats][metric])
+ end
+ end
+ end
+
+ private
+
+ def init_metrics
+ METRIC_DESCRIPTIONS.map do |name, description|
+ [name, ::Gitlab::Metrics.gauge(:"#{METRIC_PREFIX}#{name}", description)]
+ end.to_h
+ end
+
+ def host_stats
+ return [] unless ActiveRecord::Base.connected?
+
+ [{ labels: labels_for_class(ActiveRecord::Base), stats: ActiveRecord::Base.connection_pool.stat }]
+ end
+
+ def labels_for_class(klass)
+ {
+ host: klass.connection_config[:host],
+ port: klass.connection_config[:port],
+ class: klass.to_s
+ }
+ end
+ end
+ end
+ end
+end
+
+Gitlab::Metrics::Samplers::DatabaseSampler.prepend_if_ee('EE::Gitlab::Metrics::Samplers::DatabaseSampler')
diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb
index 79bbb99752e..fa742d07af2 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?
+ idempotent? && duplicate? && ::Feature.disabled?("disable_#{queue_name}_deduplication")
end
private
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d947659ff5f..14afe67bbfd 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14049,6 +14049,15 @@ msgstr ""
msgid "Network"
msgstr ""
+msgid "NetworkPolicies|Environment does not have deployment platform"
+msgstr ""
+
+msgid "NetworkPolicies|Invalid or empty policy"
+msgstr ""
+
+msgid "NetworkPolicies|Kubernetes error: %{error}"
+msgstr ""
+
msgid "NetworkPolicies|Something went wrong, unable to fetch policies"
msgstr ""
@@ -17640,6 +17649,9 @@ msgstr ""
msgid "Registration|Your profile"
msgstr ""
+msgid "Rejected (closed)"
+msgstr ""
+
msgid "Related Deployed Jobs"
msgstr ""
@@ -18855,7 +18867,7 @@ msgstr ""
msgid "Security report is out of date. Run %{newPipelineLinkStart}a new pipeline%{newPipelineLinkEnd} for the target branch (%{targetBranchName})"
msgstr ""
-msgid "SecurityConfiguration|Configured"
+msgid "SecurityConfiguration|Enabled"
msgstr ""
msgid "SecurityConfiguration|Feature"
@@ -18864,7 +18876,7 @@ msgstr ""
msgid "SecurityConfiguration|Feature documentation for %{featureName}"
msgstr ""
-msgid "SecurityConfiguration|Not yet configured"
+msgid "SecurityConfiguration|Not yet enabled"
msgstr ""
msgid "SecurityConfiguration|Secure features"
@@ -21285,9 +21297,6 @@ msgstr ""
msgid "The commit does not exist"
msgstr ""
-msgid "The configuration status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've configured a scan for the default branch, any subsequent feature branch you create will include the scan."
-msgstr ""
-
msgid "The connection will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
msgstr ""
@@ -21548,6 +21557,9 @@ msgstr ""
msgid "The staging stage shows the time between merging the MR and deploying code to the production environment. The data will be automatically added once you deploy to production for the first time."
msgstr ""
+msgid "The status of the table below only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
+msgstr ""
+
msgid "The testing stage shows the time GitLab CI takes to run every pipeline for the related merge request. The data will automatically be added after your first pipeline finishes running."
msgstr ""
@@ -24253,6 +24265,9 @@ msgstr ""
msgid "Wait for the file to load to copy its contents"
msgstr ""
+msgid "Waiting for merge (open and assigned)"
+msgstr ""
+
msgid "Waiting for performance data"
msgstr ""
@@ -24585,6 +24600,9 @@ msgstr ""
msgid "Won't fix / Accept risk"
msgstr ""
+msgid "Work in progress (open and unassigned)"
+msgstr ""
+
msgid "Work in progress Limit"
msgstr ""
@@ -26058,7 +26076,7 @@ msgstr ""
msgid "mrWidget|Deployment statistics are not available currently"
msgstr ""
-msgid "mrWidget|Detect issues before deployment with a CI pipeline"
+msgid "mrWidget|Detect issues before deployment with a CI pipeline that continuously tests your code. We created a quick guide that will show you how to create one. Make your code more secure and more robust in just a minute."
msgstr ""
msgid "mrWidget|Did not close"
@@ -26238,9 +26256,6 @@ msgstr ""
msgid "mrWidget|Your password"
msgstr ""
-msgid "mrWidget|a quick guide that'll show you how to create"
-msgstr ""
-
msgid "mrWidget|branch does not exist."
msgstr ""
@@ -26250,15 +26265,6 @@ msgstr ""
msgid "mrWidget|into"
msgstr ""
-msgid "mrWidget|one. Make your code more secure and more"
-msgstr ""
-
-msgid "mrWidget|robust in just a minute."
-msgstr ""
-
-msgid "mrWidget|that continuously tests your code. We created"
-msgstr ""
-
msgid "mrWidget|to be added to the merge train when the pipeline succeeds"
msgstr ""
diff --git a/spec/finders/alert_management/alerts_finder_spec.rb b/spec/finders/alert_management/alerts_finder_spec.rb
index 99db4ecbf03..c6d2d0ad4ef 100644
--- a/spec/finders/alert_management/alerts_finder_spec.rb
+++ b/spec/finders/alert_management/alerts_finder_spec.rb
@@ -10,214 +10,216 @@ describe AlertManagement::AlertsFinder, '#execute' do
let_it_be(:alert_3) { create(:alert_management_alert, :all_fields) }
let(:params) { {} }
- subject { described_class.new(current_user, project, params).execute }
+ describe '#execute' do
+ subject { described_class.new(current_user, project, params).execute }
- context 'user is not a developer or above' do
- it { is_expected.to be_empty }
- end
-
- context 'user is developer' do
- before do
- project.add_developer(current_user)
+ context 'user is not a developer or above' do
+ it { is_expected.to be_empty }
end
- context 'empty params' do
- it { is_expected.to contain_exactly(alert_1, alert_2) }
- end
-
- context 'iid given' do
- let(:params) { { iid: alert_1.iid } }
-
- it { is_expected.to match_array(alert_1) }
-
- context 'unknown iid' do
- let(:params) { { iid: 'unknown' } }
+ context 'user is developer' do
+ before do
+ project.add_developer(current_user)
+ end
- it { is_expected.to be_empty }
+ context 'empty params' do
+ it { is_expected.to contain_exactly(alert_1, alert_2) }
end
- end
- context 'status given' do
- let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+ context 'iid given' do
+ let(:params) { { iid: alert_1.iid } }
- it { is_expected.to match_array(alert_1) }
+ it { is_expected.to match_array(alert_1) }
- context 'with an array of statuses' do
- let(:alert_3) { create(:alert_management_alert) }
- let(:params) { { status: [AlertManagement::Alert::STATUSES[:resolved]] } }
+ context 'unknown iid' do
+ let(:params) { { iid: 'unknown' } }
- it { is_expected.to match_array(alert_1) }
+ it { is_expected.to be_empty }
+ end
end
- context 'with no alerts of status' do
- let(:params) { { status: AlertManagement::Alert::STATUSES[:acknowledged] } }
+ context 'status given' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
- it { is_expected.to be_empty }
- end
+ it { is_expected.to match_array(alert_1) }
- context 'with an empty status array' do
- let(:params) { { status: [] } }
+ context 'with an array of statuses' do
+ let(:alert_3) { create(:alert_management_alert) }
+ let(:params) { { status: [AlertManagement::Alert::STATUSES[:resolved]] } }
- it { is_expected.to match_array([alert_1, alert_2]) }
- end
+ it { is_expected.to match_array(alert_1) }
+ end
- context 'with an nil status' do
- let(:params) { { status: nil } }
+ context 'with no alerts of status' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:acknowledged] } }
- it { is_expected.to match_array([alert_1, alert_2]) }
- end
- end
+ it { is_expected.to be_empty }
+ end
- describe 'sorting' do
- context 'when sorting by created' do
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'created_asc' } }
+ context 'with an empty status array' do
+ let(:params) { { status: [] } }
- it { is_expected.to eq [alert_1, alert_2] }
+ it { is_expected.to match_array([alert_1, alert_2]) }
end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'created_desc' } }
+ context 'with an nil status' do
+ let(:params) { { status: nil } }
- it { is_expected.to eq [alert_2, alert_1] }
+ it { is_expected.to match_array([alert_1, alert_2]) }
end
end
- context 'when sorting by updated' do
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'updated_asc' } }
+ describe 'sorting' do
+ context 'when sorting by created' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'created_asc' } }
- it { is_expected.to eq [alert_1, alert_2] }
- end
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'updated_desc' } }
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'created_desc' } }
- it { is_expected.to eq [alert_2, alert_1] }
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
end
- end
- context 'when sorting by start time' do
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'start_time_asc' } }
+ context 'when sorting by updated' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'updated_asc' } }
- it { is_expected.to eq [alert_1, alert_2] }
- end
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'start_time_desc' } }
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'updated_desc' } }
- it { is_expected.to eq [alert_2, alert_1] }
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
end
- end
- context 'when sorting by end time' do
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'end_time_asc' } }
+ context 'when sorting by start time' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'start_time_asc' } }
- it { is_expected.to eq [alert_1, alert_2] }
- end
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'end_time_desc' } }
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'start_time_desc' } }
- it { is_expected.to eq [alert_2, alert_1] }
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
end
- end
- context 'when sorting by events count' do
- let_it_be(:alert_count_6) { create(:alert_management_alert, project: project, events: 6) }
- let_it_be(:alert_count_3) { create(:alert_management_alert, project: project, events: 3) }
+ context 'when sorting by end time' do
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'end_time_asc' } }
+
+ it { is_expected.to eq [alert_1, alert_2] }
+ end
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'events_count_asc' } }
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'end_time_desc' } }
- it { is_expected.to eq [alert_2, alert_1, alert_count_3, alert_count_6] }
+ it { is_expected.to eq [alert_2, alert_1] }
+ end
end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'events_count_desc' } }
+ context 'when sorting by events count' do
+ let_it_be(:alert_count_6) { create(:alert_management_alert, project: project, events: 6) }
+ let_it_be(:alert_count_3) { create(:alert_management_alert, project: project, events: 3) }
- it { is_expected.to eq [alert_count_6, alert_count_3, alert_1, alert_2] }
- end
- end
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'events_count_asc' } }
- context 'when sorting by severity' do
- let_it_be(:alert_critical) { create(:alert_management_alert, project: project, severity: :critical) }
- let_it_be(:alert_high) { create(:alert_management_alert, project: project, severity: :high) }
- let_it_be(:alert_medium) { create(:alert_management_alert, project: project, severity: :medium) }
- let_it_be(:alert_low) { create(:alert_management_alert, project: project, severity: :low) }
- let_it_be(:alert_info) { create(:alert_management_alert, project: project, severity: :info) }
- let_it_be(:alert_unknown) { create(:alert_management_alert, project: project, severity: :unknown) }
-
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'severity_asc' } }
-
- it do
- is_expected.to eq [
- alert_2,
- alert_critical,
- alert_1,
- alert_high,
- alert_medium,
- alert_low,
- alert_info,
- alert_unknown
- ]
+ it { is_expected.to eq [alert_2, alert_1, alert_count_3, alert_count_6] }
end
- end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'severity_desc' } }
-
- it do
- is_expected.to eq [
- alert_unknown,
- alert_info,
- alert_low,
- alert_medium,
- alert_1,
- alert_high,
- alert_critical,
- alert_2
- ]
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'events_count_desc' } }
+
+ it { is_expected.to eq [alert_count_6, alert_count_3, alert_1, alert_2] }
end
end
- end
- context 'when sorting by status' do
- let_it_be(:alert_triggered) { create(:alert_management_alert, project: project) }
- let_it_be(:alert_acknowledged) { create(:alert_management_alert, :acknowledged, project: project) }
- let_it_be(:alert_resolved) { create(:alert_management_alert, :resolved, project: project) }
- let_it_be(:alert_ignored) { create(:alert_management_alert, :ignored, project: project) }
-
- context 'sorts alerts ascending' do
- let(:params) { { sort: 'status_asc' } }
-
- it do
- is_expected.to eq [
- alert_triggered,
- alert_acknowledged,
- alert_1,
- alert_resolved,
- alert_2,
- alert_ignored
- ]
+ context 'when sorting by severity' do
+ let_it_be(:alert_critical) { create(:alert_management_alert, project: project, severity: :critical) }
+ let_it_be(:alert_high) { create(:alert_management_alert, project: project, severity: :high) }
+ let_it_be(:alert_medium) { create(:alert_management_alert, project: project, severity: :medium) }
+ let_it_be(:alert_low) { create(:alert_management_alert, project: project, severity: :low) }
+ let_it_be(:alert_info) { create(:alert_management_alert, project: project, severity: :info) }
+ let_it_be(:alert_unknown) { create(:alert_management_alert, project: project, severity: :unknown) }
+
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'severity_asc' } }
+
+ it do
+ is_expected.to eq [
+ alert_2,
+ alert_critical,
+ alert_1,
+ alert_high,
+ alert_medium,
+ alert_low,
+ alert_info,
+ alert_unknown
+ ]
+ end
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'severity_desc' } }
+
+ it do
+ is_expected.to eq [
+ alert_unknown,
+ alert_info,
+ alert_low,
+ alert_medium,
+ alert_1,
+ alert_high,
+ alert_critical,
+ alert_2
+ ]
+ end
end
end
- context 'sorts alerts descending' do
- let(:params) { { sort: 'status_desc' } }
-
- it do
- is_expected.to eq [
- alert_2,
- alert_ignored,
- alert_1,
- alert_resolved,
- alert_acknowledged,
- alert_triggered
- ]
+ context 'when sorting by status' do
+ let_it_be(:alert_triggered) { create(:alert_management_alert, project: project) }
+ let_it_be(:alert_acknowledged) { create(:alert_management_alert, :acknowledged, project: project) }
+ let_it_be(:alert_resolved) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_ignored) { create(:alert_management_alert, :ignored, project: project) }
+
+ context 'sorts alerts ascending' do
+ let(:params) { { sort: 'status_asc' } }
+
+ it do
+ is_expected.to eq [
+ alert_triggered,
+ alert_acknowledged,
+ alert_1,
+ alert_resolved,
+ alert_2,
+ alert_ignored
+ ]
+ end
+ end
+
+ context 'sorts alerts descending' do
+ let(:params) { { sort: 'status_desc' } }
+
+ it do
+ is_expected.to eq [
+ alert_2,
+ alert_ignored,
+ alert_1,
+ alert_resolved,
+ alert_acknowledged,
+ alert_triggered
+ ]
+ end
end
end
end
@@ -277,4 +279,20 @@ describe AlertManagement::AlertsFinder, '#execute' do
end
end
end
+
+ describe '.counts_by_status' do
+ subject { described_class.counts_by_status(current_user, project, params) }
+
+ before do
+ project.add_developer(current_user)
+ end
+
+ it { is_expected.to match({ 2 => 1, 3 => 1 }) } # one resolved and one ignored
+
+ context 'when filtering params are included' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it { is_expected.to match({ 2 => 1 }) } # one resolved
+ end
+ end
end
diff --git a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 6a239e307e9..6a239e307e9 100644
--- a/spec/javascripts/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index 9ad72e0b043..fe17c03389c 100644
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -1,9 +1,14 @@
import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
describe('stop_jobs_modal.vue', () => {
const props = {
url: `${gl.TEST_HOST}/stop_jobs_modal.vue/stopAll`,
@@ -22,8 +27,7 @@ describe('stop_jobs_modal.vue', () => {
describe('onSubmit', () => {
it('stops jobs and redirects to overview page', done => {
const responseURL = `${gl.TEST_HOST}/stop_jobs_modal.vue/jobs`;
- const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(props.url);
return Promise.resolve({
request: {
@@ -34,7 +38,7 @@ describe('stop_jobs_modal.vue', () => {
vm.onSubmit()
.then(() => {
- expect(redirectSpy).toHaveBeenCalledWith(responseURL);
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
})
.then(done)
.catch(done.fail);
@@ -42,8 +46,7 @@ describe('stop_jobs_modal.vue', () => {
it('displays error if stopping jobs failed', done => {
const dummyError = new Error('stopping jobs failed');
- const redirectSpy = spyOnDependency(stopJobsModal, 'redirectTo');
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(props.url);
return Promise.reject(dummyError);
});
@@ -52,7 +55,7 @@ describe('stop_jobs_modal.vue', () => {
.then(done.fail)
.catch(error => {
expect(error).toBe(dummyError);
- expect(redirectSpy).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled();
})
.then(done)
.catch(done.fail);
diff --git a/spec/javascripts/pages/admin/users/new/index_spec.js b/spec/frontend/pages/admin/users/new/index_spec.js
index 3896323eef7..3896323eef7 100644
--- a/spec/javascripts/pages/admin/users/new/index_spec.js
+++ b/spec/frontend/pages/admin/users/new/index_spec.js
diff --git a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
index 5bad13c1ef2..9d5beca70b5 100644
--- a/spec/javascripts/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -43,7 +43,7 @@ describe('Promote label modal', () => {
vm = mountComponent(Component, {
...labelMockData,
});
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
afterEach(() => {
@@ -52,7 +52,7 @@ describe('Promote label modal', () => {
it('redirects when a label is promoted', done => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteLabelModal.requestStarted',
@@ -79,7 +79,7 @@ describe('Promote label modal', () => {
it('displays an error if promoting a label failed', done => {
const dummyError = new Error('promoting label failed');
dummyError.response = { status: 500 };
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(labelMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteLabelModal.requestStarted',
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 9075c8aa97a..ff5dc6d8988 100644
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -1,10 +1,15 @@
import Vue from 'vue';
-
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
+import mountComponent from 'helpers/vue_mount_component_helper';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
describe('delete_milestone_modal.vue', () => {
const Component = Vue.extend(deleteMilestoneModal);
const props = {
@@ -23,29 +28,28 @@ describe('delete_milestone_modal.vue', () => {
describe('onSubmit', () => {
beforeEach(() => {
vm = mountComponent(Component, props);
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('deletes milestone and redirects to overview page', done => {
const responseURL = `${gl.TEST_HOST}/delete_milestone_modal.vue/milestoneOverview`;
- spyOn(axios, 'delete').and.callFake(url => {
+ jest.spyOn(axios, 'delete').mockImplementation(url => {
expect(url).toBe(props.milestoneUrl);
expect(eventHub.$emit).toHaveBeenCalledWith(
'deleteMilestoneModal.requestStarted',
props.milestoneUrl,
);
- eventHub.$emit.calls.reset();
+ eventHub.$emit.mockReset();
return Promise.resolve({
request: {
responseURL,
},
});
});
- const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.then(() => {
- expect(redirectSpy).toHaveBeenCalledWith(responseURL);
+ expect(redirectTo).toHaveBeenCalledWith(responseURL);
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
milestoneUrl: props.milestoneUrl,
successful: true,
@@ -58,21 +62,20 @@ describe('delete_milestone_modal.vue', () => {
it('displays error if deleting milestone failed', done => {
const dummyError = new Error('deleting milestone failed');
dummyError.response = { status: 418 };
- spyOn(axios, 'delete').and.callFake(url => {
+ jest.spyOn(axios, 'delete').mockImplementation(url => {
expect(url).toBe(props.milestoneUrl);
expect(eventHub.$emit).toHaveBeenCalledWith(
'deleteMilestoneModal.requestStarted',
props.milestoneUrl,
);
- eventHub.$emit.calls.reset();
+ eventHub.$emit.mockReset();
return Promise.reject(dummyError);
});
- const redirectSpy = spyOnDependency(deleteMilestoneModal, 'redirectTo');
vm.onSubmit()
.catch(error => {
expect(error).toBe(dummyError);
- expect(redirectSpy).not.toHaveBeenCalled();
+ expect(redirectTo).not.toHaveBeenCalled();
expect(eventHub.$emit).toHaveBeenCalledWith('deleteMilestoneModal.requestFinished', {
milestoneUrl: props.milestoneUrl,
successful: false,
diff --git a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
index 78c0070187c..ff896354d96 100644
--- a/spec/javascripts/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import mountComponent from 'helpers/vue_mount_component_helper';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
@@ -38,7 +38,7 @@ describe('Promote milestone modal', () => {
vm = mountComponent(Component, {
...milestoneMockData,
});
- spyOn(eventHub, '$emit');
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
afterEach(() => {
@@ -47,7 +47,7 @@ describe('Promote milestone modal', () => {
it('redirects when a milestone is promoted', done => {
const responseURL = `${gl.TEST_HOST}/dummy/endpoint`;
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
@@ -74,7 +74,7 @@ describe('Promote milestone modal', () => {
it('displays an error if promoting a milestone failed', done => {
const dummyError = new Error('promoting milestone failed');
dummyError.response = { status: 500 };
- spyOn(axios, 'post').and.callFake(url => {
+ jest.spyOn(axios, 'post').mockImplementation(url => {
expect(url).toBe(milestoneMockData.url);
expect(eventHub.$emit).toHaveBeenCalledWith(
'promoteMilestoneModal.requestStarted',
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
index ea809e1f170..5a61f9fca69 100644
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/pipeline_schedule_callout_spec.js
@@ -1,12 +1,20 @@
import Vue from 'vue';
import Cookies from 'js-cookie';
import PipelineSchedulesCallout from '~/pages/projects/pipeline_schedules/shared/components/pipeline_schedules_callout.vue';
+import '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg';
+
+jest.mock(
+ '~/pages/projects/pipeline_schedules/shared/icons/intro_illustration.svg',
+ () => '<svg></svg>',
+);
const PipelineSchedulesCalloutComponent = Vue.extend(PipelineSchedulesCallout);
const cookieKey = 'pipeline_schedules_callout_dismissed';
const docsUrl = 'help/ci/scheduled_pipelines';
-describe('Pipeline Schedule Callout', function() {
+describe('Pipeline Schedule Callout', () => {
+ let calloutComponent;
+
beforeEach(() => {
setFixtures(`
<div id='pipeline-schedules-callout' data-docs-url=${docsUrl}></div>
@@ -15,90 +23,90 @@ describe('Pipeline Schedule Callout', function() {
describe('independent of cookies', () => {
beforeEach(() => {
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
});
it('the component can be initialized', () => {
- expect(this.calloutComponent).toBeDefined();
+ expect(calloutComponent).toBeDefined();
});
it('correctly sets illustrationSvg', () => {
- expect(this.calloutComponent.illustrationSvg).toContain('<svg');
+ expect(calloutComponent.illustrationSvg).toContain('<svg');
});
it('correctly sets docsUrl', () => {
- expect(this.calloutComponent.docsUrl).toContain(docsUrl);
+ expect(calloutComponent.docsUrl).toContain(docsUrl);
});
});
describe(`when ${cookieKey} cookie is set`, () => {
beforeEach(() => {
Cookies.set(cookieKey, true);
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
});
it('correctly sets calloutDismissed to true', () => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
+ expect(calloutComponent.calloutDismissed).toBe(true);
});
it('does not render the callout', () => {
- expect(this.calloutComponent.$el.childNodes.length).toBe(0);
+ expect(calloutComponent.$el.childNodes.length).toBe(0);
});
});
describe('when cookie is not set', () => {
beforeEach(() => {
Cookies.remove(cookieKey);
- this.calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
+ calloutComponent = new PipelineSchedulesCalloutComponent().$mount();
});
it('correctly sets calloutDismissed to false', () => {
- expect(this.calloutComponent.calloutDismissed).toBe(false);
+ expect(calloutComponent.calloutDismissed).toBe(false);
});
it('renders the callout container', () => {
- expect(this.calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
+ expect(calloutComponent.$el.querySelector('.bordered-box')).not.toBeNull();
});
it('renders the callout svg', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('<svg');
+ expect(calloutComponent.$el.outerHTML).toContain('<svg');
});
it('renders the callout title', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines');
+ expect(calloutComponent.$el.outerHTML).toContain('Scheduling Pipelines');
});
it('renders the callout text', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
+ expect(calloutComponent.$el.outerHTML).toContain('runs pipelines in the future');
});
it('renders the documentation url', () => {
- expect(this.calloutComponent.$el.outerHTML).toContain(docsUrl);
+ expect(calloutComponent.$el.outerHTML).toContain(docsUrl);
});
it('updates calloutDismissed when close button is clicked', done => {
- this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+ calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
Vue.nextTick(() => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
+ expect(calloutComponent.calloutDismissed).toBe(true);
done();
});
});
it('#dismissCallout updates calloutDismissed', done => {
- this.calloutComponent.dismissCallout();
+ calloutComponent.dismissCallout();
Vue.nextTick(() => {
- expect(this.calloutComponent.calloutDismissed).toBe(true);
+ expect(calloutComponent.calloutDismissed).toBe(true);
done();
});
});
it('is hidden when close button is clicked', done => {
- this.calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
+ calloutComponent.$el.querySelector('#dismiss-callout-btn').click();
Vue.nextTick(() => {
- expect(this.calloutComponent.$el.childNodes.length).toBe(0);
+ expect(calloutComponent.$el.childNodes.length).toBe(0);
done();
});
});
diff --git a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
index 1809e92e1d9..1809e92e1d9 100644
--- a/spec/javascripts/pages/sessions/new/preserve_url_fragment_spec.js
+++ b/spec/frontend/pages/sessions/new/preserve_url_fragment_spec.js
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index 97b8f7bd913..1244d7342ad 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import axios from '~/lib/utils/axios_utils';
import PANEL_STATE from '~/prometheus_metrics/constants';
-import metrics from './mock_data';
+import { metrics1 as metrics } from './mock_data';
describe('PrometheusMetrics', () => {
const FIXTURE = 'services/prometheus/prometheus_service.html';
diff --git a/spec/frontend/prometheus_metrics/mock_data.js b/spec/frontend/prometheus_metrics/mock_data.js
index d5532537302..375447ac3be 100644
--- a/spec/frontend/prometheus_metrics/mock_data.js
+++ b/spec/frontend/prometheus_metrics/mock_data.js
@@ -1,4 +1,4 @@
-const metrics = [
+export const metrics1 = [
{
edit_path: '/root/prometheus-test/prometheus/metrics/3/edit',
id: 3,
@@ -19,4 +19,44 @@ const metrics = [
},
];
-export default metrics;
+export const metrics2 = [
+ {
+ group: 'Kubernetes',
+ priority: 1,
+ active_metrics: 4,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'HAProxy',
+ priority: 2,
+ active_metrics: 3,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'Apache',
+ priority: 3,
+ active_metrics: 5,
+ metrics_missing_requirements: 0,
+ },
+];
+
+export const missingVarMetrics = [
+ {
+ group: 'Kubernetes',
+ priority: 1,
+ active_metrics: 4,
+ metrics_missing_requirements: 0,
+ },
+ {
+ group: 'HAProxy',
+ priority: 2,
+ active_metrics: 3,
+ metrics_missing_requirements: 1,
+ },
+ {
+ group: 'Apache',
+ priority: 3,
+ active_metrics: 5,
+ metrics_missing_requirements: 3,
+ },
+];
diff --git a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index dca3e1553b9..437a2116f5c 100644
--- a/spec/javascripts/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import PANEL_STATE from '~/prometheus_metrics/constants';
-import { metrics, missingVarMetrics } from './mock_data';
+import { metrics2 as metrics, missingVarMetrics } from './mock_data';
describe('PrometheusMetrics', () => {
const FIXTURE = 'services/prometheus/prometheus_service.html';
@@ -126,7 +126,7 @@ describe('PrometheusMetrics', () => {
}
beforeEach(() => {
- spyOn(axios, 'get').and.callThrough();
+ jest.spyOn(axios, 'get');
prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
@@ -145,7 +145,7 @@ describe('PrometheusMetrics', () => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeFalsy();
expect(axios.get).toHaveBeenCalledWith(prometheusMetrics.activeMetricsEndpoint);
- setTimeout(() => {
+ setImmediate(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
done();
});
@@ -156,7 +156,7 @@ describe('PrometheusMetrics', () => {
prometheusMetrics.loadActiveMetrics();
- setTimeout(() => {
+ setImmediate(() => {
expect(prometheusMetrics.$monitoredMetricsLoading.hasClass('hidden')).toBeTruthy();
expect(prometheusMetrics.$monitoredMetricsEmpty.hasClass('hidden')).toBeFalsy();
done();
@@ -164,12 +164,12 @@ describe('PrometheusMetrics', () => {
});
it('should populate metrics list once response is loaded', done => {
- spyOn(prometheusMetrics, 'populateActiveMetrics');
+ jest.spyOn(prometheusMetrics, 'populateActiveMetrics').mockImplementation();
mockSuccess();
prometheusMetrics.loadActiveMetrics();
- setTimeout(() => {
+ setImmediate(() => {
expect(prometheusMetrics.populateActiveMetrics).toHaveBeenCalledWith(metrics);
done();
});
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 9f6888fca11..5230910b6f5 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -198,7 +198,7 @@ describe('Snippet header component', () => {
},
}).then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe(fullPath);
+ expect(window.location.pathname).toBe(`${fullPath}/snippets`);
});
});
});
diff --git a/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb b/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb
new file mode 100644
index 00000000000..8eb28c8c945
--- /dev/null
+++ b/spec/graphql/resolvers/alert_management/alert_status_counts_resolver_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Resolvers::AlertManagement::AlertStatusCountsResolver do
+ include GraphqlHelpers
+
+ describe '#resolve' do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let(:args) { {} }
+
+ subject { resolve_alert_status_counts(args) }
+
+ it { is_expected.to be_a(Gitlab::AlertManagement::AlertStatusCounts) }
+ specify { expect(subject.project).to eq(project) }
+
+ private
+
+ def resolve_alert_status_counts(args = {}, context = { current_user: current_user })
+ resolve(described_class, obj: project, args: args, ctx: context)
+ end
+ end
+end
diff --git a/spec/graphql/types/alert_management/alert_status_count_type_spec.rb b/spec/graphql/types/alert_management/alert_status_count_type_spec.rb
new file mode 100644
index 00000000000..1c56028425e
--- /dev/null
+++ b/spec/graphql/types/alert_management/alert_status_count_type_spec.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe GitlabSchema.types['AlertManagementAlertStatusCountsType'] do
+ specify { expect(described_class.graphql_name).to eq('AlertManagementAlertStatusCountsType') }
+
+ it 'exposes the expected fields' do
+ expected_fields = %i[
+ all
+ open
+ triggered
+ acknowledged
+ resolved
+ ignored
+ ]
+
+ expect(described_class).to have_graphql_fields(*expected_fields)
+ end
+end
diff --git a/spec/graphql/types/project_type_spec.rb b/spec/graphql/types/project_type_spec.rb
index 9a257ec8d1b..6368f743720 100644
--- a/spec/graphql/types/project_type_spec.rb
+++ b/spec/graphql/types/project_type_spec.rb
@@ -25,6 +25,7 @@ describe GitlabSchema.types['Project'] do
issue pipelines removeSourceBranchAfterMerge sentryDetailedError snippets
grafanaIntegration autocloseReferencedIssues suggestion_commit_message environments
boards jira_import_status jira_imports services releases release
+ alert_management_alerts alert_management_alert alert_management_alert_status_counts
]
expect(described_class).to include_graphql_fields(*expected_fields)
diff --git a/spec/javascripts/prometheus_metrics/mock_data.js b/spec/javascripts/prometheus_metrics/mock_data.js
deleted file mode 100644
index 3af56df92e2..00000000000
--- a/spec/javascripts/prometheus_metrics/mock_data.js
+++ /dev/null
@@ -1,41 +0,0 @@
-export const metrics = [
- {
- group: 'Kubernetes',
- priority: 1,
- active_metrics: 4,
- metrics_missing_requirements: 0,
- },
- {
- group: 'HAProxy',
- priority: 2,
- active_metrics: 3,
- metrics_missing_requirements: 0,
- },
- {
- group: 'Apache',
- priority: 3,
- active_metrics: 5,
- metrics_missing_requirements: 0,
- },
-];
-
-export const missingVarMetrics = [
- {
- group: 'Kubernetes',
- priority: 1,
- active_metrics: 4,
- metrics_missing_requirements: 0,
- },
- {
- group: 'HAProxy',
- priority: 2,
- active_metrics: 3,
- metrics_missing_requirements: 1,
- },
- {
- group: 'Apache',
- priority: 3,
- active_metrics: 5,
- metrics_missing_requirements: 3,
- },
-];
diff --git a/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
new file mode 100644
index 00000000000..816ed918fe8
--- /dev/null
+++ b/spec/lib/gitlab/alert_management/alert_status_counts_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::AlertManagement::AlertStatusCounts do
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:project) { create(:project) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, :ignored, project: project) }
+ let_it_be(:alert_3) { create(:alert_management_alert) }
+ let(:params) { {} }
+
+ describe '#execute' do
+ subject(:counts) { described_class.new(current_user, project, params) }
+
+ context 'for an unauthorized user' do
+ it 'returns zero for all statuses' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(0)
+
+ AlertManagement::Alert::STATUSES.each_key do |status|
+ expect(counts.send(status)).to eq(0)
+ end
+ end
+ end
+
+ context 'for an authorized user' do
+ before do
+ project.add_developer(current_user)
+ end
+
+ it 'returns the correct counts for each status' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(2)
+ expect(counts.resolved).to eq(1)
+ expect(counts.ignored).to eq(1)
+ expect(counts.triggered).to eq(0)
+ expect(counts.acknowledged).to eq(0)
+ end
+
+ context 'when filtering params are included' do
+ let(:params) { { status: AlertManagement::Alert::STATUSES[:resolved] } }
+
+ it 'returns the correct counts for each status' do
+ expect(counts.open).to eq(0)
+ expect(counts.all).to eq(1)
+ expect(counts.resolved).to eq(1)
+ expect(counts.ignored).to eq(0)
+ expect(counts.triggered).to eq(0)
+ expect(counts.acknowledged).to eq(0)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/network_policy_spec.rb b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
index 87ed922e099..f23d215a9a1 100644
--- a/spec/lib/gitlab/kubernetes/network_policy_spec.rb
+++ b/spec/lib/gitlab/kubernetes/network_policy_spec.rb
@@ -212,7 +212,7 @@ spec:
{
metadata: { name: name, namespace: namespace },
spec: { podSelector: pod_selector, policyTypes: %w(Ingress Egress), ingress: ingress, egress: egress }
- }
+ }.deep_stringify_keys
)
}
end
diff --git a/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
new file mode 100644
index 00000000000..fdf3b5bd045
--- /dev/null
+++ b/spec/lib/gitlab/metrics/samplers/database_sampler_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Samplers::DatabaseSampler do
+ subject { described_class.new(described_class::SAMPLING_INTERVAL_SECONDS) }
+
+ describe '#sample' do
+ before do
+ described_class::METRIC_DESCRIPTIONS.each_key do |metric|
+ allow(subject.metrics[metric]).to receive(:set)
+ end
+ end
+
+ context 'for ActiveRecord::Base' do
+ let(:labels) do
+ {
+ class: 'ActiveRecord::Base',
+ host: Gitlab::Database.config['host'],
+ port: Gitlab::Database.config['port']
+ }
+ end
+
+ context 'when the database is connected' do
+ it 'samples connection pool statistics' do
+ expect(subject.metrics[:size]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:connections]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:busy]).to receive(:set).with(labels, a_value >= 1)
+ expect(subject.metrics[:dead]).to receive(:set).with(labels, a_value >= 0)
+ expect(subject.metrics[:waiting]).to receive(:set).with(labels, a_value >= 0)
+
+ subject.sample
+ end
+ end
+
+ context 'when the database is not connected' do
+ before do
+ allow(ActiveRecord::Base).to receive(:connected?).and_return(false)
+ end
+
+ it 'records no samples' do
+ expect(subject.metrics[:size]).not_to receive(:set).with(labels, anything)
+
+ subject.sample
+ end
+ end
+ end
+ end
+end
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 6e8a8c03aad..929df0a7ffb 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,22 +113,27 @@ describe Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob, :clean_gitlab_r
end
describe 'droppable?' do
- where(:idempotent, :duplicate) do
- # [true, false].repeated_permutation(2)
- [[true, true],
- [true, false],
- [false, true],
- [false, false]]
+ where(:idempotent, :duplicate, :prevent_deduplication) 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]]
end
with_them do
before do
allow(AuthorizedProjectsWorker).to receive(:idempotent?).and_return(idempotent)
allow(duplicate_job).to receive(:duplicate?).and_return(duplicate)
+ stub_feature_flags("disable_#{queue}_deduplication" => prevent_deduplication)
end
it 'is droppable when all conditions are met' do
- if idempotent && duplicate
+ if idempotent && duplicate && !prevent_deduplication
expect(duplicate_job).to be_droppable
else
expect(duplicate_job).not_to be_droppable
diff --git a/spec/lib/gitlab/view/presenter/factory_spec.rb b/spec/lib/gitlab/view/presenter/factory_spec.rb
index 515a1b0a8e4..7bf3c325019 100644
--- a/spec/lib/gitlab/view/presenter/factory_spec.rb
+++ b/spec/lib/gitlab/view/presenter/factory_spec.rb
@@ -31,11 +31,11 @@ describe Gitlab::View::Presenter::Factory do
end
it 'uses the presenter_class if given on #initialize' do
- MyCustomPresenter = Class.new(described_class)
+ my_custom_presenter = Class.new(described_class)
- presenter = described_class.new(build, presenter_class: MyCustomPresenter).fabricate!
+ presenter = described_class.new(build, presenter_class: my_custom_presenter).fabricate!
- expect(presenter).to be_a(MyCustomPresenter)
+ expect(presenter).to be_a(my_custom_presenter)
end
end
end
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 97bffa23b31..1da0c6d4071 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -125,14 +125,14 @@ describe AlertManagement::Alert do
describe 'scopes' do
let_it_be(:project) { create(:project) }
- let_it_be(:alert_1) { create(:alert_management_alert, project: project) }
- let_it_be(:alert_2) { create(:alert_management_alert, :resolved, project: project) }
- let_it_be(:alert_3) { create(:alert_management_alert, :ignored, project: project) }
+ let_it_be(:triggered_alert) { create(:alert_management_alert, project: project) }
+ let_it_be(:resolved_alert) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:ignored_alert) { create(:alert_management_alert, :ignored, project: project) }
describe '.for_iid' do
- subject { AlertManagement::Alert.for_iid(alert_1.iid) }
+ subject { AlertManagement::Alert.for_iid(triggered_alert.iid) }
- it { is_expected.to match_array(alert_1) }
+ it { is_expected.to match_array(triggered_alert) }
end
describe '.for_status' do
@@ -140,26 +140,36 @@ describe AlertManagement::Alert do
subject { AlertManagement::Alert.for_status(status) }
- it { is_expected.to match_array(alert_2) }
+ it { is_expected.to match_array(resolved_alert) }
context 'with multiple statuses' do
let(:status) { AlertManagement::Alert::STATUSES.values_at(:resolved, :ignored) }
- it { is_expected.to match_array([alert_2, alert_3]) }
+ it { is_expected.to match_array([resolved_alert, ignored_alert]) }
end
end
- end
- describe '.for_fingerprint' do
- let_it_be(:fingerprint) { SecureRandom.hex }
- let_it_be(:project) { create(:project) }
- let_it_be(:alert_1) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
- let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
- let_it_be(:alert_3) { create(:alert_management_alert, fingerprint: fingerprint) }
+ describe '.for_fingerprint' do
+ let_it_be(:fingerprint) { SecureRandom.hex }
+ let_it_be(:alert_with_fingerprint) { create(:alert_management_alert, project: project, fingerprint: fingerprint) }
+ let_it_be(:unrelated_alert_with_finger_print) { create(:alert_management_alert, fingerprint: fingerprint) }
- subject { described_class.for_fingerprint(project, fingerprint) }
+ subject { described_class.for_fingerprint(project, fingerprint) }
+
+ it { is_expected.to contain_exactly(alert_with_fingerprint) }
+ end
- it { is_expected.to contain_exactly(alert_1) }
+ describe '.counts_by_status' do
+ subject { described_class.counts_by_status }
+
+ it do
+ is_expected.to eq(
+ triggered_alert.status => 1,
+ resolved_alert.status => 1,
+ ignored_alert.status => 1
+ )
+ end
+ end
end
describe '.search' do
diff --git a/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
new file mode 100644
index 00000000000..ffd328429ef
--- /dev/null
+++ b/spec/requests/api/graphql/project/alert_management/alert_status_counts_spec.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'getting Alert Management Alert counts by status' do
+ include GraphqlHelpers
+
+ let_it_be(:project) { create(:project, :repository) }
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:alert_1) { create(:alert_management_alert, :resolved, project: project) }
+ let_it_be(:alert_2) { create(:alert_management_alert, project: project) }
+ let_it_be(:other_project_alert) { create(:alert_management_alert) }
+ let(:params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ #{all_graphql_fields_for('AlertManagementAlertStatusCountsType'.classify)}
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'project',
+ { 'fullPath' => project.full_path },
+ query_graphql_field('alertManagementAlertStatusCounts', params, fields)
+ )
+ end
+
+ context 'with alert data' do
+ let(:alert_counts) { graphql_data.dig('project', 'alertManagementAlertStatusCounts') }
+
+ context 'without project permissions' do
+ let(:user) { create(:user) }
+
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it { expect(alert_counts).to be nil }
+ end
+
+ context 'with project permissions' do
+ before do
+ project.add_developer(current_user)
+ post_graphql(query, current_user: current_user)
+ end
+
+ it_behaves_like 'a working graphql query'
+ it 'returns the correct counts for each status' do
+ expect(alert_counts).to eq(
+ 'open' => 1,
+ 'all' => 2,
+ 'triggered' => 1,
+ 'acknowledged' => 0,
+ 'resolved' => 1,
+ 'ignored' => 0
+ )
+ end
+ end
+ end
+end