summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue10
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js3
-rw-r--r--app/assets/javascripts/notes/stores/actions.js2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss5
-rw-r--r--app/assets/stylesheets/framework/modal.scss14
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/controllers/projects/cycle_analytics/events_controller.rb2
-rw-r--r--app/controllers/projects/cycle_analytics_controller.rb2
-rw-r--r--app/models/cycle_analytics.rb46
-rw-r--r--app/models/cycle_analytics/base.rb27
-rw-r--r--app/models/cycle_analytics/project_level.rb22
-rw-r--r--app/models/prometheus_metric.rb65
-rw-r--r--app/models/prometheus_metric_enums.rb70
-rw-r--r--app/views/dashboard/milestones/index.html.haml2
-rw-r--r--app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml2
-rw-r--r--changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml5
-rw-r--r--changelogs/unreleased/64331-Assignee-field-in-a-new-issue-has-an-incorrect-line-wrap.yml5
-rw-r--r--changelogs/unreleased/embedded-metrics-be-2.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.52.0.yml5
-rw-r--r--changelogs/unreleased/prepare-cycle-analytics-for-group-level.yml5
-rw-r--r--changelogs/unreleased/winh-notes-service-deleteNote.yml5
-rw-r--r--db/fixtures/development/99_common_metrics.rb4
-rw-r--r--db/fixtures/production/999_common_metrics.rb4
-rw-r--r--db/importers/common_metrics_importer.rb105
-rw-r--r--db/migrate/20180831164910_import_common_metrics.rb4
-rw-r--r--db/migrate/20181006004100_import_common_metrics_nginx_vts.rb4
-rw-r--r--db/migrate/20190326164045_import_common_metrics_knative.rb4
-rw-r--r--db/migrate/20190408163745_prometheus_knative05_fix.rb4
-rw-r--r--doc/api/vulnerabilities.md2
-rw-r--r--doc/development/prometheus_metrics.md4
-rw-r--r--doc/user/project/integrations/prometheus.md65
-rw-r--r--jest.config.js11
-rw-r--r--lib/banzai/filter/inline_embeds_filter.rb67
-rw-r--r--lib/banzai/filter/inline_metrics_filter.rb43
-rw-r--r--lib/banzai/filter/inline_metrics_redactor_filter.rb98
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/pipeline/post_process_pipeline.rb1
-rw-r--r--lib/gitlab/cycle_analytics/base_event_fetcher.rb18
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb4
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb49
-rw-r--r--lib/gitlab/cycle_analytics/code_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/issue_helper.rb2
-rw-r--r--lib/gitlab/cycle_analytics/permissions.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/plan_helper.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb2
-rw-r--r--lib/gitlab/cycle_analytics/review_event_fetcher.rb2
-rw-r--r--lib/gitlab/cycle_analytics/test_helper.rb2
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb2
-rw-r--r--lib/gitlab/database_importers/common_metrics.rb8
-rw-r--r--lib/gitlab/database_importers/common_metrics/importer.rb78
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric.rb12
-rw-r--r--lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb40
-rw-r--r--lib/gitlab/metrics/dashboard/url.rb40
-rw-r--r--spec/features/dashboard/milestones_spec.rb14
-rw-r--r--spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap38
-rw-r--r--spec/frontend/error_tracking_settings/mock.js2
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js20
-rw-r--r--spec/lib/banzai/filter/inline_metrics_filter_spec.rb55
-rw-r--r--spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb58
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/usage_data_spec.rb2
-rw-r--r--spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb (renamed from spec/db/importers/common_metrics_importer_spec.rb)21
-rw-r--r--spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb16
-rw-r--r--spec/lib/gitlab/metrics/dashboard/url_spec.rb56
-rw-r--r--spec/models/cycle_analytics/code_spec.rb3
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb3
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb3
-rw-r--r--spec/models/cycle_analytics/production_spec.rb3
-rw-r--r--spec/models/cycle_analytics/project_level_spec.rb (renamed from spec/models/cycle_analytics_spec.rb)8
-rw-r--r--spec/models/cycle_analytics/review_spec.rb3
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb2
-rw-r--r--spec/models/cycle_analytics/test_spec.rb3
-rw-r--r--spec/routing/project_routing_spec.rb6
77 files changed, 986 insertions, 329 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index ba0a719118c..a63cb35e6f0 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.51.0
+1.52.0
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 99d77a75c23..197a0706062 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -105,7 +105,7 @@ export default {
</script>
<template>
- <div class="form-group">
+ <div class="confidential-merge-request-fork-group form-group">
<label>{{ __('Project') }}</label>
<div>
<dropdown
@@ -126,6 +126,14 @@ export default {
{{ __('No forks available to you.') }}<br />
<span v-html="noForkText"></span>
</template>
+ <gl-link
+ :href="helpPagePath"
+ class="w-auto p-0 d-inline-block text-primary bg-transparent"
+ target="_blank"
+ >
+ <span class="sr-only">{{ __('Read more') }}</span>
+ <i class="fa fa-question-circle" aria-hidden="true"></i>
+ </gl-link>
</p>
</div>
</div>
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index bc0f5c19b9d..9e0392110b6 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -9,9 +9,6 @@ export default {
const config = filter !== undefined ? { params: { notes_filter: filter } } : null;
return Vue.http.get(endpoint, config);
},
- deleteNote(endpoint) {
- return Vue.http.delete(endpoint);
- },
replyToDiscussion(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true });
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 2eefef8bd6e..30eab272aa9 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -62,7 +62,7 @@ export const updateDiscussion = ({ commit, state }, discussion) => {
};
export const deleteNote = ({ commit, dispatch, state }, note) =>
- service.deleteNote(note.path).then(() => {
+ axios.delete(note.path).then(() => {
const discussion = state.discussions.find(({ id }) => id === note.discussion_id);
commit(types.DELETE_NOTE, note);
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e75c1379dfb..29f63e9578d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -63,7 +63,8 @@
margin-top: 11px;
}
-.dropdown-toggle {
+.dropdown-toggle,
+.confidential-merge-request-fork-group .dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $white-light;
color: $gl-text-color;
@@ -288,7 +289,7 @@
padding: 0 1px;
a,
- button:not(.dropdown-toggle,.ci-action-icon-container),
+ button,
.menu-item {
@include dropdown-link;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 1d00372d04d..b721b90fbb3 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -90,6 +90,20 @@ body.modal-open {
.modal {
background-color: $black-transparent;
+ .modal-content {
+ border-radius: $modal-border-radius;
+
+ *:first-child {
+ border-top-left-radius: $modal-border-radius;
+ border-top-right-radius: $modal-border-radius;
+ }
+
+ *:last-child {
+ border-bottom-left-radius: $modal-border-radius;
+ border-bottom-right-radius: $modal-border-radius;
+ }
+ }
+
@include media-breakpoint-up(sm) {
.modal-dialog {
margin: 64px auto;
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 4521643ce08..c108f45622f 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -806,6 +806,7 @@ Modals
*/
$modal-body-height: 80px;
$modal-border-color: #e9ecef;
+$modal-border-radius: 0.25rem;
$priority-label-empty-state-width: 114px;
diff --git a/app/controllers/projects/cycle_analytics/events_controller.rb b/app/controllers/projects/cycle_analytics/events_controller.rb
index fb43356ff10..6314d9f2a9f 100644
--- a/app/controllers/projects/cycle_analytics/events_controller.rb
+++ b/app/controllers/projects/cycle_analytics/events_controller.rb
@@ -50,7 +50,7 @@ module Projects
end
def cycle_analytics
- @cycle_analytics ||= ::CycleAnalytics.new(project, options(events_params))
+ @cycle_analytics ||= ::CycleAnalytics::ProjectLevel.new(project, options: options(events_params))
end
def events_params
diff --git a/app/controllers/projects/cycle_analytics_controller.rb b/app/controllers/projects/cycle_analytics_controller.rb
index 8c071496ba9..2d46a71bf99 100644
--- a/app/controllers/projects/cycle_analytics_controller.rb
+++ b/app/controllers/projects/cycle_analytics_controller.rb
@@ -9,7 +9,7 @@ class Projects::CycleAnalyticsController < Projects::ApplicationController
before_action :authorize_read_cycle_analytics!
def show
- @cycle_analytics = ::CycleAnalytics.new(@project, options(cycle_analytics_params))
+ @cycle_analytics = ::CycleAnalytics::ProjectLevel.new(@project, options: options(cycle_analytics_params))
@cycle_analytics_no_data = @cycle_analytics.no_stats?
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
deleted file mode 100644
index d0f5b6970b1..00000000000
--- a/app/models/cycle_analytics.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# frozen_string_literal: true
-
-class CycleAnalytics
- STAGES = %i[issue plan code test review staging production].freeze
-
- def initialize(project, options)
- @project = project
- @options = options
- end
-
- def all_medians_per_stage
- STAGES.each_with_object({}) do |stage_name, medians_per_stage|
- medians_per_stage[stage_name] = self[stage_name].median
- end
- end
-
- def summary
- @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
- from: @options[:from],
- current_user: @options[:current_user]).data
- end
-
- def stats
- @stats ||= stats_per_stage
- end
-
- def no_stats?
- stats.all? { |hash| hash[:value].nil? }
- end
-
- def permissions(user:)
- Gitlab::CycleAnalytics::Permissions.get(user: user, project: @project)
- end
-
- def [](stage_name)
- Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
- end
-
- private
-
- def stats_per_stage
- STAGES.map do |stage_name|
- self[stage_name].as_json
- end
- end
-end
diff --git a/app/models/cycle_analytics/base.rb b/app/models/cycle_analytics/base.rb
new file mode 100644
index 00000000000..d7b28cd1b67
--- /dev/null
+++ b/app/models/cycle_analytics/base.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module CycleAnalytics
+ class Base
+ STAGES = %i[issue plan code test review staging production].freeze
+
+ def all_medians_by_stage
+ STAGES.each_with_object({}) do |stage_name, medians_per_stage|
+ medians_per_stage[stage_name] = self[stage_name].median
+ end
+ end
+
+ def stats
+ @stats ||= STAGES.map do |stage_name|
+ self[stage_name].as_json
+ end
+ end
+
+ def no_stats?
+ stats.all? { |hash| hash[:value].nil? }
+ end
+
+ def [](stage_name)
+ Gitlab::CycleAnalytics::Stage[stage_name].new(project: @project, options: @options)
+ end
+ end
+end
diff --git a/app/models/cycle_analytics/project_level.rb b/app/models/cycle_analytics/project_level.rb
new file mode 100644
index 00000000000..22631cc7d41
--- /dev/null
+++ b/app/models/cycle_analytics/project_level.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module CycleAnalytics
+ class ProjectLevel < Base
+ attr_reader :project, :options
+
+ def initialize(project, options:)
+ @project = project
+ @options = options
+ end
+
+ def summary
+ @summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(project,
+ from: options[:from],
+ current_user: options[:current_user]).data
+ end
+
+ def permissions(user:)
+ Gitlab::CycleAnalytics::Permissions.get(user: user, project: project)
+ end
+ end
+end
diff --git a/app/models/prometheus_metric.rb b/app/models/prometheus_metric.rb
index 62090444f79..b8e7673dcf5 100644
--- a/app/models/prometheus_metric.rb
+++ b/app/models/prometheus_metric.rb
@@ -3,68 +3,7 @@
class PrometheusMetric < ApplicationRecord
belongs_to :project, validate: true, inverse_of: :prometheus_metrics
- enum group: {
- # built-in groups
- nginx_ingress_vts: -1,
- ha_proxy: -2,
- aws_elb: -3,
- nginx: -4,
- kubernetes: -5,
- nginx_ingress: -6,
-
- # custom/user groups
- business: 0,
- response: 1,
- system: 2
- }
-
- GROUP_DETAILS = {
- # built-in groups
- nginx_ingress_vts: {
- group_title: _('Response metrics (NGINX Ingress VTS)'),
- required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
- priority: 10
- }.freeze,
- nginx_ingress: {
- group_title: _('Response metrics (NGINX Ingress)'),
- required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
- priority: 10
- }.freeze,
- ha_proxy: {
- group_title: _('Response metrics (HA Proxy)'),
- required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
- priority: 10
- }.freeze,
- aws_elb: {
- group_title: _('Response metrics (AWS ELB)'),
- required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
- priority: 10
- }.freeze,
- nginx: {
- group_title: _('Response metrics (NGINX)'),
- required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
- priority: 10
- }.freeze,
- kubernetes: {
- group_title: _('System metrics (Kubernetes)'),
- required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
- priority: 5
- }.freeze,
-
- # custom/user groups
- business: {
- group_title: _('Business metrics (Custom)'),
- priority: 0
- }.freeze,
- response: {
- group_title: _('Response metrics (Custom)'),
- priority: -5
- }.freeze,
- system: {
- group_title: _('System metrics (Custom)'),
- priority: -10
- }.freeze
- }.freeze
+ enum group: PrometheusMetricEnums.groups
validates :title, presence: true
validates :query, presence: true
@@ -121,6 +60,6 @@ class PrometheusMetric < ApplicationRecord
private
def group_details(group)
- GROUP_DETAILS.fetch(group.to_sym)
+ PrometheusMetricEnums.group_details.fetch(group.to_sym)
end
end
diff --git a/app/models/prometheus_metric_enums.rb b/app/models/prometheus_metric_enums.rb
new file mode 100644
index 00000000000..6cb22cc69cd
--- /dev/null
+++ b/app/models/prometheus_metric_enums.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module PrometheusMetricEnums
+ def self.groups
+ {
+ # built-in groups
+ nginx_ingress_vts: -1,
+ ha_proxy: -2,
+ aws_elb: -3,
+ nginx: -4,
+ kubernetes: -5,
+ nginx_ingress: -6,
+
+ # custom/user groups
+ business: 0,
+ response: 1,
+ system: 2
+ }
+ end
+
+ def self.group_details
+ {
+ # built-in groups
+ nginx_ingress_vts: {
+ group_title: _('Response metrics (NGINX Ingress VTS)'),
+ required_metrics: %w(nginx_upstream_responses_total nginx_upstream_response_msecs_avg),
+ priority: 10
+ }.freeze,
+ nginx_ingress: {
+ group_title: _('Response metrics (NGINX Ingress)'),
+ required_metrics: %w(nginx_ingress_controller_requests nginx_ingress_controller_ingress_upstream_latency_seconds_sum),
+ priority: 10
+ }.freeze,
+ ha_proxy: {
+ group_title: _('Response metrics (HA Proxy)'),
+ required_metrics: %w(haproxy_frontend_http_requests_total haproxy_frontend_http_responses_total),
+ priority: 10
+ }.freeze,
+ aws_elb: {
+ group_title: _('Response metrics (AWS ELB)'),
+ required_metrics: %w(aws_elb_request_count_sum aws_elb_latency_average aws_elb_httpcode_backend_5_xx_sum),
+ priority: 10
+ }.freeze,
+ nginx: {
+ group_title: _('Response metrics (NGINX)'),
+ required_metrics: %w(nginx_server_requests nginx_server_requestMsec),
+ priority: 10
+ }.freeze,
+ kubernetes: {
+ group_title: _('System metrics (Kubernetes)'),
+ required_metrics: %w(container_memory_usage_bytes container_cpu_usage_seconds_total),
+ priority: 5
+ }.freeze,
+
+ # custom/user groups
+ business: {
+ group_title: _('Business metrics (Custom)'),
+ priority: 0
+ }.freeze,
+ response: {
+ group_title: _('Response metrics (Custom)'),
+ priority: -5
+ }.freeze,
+ system: {
+ group_title: _('System metrics (Custom)'),
+ priority: -10
+ }.freeze
+ }.freeze
+ end
+end
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 37ba2143eba..b9be6028b72 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -8,7 +8,7 @@
- if current_user
.page-title-controls
= render 'shared/new_project_item_select',
- path: 'milestones/new', label: 'New milestone',
+ path: '-/milestones/new', label: 'New milestone',
include_groups: true, type: :milestones
.top-area
diff --git a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
index 5336159e762..60dc893d9f9 100644
--- a/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
+++ b/app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml
@@ -1,4 +1,4 @@
-= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-lg-4" : "col-sm-2"}"
+= form.label :assignee_id, "Assignee", class: "col-form-label #{has_due_date ? "col-md-2 col-lg-4" : "col-sm-2"}"
.col-sm-10{ class: ("col-md-8" if has_due_date) }
.issuable-form-select-holder.selectbox
- issuable.assignees.each do |assignee|
diff --git a/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml b/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml
new file mode 100644
index 00000000000..825247db3e7
--- /dev/null
+++ b/changelogs/unreleased/64321-wrong-url-when-creating-milestones-from-instance-milestones-dashboard.yml
@@ -0,0 +1,5 @@
+---
+title: Fix wrong URL when creating milestones from instance milestones dashboard
+merge_request: 30512
+author:
+type: fixed
diff --git a/changelogs/unreleased/64331-Assignee-field-in-a-new-issue-has-an-incorrect-line-wrap.yml b/changelogs/unreleased/64331-Assignee-field-in-a-new-issue-has-an-incorrect-line-wrap.yml
new file mode 100644
index 00000000000..7d67b94869d
--- /dev/null
+++ b/changelogs/unreleased/64331-Assignee-field-in-a-new-issue-has-an-incorrect-line-wrap.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed incorrect line wrap for assignee label in issues
+merge_request: 30523
+author: Marc Schwede
+type: fixed
diff --git a/changelogs/unreleased/embedded-metrics-be-2.yml b/changelogs/unreleased/embedded-metrics-be-2.yml
new file mode 100644
index 00000000000..2623b4a2e0c
--- /dev/null
+++ b/changelogs/unreleased/embedded-metrics-be-2.yml
@@ -0,0 +1,5 @@
+---
+title: Expose placeholder element for metrics charts in GFM
+merge_request: 29861
+author:
+type: added
diff --git a/changelogs/unreleased/gitaly-version-v1.52.0.yml b/changelogs/unreleased/gitaly-version-v1.52.0.yml
new file mode 100644
index 00000000000..d56a392c4ff
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.52.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.52.0
+merge_request: 30568
+author:
+type: changed
diff --git a/changelogs/unreleased/prepare-cycle-analytics-for-group-level.yml b/changelogs/unreleased/prepare-cycle-analytics-for-group-level.yml
new file mode 100644
index 00000000000..d7bfc67b208
--- /dev/null
+++ b/changelogs/unreleased/prepare-cycle-analytics-for-group-level.yml
@@ -0,0 +1,5 @@
+---
+title: Modify cycle analytics on project level
+merge_request: 30356
+author:
+type: changed
diff --git a/changelogs/unreleased/winh-notes-service-deleteNote.yml b/changelogs/unreleased/winh-notes-service-deleteNote.yml
new file mode 100644
index 00000000000..cf2b37755a2
--- /dev/null
+++ b/changelogs/unreleased/winh-notes-service-deleteNote.yml
@@ -0,0 +1,5 @@
+---
+title: Remove deleteNote from notes service
+merge_request: 30537
+author: Frank van Rest
+type: other
diff --git a/db/fixtures/development/99_common_metrics.rb b/db/fixtures/development/99_common_metrics.rb
index 1f39c0ce5a0..d52f78ea536 100644
--- a/db/fixtures/development/99_common_metrics.rb
+++ b/db/fixtures/development/99_common_metrics.rb
@@ -1,5 +1,3 @@
# frozen_string_literal: true
-require Rails.root.join('db/importers/common_metrics_importer.rb')
-
-::Importers::CommonMetricsImporter.new.execute
+::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
diff --git a/db/fixtures/production/999_common_metrics.rb b/db/fixtures/production/999_common_metrics.rb
index 1f39c0ce5a0..d52f78ea536 100644
--- a/db/fixtures/production/999_common_metrics.rb
+++ b/db/fixtures/production/999_common_metrics.rb
@@ -1,5 +1,3 @@
# frozen_string_literal: true
-require Rails.root.join('db/importers/common_metrics_importer.rb')
-
-::Importers::CommonMetricsImporter.new.execute
+::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
diff --git a/db/importers/common_metrics_importer.rb b/db/importers/common_metrics_importer.rb
deleted file mode 100644
index 195bde8f34a..00000000000
--- a/db/importers/common_metrics_importer.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-# frozen_string_literal: true
-
-module Importers
- class PrometheusMetric < ActiveRecord::Base
- enum group: {
- # built-in groups
- nginx_ingress_vts: -1,
- ha_proxy: -2,
- aws_elb: -3,
- nginx: -4,
- kubernetes: -5,
- nginx_ingress: -6,
-
- # custom groups
- business: 0,
- response: 1,
- system: 2
- }
-
- scope :common, -> { where(common: true) }
-
- GROUP_TITLES = {
- business: _('Business metrics (Custom)'),
- response: _('Response metrics (Custom)'),
- system: _('System metrics (Custom)'),
- nginx_ingress_vts: _('Response metrics (NGINX Ingress VTS)'),
- nginx_ingress: _('Response metrics (NGINX Ingress)'),
- ha_proxy: _('Response metrics (HA Proxy)'),
- aws_elb: _('Response metrics (AWS ELB)'),
- nginx: _('Response metrics (NGINX)'),
- kubernetes: _('System metrics (Kubernetes)')
- }.freeze
- end
-
- class CommonMetricsImporter
- MissingQueryId = Class.new(StandardError)
-
- attr_reader :content
-
- def initialize(filename = 'common_metrics.yml')
- @content = YAML.load_file(Rails.root.join('config', 'prometheus', filename))
- end
-
- def execute
- PrometheusMetric.reset_column_information
-
- process_content do |id, attributes|
- find_or_build_metric!(id)
- .update!(**attributes)
- end
- end
-
- private
-
- def process_content(&blk)
- content['panel_groups'].map do |group|
- process_group(group, &blk)
- end
- end
-
- def process_group(group, &blk)
- attributes = {
- group: find_group_title_key(group['group'])
- }
-
- group['panels'].map do |panel|
- process_panel(panel, attributes, &blk)
- end
- end
-
- def process_panel(panel, attributes, &blk)
- attributes = attributes.merge(
- title: panel['title'],
- y_label: panel['y_label'])
-
- panel['metrics'].map do |metric_details|
- process_metric_details(metric_details, attributes, &blk)
- end
- end
-
- def process_metric_details(metric_details, attributes, &blk)
- attributes = attributes.merge(
- legend: metric_details['label'],
- query: metric_details['query_range'],
- unit: metric_details['unit'])
-
- yield(metric_details['id'], attributes)
- end
-
- def find_or_build_metric!(id)
- raise MissingQueryId unless id
-
- PrometheusMetric.common.find_by(identifier: id) ||
- PrometheusMetric.new(common: true, identifier: id)
- end
-
- def find_group_title_key(title)
- PrometheusMetric.groups[find_group_title(title)]
- end
-
- def find_group_title(title)
- PrometheusMetric::GROUP_TITLES.invert[title]
- end
- end
-end
diff --git a/db/migrate/20180831164910_import_common_metrics.rb b/db/migrate/20180831164910_import_common_metrics.rb
index f67d5f40aad..4e61a25c1ad 100644
--- a/db/migrate/20180831164910_import_common_metrics.rb
+++ b/db/migrate/20180831164910_import_common_metrics.rb
@@ -3,12 +3,10 @@
class ImportCommonMetrics < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
- require Rails.root.join('db/importers/common_metrics_importer.rb')
-
DOWNTIME = false
def up
- Importers::CommonMetricsImporter.new.execute
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
diff --git a/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb b/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
index 5cd312837df..2b238774dca 100644
--- a/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
+++ b/db/migrate/20181006004100_import_common_metrics_nginx_vts.rb
@@ -1,12 +1,10 @@
class ImportCommonMetricsNginxVts < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
- require Rails.root.join('db/importers/common_metrics_importer.rb')
-
DOWNTIME = false
def up
- Importers::CommonMetricsImporter.new.execute
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
diff --git a/db/migrate/20190326164045_import_common_metrics_knative.rb b/db/migrate/20190326164045_import_common_metrics_knative.rb
index 340ec1e1f75..6b331755774 100644
--- a/db/migrate/20190326164045_import_common_metrics_knative.rb
+++ b/db/migrate/20190326164045_import_common_metrics_knative.rb
@@ -3,12 +3,10 @@
class ImportCommonMetricsKnative < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
- require Rails.root.join('db/importers/common_metrics_importer.rb')
-
DOWNTIME = false
def up
- Importers::CommonMetricsImporter.new.execute
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
diff --git a/db/migrate/20190408163745_prometheus_knative05_fix.rb b/db/migrate/20190408163745_prometheus_knative05_fix.rb
index c11f6f0e29b..3d0aa782669 100644
--- a/db/migrate/20190408163745_prometheus_knative05_fix.rb
+++ b/db/migrate/20190408163745_prometheus_knative05_fix.rb
@@ -6,12 +6,10 @@
class PrometheusKnative05Fix < ActiveRecord::Migration[5.0]
include Gitlab::Database::MigrationHelpers
- require Rails.root.join('db/importers/common_metrics_importer.rb')
-
DOWNTIME = false
def up
- Importers::CommonMetricsImporter.new.execute
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
diff --git a/doc/api/vulnerabilities.md b/doc/api/vulnerabilities.md
index cc6e5d3960b..eaa4c13de55 100644
--- a/doc/api/vulnerabilities.md
+++ b/doc/api/vulnerabilities.md
@@ -30,6 +30,7 @@ GET /projects/:id/vulnerabilities?scope=all
GET /projects/:id/vulnerabilities?scope=dismissed
GET /projects/:id/vulnerabilities?severity=high
GET /projects/:id/vulnerabilities?confidence=unknown,experimental
+GET /projects/:id/vulnerabilities?pipeline_id=42
```
| Attribute | Type | Required | Description |
@@ -39,6 +40,7 @@ GET /projects/:id/vulnerabilities?confidence=unknown,experimental
| `scope` | string | no | Returns vulnerabilities for the given scope: `all` or `dismissed`. Defaults to `dismissed` |
| `severity` | string array | no | Returns vulnerabilities belonging to specified severity level: `undefined`, `info`, `unknown`, `low`, `medium`, `high`, or `critical`. Defaults to all' |
| `confidence` | string array | no | Returns vulnerabilities belonging to specified confidence level: `undefined`, `ignore`, `unknown`, `experimental`, `low`, `medium`, `high`, or `confirmed`. Defaults to all |
+| `pipeline_id` | integer/string | no | Returns vulnerabilities belonging to specified pipeline. |
```bash
curl --header "PRIVATE-TOKEN: <your_access_token>" https://gitlab.example.com/api/v4/projects/4/vulnerabilities
diff --git a/doc/development/prometheus_metrics.md b/doc/development/prometheus_metrics.md
index 0511e735843..576601372a3 100644
--- a/doc/development/prometheus_metrics.md
+++ b/doc/development/prometheus_metrics.md
@@ -33,12 +33,10 @@ For example: you might be interested in migrating all dependent data to a differ
class ImportCommonMetrics < ActiveRecord::Migration[4.2]
include Gitlab::Database::MigrationHelpers
- require Rails.root.join('db/importers/common_metrics_importer.rb')
-
DOWNTIME = false
def up
- Importers::CommonMetricsImporter.new.execute
+ ::Gitlab::DatabaseImporters::CommonMetrics::Importer.new.execute
end
def down
diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md
index 01b6650bfab..765aa91b00f 100644
--- a/doc/user/project/integrations/prometheus.md
+++ b/doc/user/project/integrations/prometheus.md
@@ -121,7 +121,70 @@ GitLab supports a limited set of [CI variables](../../../ci/variables/README.htm
To specify a variable in a query, enclose it in curly braces with a leading percent. For example: `%{ci_environment_slug}`.
-### Setting up alerts for Prometheus metrics **(ULTIMATE)**
+### Defining Dashboards for Prometheus Metrics per Project
+
+All projects include a GitLab-defined system dashboard, which includes a few key metrics. Optionally, additional dashboards can also be defined by including configuration files in the project repository under `.gitlab/dashboards`. Configuration files nested under subdirectories will not be available in the UI. Each file should define the layout of the dashboard and the prometheus queries used to populate data. Dashboards can be selected from the dropdown in the UI.
+
+#### Relationship to Custom Metrics
+
+[Custom Metrics](#adding-additional-metrics-premium) are defined through the UI and, at this point, are unique from metrics defined in dashboard configuration files. Custom Metrics will appear on the system dashboard, as well as support alerting, whereas metrics defined in configuration files do not yet support alerts.
+
+#### Dashboard Configuration
+
+Dashboards have several components. A dashboard has many panel groups, which are comprised of panels, which support one or more metrics. The dashboard should be saved with the `.yml` extension.
+
+Sample YML Configuration
+```
+dashboard: 'Dashboard Title'
+priority: 2
+panel_groups:
+ - group: 'Group Title'
+ panels:
+ - type: area-chart
+ title: "Chart Title"
+ y_label: "Y-Axis"
+ metrics:
+ - id: metric_of_ages
+ query_range: 'http_requests_total'
+ label: "Metric of Ages"
+ unit: "count"
+```
+
+The above sample dashboard would display a single area chart. The following sections outline the details of expected properties.
+
+##### Dashboard Properties
+| Property | Type | Required? | Meaning |
+| ------ | ------ | ------ | ------ |
+| `dashboard` | string | required | Heading for the dashboard. Only one dashboard should be defined per file. |
+| `priority` | number | optional, default to definition order | Order to appear in dashboard dropdown, higher priority should be higher in the dropdown. Numbers do not need to be consecutive. |
+| `panel_groups` | array | required | The panel groups which should be on the dashboard. |
+
+##### Panel Group Properties
+| Property | Type | Required? | Meaning |
+| ------ | ------ | ------ | ------ |
+| `group` | string | required | Heading for the panel group. |
+| `priority` | number | optional, defaults to order in file | Order to appear on the dashboard, higher priority will be higher on the page. Numbers do not need to be consecutive. |
+| `panels` | array | required | The panels which should be in the panel group. |
+
+##### Panel Properties
+| Property | Type | Required? | Meaning |
+| ------ | ------ | ------ | ------- |
+| `type` | enum | optional, defaults to `area-chart` | Specifies the chart type to use. Only `area-chart` is currently supported. |
+| `title` | string | required | Heading for the panel. |
+| `y_label` | string | optional, but highly encouraged | Y-Axis label for the panel. |
+| `weight` | number | optional, defaults to order in file | Order to appear within the grouping, higher priority will be higher on the page. Numbers do not need to be consecutive. |
+| `metrics` | array | required | The metrics which should be displayed in the panel. |
+
+##### Metric Properties
+| Property | Type | Required? | Meaning |
+| ------ | ------ | ------ | ------ |
+| `id` | string | optional | Used for associating dashboard metrics with database records. Must be unique across dashboard configuration files. Required for [alerting](#setting-up-alerts-for-prometheus-metrics-ultimate) (support not yet enabled, see [relevant issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60319)). |
+| `unit` | string | required | Defines the unit of the query's return data. |
+| `label` | string | optional, but highly encouraged | Defines the legend-label for the query. Should be unique within the panel's metrics. |
+| `query` | string | required unless `query_range` is defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. |
+| `query_range` | string | required unless `query` is defined | Defines the Prometheus query to be used to populate the chart/panel. If defined, the `query_range` endpoint of the [Prometheus API](https://prometheus.io/docs/prometheus/latest/querying/api/) will be utilized. |
+
+### Setting up alerts for Prometheus metrics **[ULTIMATE]**
#### Managed Prometheus instances
diff --git a/jest.config.js b/jest.config.js
index 84481642250..986b8465eef 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -15,9 +15,18 @@ if (process.env.CI) {
]);
}
+let testMatch = ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'];
+
+// workaround for eslint-import-resolver-jest only resolving in test files
+// see https://github.com/JoinColony/eslint-import-resolver-jest#note
+const isESLint = module.parent.path.includes('/eslint-import-resolver-jest/');
+if (isESLint) {
+ testMatch = testMatch.map(path => path.replace('_spec.js', ''));
+}
+
// eslint-disable-next-line import/no-commonjs
module.exports = {
- testMatch: ['<rootDir>/spec/frontend/**/*_spec.js', '<rootDir>/ee/spec/frontend/**/*_spec.js'],
+ testMatch,
moduleFileExtensions: ['js', 'json', 'vue'],
moduleNameMapper: {
'^~(/.*)$': '<rootDir>/app/assets/javascripts$1',
diff --git a/lib/banzai/filter/inline_embeds_filter.rb b/lib/banzai/filter/inline_embeds_filter.rb
new file mode 100644
index 00000000000..97394fd8f82
--- /dev/null
+++ b/lib/banzai/filter/inline_embeds_filter.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a node for each occurence of
+ # a given link format. To transform references to DB
+ # resources in place, prefer to inherit from AbstractReferenceFilter.
+ class InlineEmbedsFilter < HTML::Pipeline::Filter
+ # Find every relevant link, create a new node based on
+ # the link, and insert this node after any html content
+ # surrounding the link.
+ def call
+ return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
+
+ doc.xpath(xpath_search).each do |node|
+ next unless element = element_to_embed(node)
+
+ # We want this to follow any surrounding content. For example,
+ # if a link is inline in a paragraph.
+ node.parent.children.last.add_next_sibling(element)
+ end
+
+ doc
+ end
+
+ # Implement in child class.
+ #
+ # Return a Nokogiri::XML::Element to embed in the
+ # markdown.
+ def create_element(params)
+ end
+
+ # Implement in child class unless overriding #embed_params
+ #
+ # Returns the regex pattern used to filter
+ # to only matching urls.
+ def link_pattern
+ end
+
+ # Returns the xpath query string used to select nodes
+ # from the html document on which the embed is based.
+ #
+ # Override to select nodes other than links.
+ def xpath_search
+ 'descendant-or-self::a[@href]'
+ end
+
+ # Creates a new element based on the parameters
+ # obtained from the target link
+ def element_to_embed(node)
+ return unless params = embed_params(node)
+
+ create_element(params)
+ end
+
+ # Returns a hash of named parameters based on the
+ # provided regex with string keys.
+ #
+ # Override to select nodes other than links.
+ def embed_params(node)
+ url = node['href']
+
+ link_pattern.match(url) { |m| m.named_captures }
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_filter.rb b/lib/banzai/filter/inline_metrics_filter.rb
new file mode 100644
index 00000000000..0120cc37d6f
--- /dev/null
+++ b/lib/banzai/filter/inline_metrics_filter.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that inserts a placeholder element for each
+ # reference to a metrics dashboard.
+ class InlineMetricsFilter < Banzai::Filter::InlineEmbedsFilter
+ # Placeholder element for the frontend to use as an
+ # injection point for charts.
+ def create_element(params)
+ doc.document.create_element(
+ 'div',
+ class: 'js-render-metrics',
+ 'data-dashboard-url': metrics_dashboard_url(params)
+ )
+ end
+
+ # Endpoint FE should hit to collect the appropriate
+ # chart information
+ def metrics_dashboard_url(params)
+ Gitlab::Metrics::Dashboard::Url.build_dashboard_url(
+ params['namespace'],
+ params['project'],
+ params['environment'],
+ embedded: true
+ )
+ end
+
+ # Search params for selecting metrics links. A few
+ # simple checks is enough to boost performance without
+ # the cost of doing a full regex match.
+ def xpath_search
+ "descendant-or-self::a[contains(@href,'metrics') and \
+ starts-with(@href, '#{Gitlab.config.gitlab.url}')]"
+ end
+
+ # Regular expression matching metrics urls
+ def link_pattern
+ Gitlab::Metrics::Dashboard::Url.regex
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/inline_metrics_redactor_filter.rb b/lib/banzai/filter/inline_metrics_redactor_filter.rb
new file mode 100644
index 00000000000..ff91be2cbb7
--- /dev/null
+++ b/lib/banzai/filter/inline_metrics_redactor_filter.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Banzai
+ module Filter
+ # HTML filter that removes embeded elements that the current user does
+ # not have permission to view.
+ class InlineMetricsRedactorFilter < HTML::Pipeline::Filter
+ include Gitlab::Utils::StrongMemoize
+
+ METRICS_CSS_CLASS = '.js-render-metrics'
+
+ # Finds all embeds based on the css class the FE
+ # uses to identify the embedded content, removing
+ # only unnecessary nodes.
+ def call
+ return doc unless Feature.enabled?(:gfm_embedded_metrics, context[:project])
+
+ nodes.each do |node|
+ path = paths_by_node[node]
+ user_has_access = user_access_by_path[path]
+
+ node.remove unless user_has_access
+ end
+
+ doc
+ end
+
+ private
+
+ def user
+ context[:current_user]
+ end
+
+ # Returns all nodes which the FE will identify as
+ # a metrics dashboard placeholder element
+ #
+ # @return [Nokogiri::XML::NodeSet]
+ def nodes
+ @nodes ||= doc.css(METRICS_CSS_CLASS)
+ end
+
+ # Maps a node to the full path of a project.
+ # Memoized so we only need to run the regex to get
+ # the project full path from the url once per node.
+ #
+ # @return [Hash<Nokogiri::XML::Node, String>]
+ def paths_by_node
+ strong_memoize(:paths_by_node) do
+ nodes.each_with_object({}) do |node, paths|
+ paths[node] = path_for_node(node)
+ end
+ end
+ end
+
+ # Gets a project's full_path from the dashboard url
+ # in the placeholder node. The FE will use the attr
+ # `data-dashboard-url`, so we want to check against that
+ # attribute directly in case a user has manually
+ # created a metrics element (rather than supporting
+ # an alternate attr in InlineMetricsFilter).
+ #
+ # @return [String]
+ def path_for_node(node)
+ url = node.attribute('data-dashboard-url').to_s
+
+ Gitlab::Metrics::Dashboard::Url.regex.match(url) do |m|
+ "#{$~[:namespace]}/#{$~[:project]}"
+ end
+ end
+
+ # Maps a project's full path to a Project object.
+ # Contains all of the Projects referenced in the
+ # metrics placeholder elements of the current document
+ #
+ # @return [Hash<String, Project>]
+ def projects_by_path
+ strong_memoize(:projects_by_path) do
+ Project.eager_load(:route, namespace: [:route])
+ .where_full_path_in(paths_by_node.values.uniq)
+ .index_by(&:full_path)
+ end
+ end
+
+ # Returns a mapping representing whether the current user
+ # has permission to view the metrics for the project.
+ # Determined in a batch
+ #
+ # @return [Hash<Project, Boolean>]
+ def user_access_by_path
+ strong_memoize(:user_access_by_path) do
+ projects_by_path.each_with_object({}) do |(path, project), access|
+ access[path] = Ability.allowed?(user, :read_environment, project)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index d67f461be57..2c1006f708a 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -25,6 +25,7 @@ module Banzai
Filter::VideoLinkFilter,
Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
+ Filter::InlineMetricsFilter,
Filter::TableOfContentsFilter,
Filter::AutolinkFilter,
Filter::ExternalLinkFilter,
diff --git a/lib/banzai/pipeline/post_process_pipeline.rb b/lib/banzai/pipeline/post_process_pipeline.rb
index 7eaad6d7560..5c199453638 100644
--- a/lib/banzai/pipeline/post_process_pipeline.rb
+++ b/lib/banzai/pipeline/post_process_pipeline.rb
@@ -13,6 +13,7 @@ module Banzai
def self.internal_link_filters
[
Filter::RedactorFilter,
+ Filter::InlineMetricsRedactorFilter,
Filter::RelativeLinkFilter,
Filter::IssuableStateFilter,
Filter::SuggestionFilter
diff --git a/lib/gitlab/cycle_analytics/base_event_fetcher.rb b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
index 304d60996a6..0cacef5b278 100644
--- a/lib/gitlab/cycle_analytics/base_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/base_event_fetcher.rb
@@ -5,11 +5,11 @@ module Gitlab
class BaseEventFetcher
include BaseQuery
- attr_reader :projections, :query, :stage, :order
+ attr_reader :projections, :query, :stage, :order, :project, :options
MAX_EVENTS = 50
- def initialize(project:, stage:, options:)
+ def initialize(project: nil, stage:, options:)
@project = project
@stage = stage
@options = options
@@ -40,13 +40,13 @@ module Gitlab
end
def events_query
- diff_fn = subtract_datetimes_diff(base_query, @options[:start_time_attrs], @options[:end_time_attrs])
+ diff_fn = subtract_datetimes_diff(base_query, options[:start_time_attrs], options[:end_time_attrs])
base_query.project(extract_diff_epoch(diff_fn).as('total_time'), *projections).order(order.desc).take(MAX_EVENTS)
end
def default_order
- [@options[:start_time_attrs]].flatten.first
+ [options[:start_time_attrs]].flatten.first
end
def serialize(_event)
@@ -59,13 +59,21 @@ module Gitlab
def allowed_ids
@allowed_ids ||= allowed_ids_finder_class
- .new(@options[:current_user], project_id: @project.id)
+ .new(options[:current_user], allowed_ids_source)
.execute.where(id: event_result_ids).pluck(:id)
end
def event_result_ids
event_result.map { |event| event['id'] }
end
+
+ def allowed_ids_source
+ { project_id: project.id }
+ end
+
+ def projects
+ [project]
+ end
end
end
end
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
index 36231b187cd..39fc1759cfc 100644
--- a/lib/gitlab/cycle_analytics/base_query.rb
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -10,7 +10,7 @@ module Gitlab
private
def base_query
- @base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @base_query ||= stage_query(projects.map(&:id))
end
def stage_query(project_ids)
@@ -18,7 +18,7 @@ module Gitlab
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
- .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .where(issue_table[:created_at].gteq(options[:from]))
# Load merge_requests
query = query.join(mr_table, Arel::Nodes::OuterJoin)
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index e2d6a301734..98b86e54340 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -5,7 +5,9 @@ module Gitlab
class BaseStage
include BaseQuery
- def initialize(project:, options:)
+ attr_reader :project, :options
+
+ def initialize(project: nil, options:)
@project = project
@options = options
end
@@ -14,8 +16,8 @@ module Gitlab
event_fetcher.fetch
end
- def as_json
- AnalyticsStageSerializer.new.represent(self)
+ def as_json(serializer: AnalyticsStageSerializer)
+ serializer.new.represent(self)
end
def title
@@ -23,21 +25,14 @@ module Gitlab
end
def median
- BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
- cte_table = Arel::Table.new("cte_table_for_#{name}")
-
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(cte_table,
- subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
+ return if project.nil?
+ BatchLoader.for(project.id).batch(key: name) do |project_ids, loader|
if project_ids.one?
- loader.call(@project.id, median_datetime(cte_table, interval_query, name))
+ loader.call(project.id, median_query(project_ids))
else
begin
- median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
+ median_datetimes(cte_table, interval_query(project_ids), name, :project_id)&.each do |project_id, median|
loader.call(project_id, median)
end
rescue NotSupportedError
@@ -47,20 +42,42 @@ module Gitlab
end
end
+ def median_query(project_ids)
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+
+ median_datetime(cte_table, interval_query(project_ids), name)
+ end
+
def name
raise NotImplementedError.new("Expected #{self.name} to implement name")
end
+ def cte_table
+ Arel::Table.new("cte_table_for_#{name}")
+ end
+
+ def interval_query(project_ids)
+ Arel::Nodes::As.new(cte_table,
+ subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
+ end
+
private
def event_fetcher
- @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: @project,
+ @event_fetcher ||= Gitlab::CycleAnalytics::EventFetcher[name].new(project: project,
stage: name,
options: event_options)
end
def event_options
- @options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
+ options.merge(start_time_attrs: start_time_attrs, end_time_attrs: end_time_attrs)
+ end
+
+ def projects
+ [project]
end
end
end
diff --git a/lib/gitlab/cycle_analytics/code_event_fetcher.rb b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
index 6c348f1862d..9e7ca529579 100644
--- a/lib/gitlab/cycle_analytics/code_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/code_event_fetcher.rb
@@ -20,7 +20,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
+ AnalyticsMergeRequestSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class
diff --git a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
index 8a870f2e2a3..bb3520ae920 100644
--- a/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/issue_event_fetcher.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event)
+ AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class
diff --git a/lib/gitlab/cycle_analytics/issue_helper.rb b/lib/gitlab/cycle_analytics/issue_helper.rb
index c9266341378..ac836b8bf0f 100644
--- a/lib/gitlab/cycle_analytics/issue_helper.rb
+++ b/lib/gitlab/cycle_analytics/issue_helper.rb
@@ -7,7 +7,7 @@ module Gitlab
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
- .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
query
diff --git a/lib/gitlab/cycle_analytics/permissions.rb b/lib/gitlab/cycle_analytics/permissions.rb
index afefd09b614..03ba98b4dfb 100644
--- a/lib/gitlab/cycle_analytics/permissions.rb
+++ b/lib/gitlab/cycle_analytics/permissions.rb
@@ -23,7 +23,7 @@ module Gitlab
end
def get
- ::CycleAnalytics::STAGES.each do |stage|
+ ::CycleAnalytics::Base::STAGES.each do |stage|
@stage_permission_hash[stage] = authorized_stage?(stage)
end
diff --git a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
index d924f956dcd..49a6b099f34 100644
--- a/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/plan_event_fetcher.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event)
+ AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class
diff --git a/lib/gitlab/cycle_analytics/plan_helper.rb b/lib/gitlab/cycle_analytics/plan_helper.rb
index 30fc2ce6d40..ae578d45ad5 100644
--- a/lib/gitlab/cycle_analytics/plan_helper.rb
+++ b/lib/gitlab/cycle_analytics/plan_helper.rb
@@ -7,7 +7,7 @@ module Gitlab
query = issue_table.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
.project(issue_table[:project_id].as("project_id"))
.where(issue_table[:project_id].in(project_ids))
- .where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .where(issue_table[:created_at].gteq(options[:from]))
.where(issue_metrics_table[:first_added_to_board_at].not_eq(nil).or(issue_metrics_table[:first_associated_with_milestone_at].not_eq(nil)))
.where(issue_metrics_table[:first_mentioned_in_commit_at].not_eq(nil))
diff --git a/lib/gitlab/cycle_analytics/production_event_fetcher.rb b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
index 6bcbe0412a9..949119d69a0 100644
--- a/lib/gitlab/cycle_analytics/production_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/production_event_fetcher.rb
@@ -18,7 +18,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsIssueSerializer.new(project: @project).represent(event)
+ AnalyticsIssueSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
index aff65b150fb..778757a9ede 100644
--- a/lib/gitlab/cycle_analytics/production_helper.rb
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -6,7 +6,7 @@ module Gitlab
def stage_query(project_ids)
super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at]
- .gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .gteq(options[:from]))
end
end
end
diff --git a/lib/gitlab/cycle_analytics/review_event_fetcher.rb b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
index b6354b5ffad..d31736e755d 100644
--- a/lib/gitlab/cycle_analytics/review_event_fetcher.rb
+++ b/lib/gitlab/cycle_analytics/review_event_fetcher.rb
@@ -19,7 +19,7 @@ module Gitlab
private
def serialize(event)
- AnalyticsMergeRequestSerializer.new(project: @project).represent(event)
+ AnalyticsMergeRequestSerializer.new(project: project).represent(event)
end
def allowed_ids_finder_class
diff --git a/lib/gitlab/cycle_analytics/test_helper.rb b/lib/gitlab/cycle_analytics/test_helper.rb
index 32fca7fa898..d9124d62c7c 100644
--- a/lib/gitlab/cycle_analytics/test_helper.rb
+++ b/lib/gitlab/cycle_analytics/test_helper.rb
@@ -14,7 +14,7 @@ module Gitlab
private
def branch
- @branch ||= @options[:branch] # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ @branch ||= options[:branch]
end
end
end
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
index 913ee373f54..644300caead 100644
--- a/lib/gitlab/cycle_analytics/usage_data.rb
+++ b/lib/gitlab/cycle_analytics/usage_data.rb
@@ -32,7 +32,7 @@ module Gitlab
def medians_per_stage
projects.each_with_object({}) do |project, hsh|
- ::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
+ ::CycleAnalytics::ProjectLevel.new(project, options: options).all_medians_by_stage.each do |stage_name, median|
hsh[stage_name] ||= []
hsh[stage_name] << median
end
diff --git a/lib/gitlab/database_importers/common_metrics.rb b/lib/gitlab/database_importers/common_metrics.rb
new file mode 100644
index 00000000000..f964ae8a275
--- /dev/null
+++ b/lib/gitlab/database_importers/common_metrics.rb
@@ -0,0 +1,8 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module CommonMetrics
+ end
+ end
+end
diff --git a/lib/gitlab/database_importers/common_metrics/importer.rb b/lib/gitlab/database_importers/common_metrics/importer.rb
new file mode 100644
index 00000000000..6c61e05674e
--- /dev/null
+++ b/lib/gitlab/database_importers/common_metrics/importer.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module CommonMetrics
+ class Importer
+ MissingQueryId = Class.new(StandardError)
+
+ attr_reader :content
+
+ def initialize(filename = 'common_metrics.yml')
+ @content = YAML.load_file(Rails.root.join('config', 'prometheus', filename))
+ end
+
+ def execute
+ CommonMetrics::PrometheusMetric.reset_column_information
+
+ process_content do |id, attributes|
+ find_or_build_metric!(id)
+ .update!(**attributes)
+ end
+ end
+
+ private
+
+ def process_content(&blk)
+ content['panel_groups'].map do |group|
+ process_group(group, &blk)
+ end
+ end
+
+ def process_group(group, &blk)
+ attributes = {
+ group: find_group_title_key(group['group'])
+ }
+
+ group['panels'].map do |panel|
+ process_panel(panel, attributes, &blk)
+ end
+ end
+
+ def process_panel(panel, attributes, &blk)
+ attributes = attributes.merge(
+ title: panel['title'],
+ y_label: panel['y_label'])
+
+ panel['metrics'].map do |metric_details|
+ process_metric_details(metric_details, attributes, &blk)
+ end
+ end
+
+ def process_metric_details(metric_details, attributes, &blk)
+ attributes = attributes.merge(
+ legend: metric_details['label'],
+ query: metric_details['query_range'],
+ unit: metric_details['unit'])
+
+ yield(metric_details['id'], attributes)
+ end
+
+ def find_or_build_metric!(id)
+ raise MissingQueryId unless id
+
+ CommonMetrics::PrometheusMetric.common.find_by(identifier: id) ||
+ CommonMetrics::PrometheusMetric.new(common: true, identifier: id)
+ end
+
+ def find_group_title_key(title)
+ CommonMetrics::PrometheusMetricEnums.groups[find_group_title(title)]
+ end
+
+ def find_group_title(title)
+ CommonMetrics::PrometheusMetricEnums.group_titles.invert[title]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb
new file mode 100644
index 00000000000..b4a392cbea9
--- /dev/null
+++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module CommonMetrics
+ class PrometheusMetric < ApplicationRecord
+ enum group: PrometheusMetricEnums.groups
+ scope :common, -> { where(common: true) }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
new file mode 100644
index 00000000000..c9e957ec7c0
--- /dev/null
+++ b/lib/gitlab/database_importers/common_metrics/prometheus_metric_enums.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module DatabaseImporters
+ module CommonMetrics
+ module PrometheusMetricEnums
+ def self.groups
+ {
+ # built-in groups
+ nginx_ingress_vts: -1,
+ ha_proxy: -2,
+ aws_elb: -3,
+ nginx: -4,
+ kubernetes: -5,
+ nginx_ingress: -6,
+
+ # custom groups
+ business: 0,
+ response: 1,
+ system: 2
+ }
+ end
+
+ def self.group_titles
+ {
+ business: _('Business metrics (Custom)'),
+ response: _('Response metrics (Custom)'),
+ system: _('System metrics (Custom)'),
+ nginx_ingress_vts: _('Response metrics (NGINX Ingress VTS)'),
+ nginx_ingress: _('Response metrics (NGINX Ingress)'),
+ ha_proxy: _('Response metrics (HA Proxy)'),
+ aws_elb: _('Response metrics (AWS ELB)'),
+ nginx: _('Response metrics (NGINX)'),
+ kubernetes: _('System metrics (Kubernetes)')
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/metrics/dashboard/url.rb b/lib/gitlab/metrics/dashboard/url.rb
new file mode 100644
index 00000000000..b197e7ca86b
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/url.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+# Manages url matching for metrics dashboards.
+module Gitlab
+ module Metrics
+ module Dashboard
+ class Url
+ class << self
+ # Matches urls for a metrics dashboard. This could be
+ # either the /metrics endpoint or the /metrics_dashboard
+ # endpoint.
+ #
+ # EX - https://<host>/<namespace>/<project>/environments/<env_id>/metrics
+ def regex
+ %r{
+ (?<url>
+ #{Regexp.escape(Gitlab.config.gitlab.url)}
+ \/#{Project.reference_pattern}
+ (?:\/\-)?
+ \/environments
+ \/(?<environment>\d+)
+ \/metrics
+ (?<query>
+ \?[a-z0-9_=-]+
+ (&[a-z0-9_=-]+)*
+ )?
+ (?<anchor>\#[a-z0-9_-]+)?
+ )
+ }x
+ end
+
+ # Builds a metrics dashboard url based on the passed in arguments
+ def build_dashboard_url(*args)
+ Gitlab::Routing.url_helpers.metrics_dashboard_namespace_project_environment_url(*args)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/dashboard/milestones_spec.rb b/spec/features/dashboard/milestones_spec.rb
index 8fb2e37e269..c3310a4a132 100644
--- a/spec/features/dashboard/milestones_spec.rb
+++ b/spec/features/dashboard/milestones_spec.rb
@@ -29,5 +29,19 @@ describe 'Dashboard > Milestones' do
expect(page).to have_content(milestone.title)
expect(page).to have_content(group.name)
end
+
+ describe 'new milestones dropdown', :js do
+ it 'takes user to a new milestone page', :js do
+ find('.new-project-item-select-button').click
+
+ page.within('.select2-results') do
+ first('.select2-result-label').click
+ end
+
+ find('.new-project-item-link').click
+
+ expect(current_path).to eq(new_group_milestone_path(group))
+ end
+ end
end
end
diff --git a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
index fd307ce5ab3..47bdc677068 100644
--- a/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
+++ b/spec/frontend/confidential_merge_request/components/__snapshots__/project_form_group_spec.js.snap
@@ -2,7 +2,7 @@
exports[`Confidential merge request project form group component renders empty state when response is empty 1`] = `
<div
- class="form-group"
+ class="confidential-merge-request-fork-group form-group"
>
<label>
Project
@@ -28,6 +28,23 @@ exports[`Confidential merge request project form group component renders empty s
</a>
and set the forks visiblity to private.
</span>
+
+ <gllink-stub
+ class="w-auto p-0 d-inline-block text-primary bg-transparent"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
</p>
</div>
</div>
@@ -35,7 +52,7 @@ exports[`Confidential merge request project form group component renders empty s
exports[`Confidential merge request project form group component renders fork dropdown 1`] = `
<div
- class="form-group"
+ class="confidential-merge-request-fork-group form-group"
>
<label>
Project
@@ -61,6 +78,23 @@ exports[`Confidential merge request project form group component renders fork dr
</a>
and set the forks visiblity to private.
</span>
+
+ <gllink-stub
+ class="w-auto p-0 d-inline-block text-primary bg-transparent"
+ href="/help"
+ target="_blank"
+ >
+ <span
+ class="sr-only"
+ >
+ Read more
+ </span>
+
+ <i
+ aria-hidden="true"
+ class="fa fa-question-circle"
+ />
+ </gllink-stub>
</p>
</div>
</div>
diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js
index 42233f82d54..8c5bfd08beb 100644
--- a/spec/frontend/error_tracking_settings/mock.js
+++ b/spec/frontend/error_tracking_settings/mock.js
@@ -1,5 +1,5 @@
import createStore from '~/error_tracking_settings/store';
-import { TEST_HOST } from '../helpers/test_constants';
+import { TEST_HOST } from 'helpers/test_constants';
const defaultStore = createStore();
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index c97ff5236ec..c461c28a37b 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -18,6 +18,8 @@ import {
noteableDataMock,
individualNote,
} from '../mock_data';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
const TEST_ERROR_MESSAGE = 'Test error message';
@@ -335,28 +337,24 @@ describe('Actions Notes Store', () => {
});
describe('deleteNote', () => {
- const interceptor = (request, next) => {
- next(
- request.respondWith(JSON.stringify({}), {
- status: 200,
- }),
- );
- };
+ const endpoint = `${TEST_HOST}/note`;
+ let axiosMock;
beforeEach(() => {
- Vue.http.interceptors.push(interceptor);
+ axiosMock = new AxiosMockAdapter(axios);
+ axiosMock.onDelete(endpoint).replyOnce(200, {});
$('body').attr('data-page', '');
});
afterEach(() => {
- Vue.http.interceptors = _.without(Vue.http.interceptors, interceptor);
+ axiosMock.restore();
$('body').attr('data-page', '');
});
it('commits DELETE_NOTE and dispatches updateMergeRequestWidget', done => {
- const note = { path: `${gl.TEST_HOST}`, id: 1 };
+ const note = { path: endpoint, id: 1 };
testAction(
actions.deleteNote,
@@ -381,7 +379,7 @@ describe('Actions Notes Store', () => {
});
it('dispatches removeDiscussionsFromDiff on merge request page', done => {
- const note = { path: `${gl.TEST_HOST}`, id: 1 };
+ const note = { path: endpoint, id: 1 };
$('body').attr('data-page', 'projects:merge_requests:show');
diff --git a/spec/lib/banzai/filter/inline_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
new file mode 100644
index 00000000000..772c94e3180
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_metrics_filter_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::InlineMetricsFilter do
+ include FilterSpecHelper
+
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the document has an external link' do
+ let(:url) { 'https://foo.com' }
+
+ it 'leaves regular non-metrics links unchanged' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'when the document has a metrics dashboard link' do
+ let(:params) { ['foo', 'bar', 12] }
+ let(:url) { urls.metrics_namespace_project_environment_url(*params) }
+
+ it 'leaves the original link unchanged' do
+ expect(doc.at_css('a').to_s).to eq input
+ end
+
+ it 'appends a metrics charts placeholder with dashboard url after metrics links' do
+ node = doc.at_css('.js-render-metrics')
+ expect(node).to be_present
+
+ dashboard_url = urls.metrics_dashboard_namespace_project_environment_url(*params, embedded: true)
+ expect(node.attribute('data-dashboard-url').to_s).to eq dashboard_url
+ end
+
+ context 'when the metrics dashboard link is part of a paragraph' do
+ let(:paragraph) { %(This is an <a href="#{url}">example</a> of metrics.) }
+ let(:input) { %(<p>#{paragraph}</p>) }
+
+ it 'appends the charts placeholder after the enclosing paragraph' do
+ expect(doc.at_css('p').to_s).to include paragraph
+ expect(doc.at_css('.js-render-metrics')).to be_present
+ end
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(gfm_embedded_metrics: false)
+ end
+
+ it 'does nothing' do
+ expect(doc.to_s).to eq input
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
new file mode 100644
index 00000000000..fb2186e9d12
--- /dev/null
+++ b/spec/lib/banzai/filter/inline_metrics_redactor_filter_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Banzai::Filter::InlineMetricsRedactorFilter do
+ include FilterSpecHelper
+
+ set(:project) { create(:project) }
+
+ let(:url) { urls.metrics_dashboard_project_environment_url(project, 1, embedded: true) }
+ let(:input) { %(<a href="#{url}">example</a>) }
+ let(:doc) { filter(input) }
+
+ context 'when the feature is disabled' do
+ before do
+ stub_feature_flags(gfm_embedded_metrics: false)
+ end
+
+ it 'does nothing' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'without a metrics charts placeholder' do
+ it 'leaves regular non-metrics links unchanged' do
+ expect(doc.to_s).to eq input
+ end
+ end
+
+ context 'with a metrics charts placeholder' do
+ let(:input) { %(<div class="js-render-metrics" data-dashboard-url="#{url}"></div>) }
+
+ context 'no user is logged in' do
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user does not have permission do see charts' do
+ let(:doc) { filter(input, current_user: build(:user)) }
+
+ it 'redacts the placeholder' do
+ expect(doc.to_s).to be_empty
+ end
+ end
+
+ context 'the user has requisite permissions' do
+ let(:user) { create(:user) }
+ let(:doc) { filter(input, current_user: user) }
+
+ it 'leaves the placeholder' do
+ project.add_maintainer(user)
+
+ expect(doc.to_s).to eq input
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index f8b103c0fab..5ee02650e49 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -7,7 +7,7 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
let(:events) do
- CycleAnalytics.new(project, { from: from_date, current_user: user })[stage].events
+ CycleAnalytics::ProjectLevel.new(project, options: { from: from_date, current_user: user })[stage].events
end
before do
diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
index a785b17f682..8122e85a981 100644
--- a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
@@ -34,7 +34,7 @@ describe Gitlab::CycleAnalytics::UsageData do
expect(result).to have_key(:avg_cycle_analytics)
- CycleAnalytics::STAGES.each do |stage|
+ CycleAnalytics::Base::STAGES.each do |stage|
expect(result[:avg_cycle_analytics]).to have_key(stage)
stage_values = result[:avg_cycle_analytics][stage]
diff --git a/spec/db/importers/common_metrics_importer_spec.rb b/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb
index a717c8cd04d..57c8bafd488 100644
--- a/spec/db/importers/common_metrics_importer_spec.rb
+++ b/spec/lib/gitlab/database_importers/common_metrics/importer_spec.rb
@@ -1,25 +1,8 @@
# frozen_string_literal: true
require 'rails_helper'
-require Rails.root.join("db", "importers", "common_metrics_importer.rb")
-describe Importers::PrometheusMetric do
- let(:existing_group_titles) do
- ::PrometheusMetric::GROUP_DETAILS.each_with_object({}) do |(key, value), memo|
- memo[key] = value[:group_title]
- end
- end
-
- it 'group enum equals ::PrometheusMetric' do
- expect(described_class.groups).to eq(::PrometheusMetric.groups)
- end
-
- it 'GROUP_TITLES equals ::PrometheusMetric' do
- expect(described_class::GROUP_TITLES).to eq(existing_group_titles)
- end
-end
-
-describe Importers::CommonMetricsImporter do
+describe Gitlab::DatabaseImporters::CommonMetrics::Importer do
subject { described_class.new }
context "does import common_metrics.yml" do
@@ -104,7 +87,7 @@ describe Importers::CommonMetricsImporter do
let(:query_identifier) { }
it 'raises exception' do
- expect { subject.execute }.to raise_error(described_class::MissingQueryId)
+ expect { subject.execute }.to raise_error(Gitlab::DatabaseImporters::CommonMetrics::Importer::MissingQueryId)
end
end
diff --git a/spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb b/spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb
new file mode 100644
index 00000000000..94f544e59b3
--- /dev/null
+++ b/spec/lib/gitlab/database_importers/common_metrics/prometheus_metric_spec.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+describe Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetric do
+ it 'group enum equals ::PrometheusMetric' do
+ expect(described_class.groups).to eq(::PrometheusMetric.groups)
+ end
+
+ it '.group_titles equals ::PrometheusMetric' do
+ existing_group_titles = ::PrometheusMetricEnums.group_details.each_with_object({}) do |(key, value), memo|
+ memo[key] = value[:group_title]
+ end
+ expect(Gitlab::DatabaseImporters::CommonMetrics::PrometheusMetricEnums.group_titles).to eq(existing_group_titles)
+ end
+end
diff --git a/spec/lib/gitlab/metrics/dashboard/url_spec.rb b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
new file mode 100644
index 00000000000..34bc6359414
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/url_spec.rb
@@ -0,0 +1,56 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Url do
+ describe '#regex' do
+ it 'returns a regular expression' do
+ expect(described_class.regex).to be_a Regexp
+ end
+
+ it 'matches a metrics dashboard link with named params' do
+ url = Gitlab::Routing.url_helpers.metrics_namespace_project_environment_url('foo', 'bar', 1, start: 123345456, anchor: 'title')
+
+ expected_params = {
+ 'url' => url,
+ 'namespace' => 'foo',
+ 'project' => 'bar',
+ 'environment' => '1',
+ 'query' => '?start=123345456',
+ 'anchor' => '#title'
+ }
+
+ expect(described_class.regex).to match url
+
+ described_class.regex.match(url) do |m|
+ expect(m.named_captures).to eq expected_params
+ end
+ end
+
+ it 'does not match other gitlab urls that contain the term metrics' do
+ url = Gitlab::Routing.url_helpers.active_common_namespace_project_prometheus_metrics_url('foo', 'bar', :json)
+
+ expect(described_class.regex).not_to match url
+ end
+
+ it 'does not match other gitlab urls' do
+ url = Gitlab.config.gitlab.url
+
+ expect(described_class.regex).not_to match url
+ end
+
+ it 'does not match non-gitlab urls' do
+ url = 'https://www.super_awesome_site.com/'
+
+ expect(described_class.regex).not_to match url
+ end
+ end
+
+ describe '#build_dashboard_url' do
+ it 'builds the url for the dashboard endpoint' do
+ url = described_class.build_dashboard_url('foo', 'bar', 1)
+
+ expect(url).to match described_class.regex
+ end
+ end
+end
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index b22a0340015..db6e70973ae 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#code' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
context 'with deployment' do
generate_cycle_analytics_spec(
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 07d60be091a..7b18e24a351 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#issue' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :issue,
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 3d22a284264..73fc98ba6cf 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#plan' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :plan,
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index 383727cd8f7..ddd199362d1 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#production' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :production,
diff --git a/spec/models/cycle_analytics_spec.rb b/spec/models/cycle_analytics/project_level_spec.rb
index 5d8b5b573cf..77bd0bfeb9c 100644
--- a/spec/models/cycle_analytics_spec.rb
+++ b/spec/models/cycle_analytics/project_level_spec.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-describe CycleAnalytics do
+describe CycleAnalytics::ProjectLevel do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
@@ -11,9 +11,9 @@ describe CycleAnalytics do
let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
- subject { described_class.new(project, from: from_date) }
+ subject { described_class.new(project, options: { from: from_date }) }
- describe '#all_medians_per_stage' do
+ describe '#all_medians_by_stage' do
before do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
@@ -26,7 +26,7 @@ describe CycleAnalytics do
hsh[stage_name] = subject[stage_name].median.presence
end
- expect(subject.all_medians_per_stage).to eq(values)
+ expect(subject.all_medians_by_stage).to eq(values)
end
end
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 1af5f9cc1f4..63c481ed465 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#review' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :review,
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index 8375944f03c..c134b97553f 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -9,7 +9,7 @@ describe 'CycleAnalytics#staging' do
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :staging,
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index b78258df564..a6ea73b2699 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -8,7 +8,8 @@ describe 'CycleAnalytics#test' do
let(:project) { create(:project, :repository) }
let(:from_date) { 10.days.ago }
let(:user) { create(:user, :admin) }
- subject { CycleAnalytics.new(project, from: from_date) }
+
+ subject { CycleAnalytics::ProjectLevel.new(project, options: { from: from_date }) }
generate_cycle_analytics_spec(
phase: :test,
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 6dde40d1cb6..8a3de2a52fc 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -713,4 +713,10 @@ describe 'project routing' do
end
end
end
+
+ describe Projects::DeployTokensController, 'routing' do
+ it 'routes to deploy_tokens#revoke' do
+ expect(put("/gitlab/gitlabhq/-/deploy_tokens/1/revoke")).to route_to("projects/deploy_tokens#revoke", namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
+ end
+ end
end