summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/CODEOWNERS1
-rw-r--r--.gitlab/ci/review.gitlab-ci.yml5
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md2
-rw-r--r--app/services/create_branch_service.rb2
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb157
-rw-r--r--app/views/admin/sessions/_new_base.html.haml2
-rw-r--r--app/views/admin/sessions/_tabs_normal.html.haml2
-rw-r--r--app/views/admin/sessions/new.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml27
-rw-r--r--changelogs/unreleased/34299-enable-color-chip-asciidoc.yml5
-rw-r--r--changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml5
-rw-r--r--changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml5
-rw-r--r--lib/banzai/pipeline/ascii_doc_pipeline.rb1
-rw-r--r--lib/gitlab/metrics/dashboard/errors.rb5
-rw-r--r--lib/gitlab/metrics/dashboard/processor.rb3
-rw-r--r--lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb224
-rw-r--r--lib/grafana/client.rb14
-rw-r--r--locale/gitlab.pot8
-rw-r--r--spec/factories/grafana_integrations.rb2
-rw-r--r--spec/features/admin/admin_settings_spec.rb31
-rw-r--r--spec/fixtures/grafana/dashboard_response.json764
-rw-r--r--spec/fixtures/grafana/datasource_response.json21
-rw-r--r--spec/fixtures/grafana/expected_grafana_embed.json27
-rw-r--r--spec/fixtures/grafana/simplified_dashboard_response.json40
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json1
-rw-r--r--spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json1
-rw-r--r--spec/lib/gitlab/metrics/dashboard/processor_spec.rb8
-rw-r--r--spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb106
-rw-r--r--spec/lib/grafana/client_spec.rb26
-rw-r--r--spec/requests/api/branches_spec.rb2
-rw-r--r--spec/services/create_branch_service_spec.rb15
-rw-r--r--spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb177
-rw-r--r--spec/support/helpers/grafana_api_helpers.rb32
-rw-r--r--spec/support/helpers/login_helpers.rb2
34 files changed, 1697 insertions, 28 deletions
diff --git a/.gitlab/CODEOWNERS b/.gitlab/CODEOWNERS
index a02740373da..ddfdf72cf99 100644
--- a/.gitlab/CODEOWNERS
+++ b/.gitlab/CODEOWNERS
@@ -32,4 +32,5 @@ lib/gitlab/github_import/ @gitlab-org/maintainers/database
/.gitlab/ci/ @gl-quality/eng-prod
Dangerfile @gl-quality/eng-prod
/danger/ @gl-quality/eng-prod
+/lib/gitlab/danger/ @gl-quality/eng-prod
/scripts/ @gl-quality/eng-prod
diff --git a/.gitlab/ci/review.gitlab-ci.yml b/.gitlab/ci/review.gitlab-ci.yml
index ad516aba7c3..09cf38908a6 100644
--- a/.gitlab/ci/review.gitlab-ci.yml
+++ b/.gitlab/ci/review.gitlab-ci.yml
@@ -97,7 +97,10 @@ schedule:review-build-cng:
variables:
HOST_SUFFIX: "${CI_ENVIRONMENT_SLUG}"
DOMAIN: "-${CI_ENVIRONMENT_SLUG}.${REVIEW_APPS_DOMAIN}"
- GITLAB_HELM_CHART_REF: "v2.3.7"
+ # v2.3.7 + some stability improvements not yet released:
+ # - sidekiq readinessProbe should be `pgrep -f sidekiq`: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/991
+ # - Allows livenessProbe and readinessProbe to be configured for unicorn: https://gitlab.com/gitlab-org/charts/gitlab/merge_requests/985
+ GITLAB_HELM_CHART_REF: "df7c52dc69df441909880b8f2fd15e938cdb2047"
GITLAB_EDITION: "ce"
environment:
name: review/${CI_COMMIT_REF_NAME}
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index 3e634de4f0c..e06a6fb0cff 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -29,7 +29,7 @@ Set the title to: `Description of the original issue`
#### Documentation and final details
-- [ ] Check the topic on #security to see when the next release is going to happen and add a link to the [links section](#links)
+- [ ] Check the topic on #releases to see when the next release is going to happen and add a link to the [links section](#links)
- [ ] Add links to this issue and your MRs in the description of the security release issue
- [ ] Find out the versions affected (the Git history of the files affected may help you with this) and add them to the [details section](#details)
- [ ] Fill in any upgrade notes that users may need to take into account in the [details section](#details)
diff --git a/app/services/create_branch_service.rb b/app/services/create_branch_service.rb
index 110e589e30d..d58cb0f9e2b 100644
--- a/app/services/create_branch_service.rb
+++ b/app/services/create_branch_service.rb
@@ -14,7 +14,7 @@ class CreateBranchService < BaseService
if new_branch
success(new_branch)
else
- error('Invalid reference name')
+ error("Invalid reference name: #{branch_name}")
end
rescue Gitlab::Git::PreReceiveError => ex
error(ex.message)
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
new file mode 100644
index 00000000000..f302a786ba8
--- /dev/null
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -0,0 +1,157 @@
+# frozen_string_literal: true
+
+# Responsible for returning a gitlab-compatible dashboard
+# containing info based on a grafana dashboard and datasource.
+#
+# Use Gitlab::Metrics::Dashboard::Finder to retrive dashboards.
+module Metrics
+ module Dashboard
+ class GrafanaMetricEmbedService < ::Metrics::Dashboard::BaseService
+ include ReactiveCaching
+
+ SEQUENCE = [
+ ::Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter
+ ].freeze
+
+ self.reactive_cache_key = ->(service) { service.cache_key }
+ self.reactive_cache_lease_timeout = 30.seconds
+ self.reactive_cache_refresh_interval = 30.minutes
+ self.reactive_cache_lifetime = 30.minutes
+ self.reactive_cache_worker_finder = ->(_id, *args) { from_cache(*args) }
+
+ class << self
+ # Determines whether the provided params are sufficient
+ # to uniquely identify a grafana dashboard.
+ def valid_params?(params)
+ [
+ params[:embedded],
+ params[:grafana_url]
+ ].all?
+ end
+
+ def from_cache(project_id, user_id, grafana_url)
+ project = Project.find(project_id)
+ user = User.find(user_id)
+
+ new(project, user, grafana_url: grafana_url)
+ end
+ end
+
+ def get_dashboard
+ with_reactive_cache(*cache_key) { |result| result }
+ end
+
+ # Inherits the primary logic from the parent class and
+ # maintains the service's API while including ReactiveCache
+ def calculate_reactive_cache(*)
+ ::Metrics::Dashboard::BaseService
+ .instance_method(:get_dashboard)
+ .bind(self)
+ .call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
+ end
+
+ def cache_key(*args)
+ [project.id, current_user.id, grafana_url]
+ end
+
+ # Required for ReactiveCaching; Usage overridden by
+ # self.reactive_cache_worker_finder
+ def id
+ nil
+ end
+
+ private
+
+ def get_raw_dashboard
+ raise MissingIntegrationError unless client
+
+ grafana_dashboard = fetch_dashboard
+ datasource = fetch_datasource(grafana_dashboard)
+
+ params.merge!(grafana_dashboard: grafana_dashboard, datasource: datasource)
+
+ {}
+ end
+
+ def fetch_dashboard
+ uid = GrafanaUidParser.new(grafana_url, project).parse
+ raise DashboardProcessingError.new('Dashboard uid not found') unless uid
+
+ response = client.get_dashboard(uid: uid)
+
+ parse_json(response.body)
+ end
+
+ def fetch_datasource(dashboard)
+ name = DatasourceNameParser.new(grafana_url, dashboard).parse
+ raise DashboardProcessingError.new('Datasource name not found') unless name
+
+ response = client.get_datasource(name: name)
+
+ parse_json(response.body)
+ end
+
+ def grafana_url
+ params[:grafana_url]
+ end
+
+ def client
+ project.grafana_integration&.client
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :read_project, project)
+ end
+
+ def sequence
+ SEQUENCE
+ end
+
+ def parse_json(json)
+ JSON.parse(json, symbolize_names: true)
+ rescue JSON::ParserError
+ raise DashboardProcessingError.new('Grafana response contains invalid json')
+ end
+ end
+
+ # Identifies the uid of the dashboard based on url format
+ class GrafanaUidParser
+ def initialize(grafana_url, project)
+ @grafana_url, @project = grafana_url, project
+ end
+
+ def parse
+ @grafana_url.match(uid_regex) { |m| m.named_captures['uid'] }
+ end
+
+ private
+
+ # URLs are expected to look like https://domain.com/d/:uid/other/stuff
+ def uid_regex
+ base_url = @project.grafana_integration.grafana_url.chomp('/')
+
+ %r{(#{Regexp.escape(base_url)}\/d\/(?<uid>\w+)\/)}x
+ end
+ end
+
+ # Identifies the name of the datasource for a dashboard
+ # based on the panelId query parameter found in the url
+ class DatasourceNameParser
+ def initialize(grafana_url, grafana_dashboard)
+ @grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard
+ end
+
+ def parse
+ @grafana_dashboard[:dashboard][:panels]
+ .find { |panel| panel[:id].to_s == query_params[:panelId] }
+ .try(:[], :datasource)
+ end
+
+ private
+
+ def query_params
+ Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url)
+ end
+ end
+ end
+end
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index 55aea0296e7..3d77a439d61 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -4,4 +4,4 @@
= password_field_tag :password, nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
- = submit_tag _('Enter admin mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
+ = submit_tag _('Enter Admin Mode'), class: 'btn btn-success', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/admin/sessions/_tabs_normal.html.haml b/app/views/admin/sessions/_tabs_normal.html.haml
index f5dedb5ad76..20830051d31 100644
--- a/app/views/admin/sessions/_tabs_normal.html.haml
+++ b/app/views/admin/sessions/_tabs_normal.html.haml
@@ -1,3 +1,3 @@
%ul.nav-links.new-session-tabs.nav-tabs.nav{ role: 'tablist' }
%li.nav-item{ role: 'presentation' }
- %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter admin mode')
+ %a.nav-link.active{ href: '#login-pane', data: { toggle: 'tab', qa_selector: 'sign_in_tab' }, role: 'tab' }= _('Enter Admin Mode')
diff --git a/app/views/admin/sessions/new.html.haml b/app/views/admin/sessions/new.html.haml
index ee06b4a1741..73028e78ea5 100644
--- a/app/views/admin/sessions/new.html.haml
+++ b/app/views/admin/sessions/new.html.haml
@@ -1,5 +1,5 @@
- @hide_breadcrumbs = true
-- page_title _('Enter admin mode')
+- page_title _('Enter Admin Mode')
.row.justify-content-center
.col-6.new-session-forms-container
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5122c2517aa..d339751848b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -55,15 +55,15 @@
= nav_link(controller: 'admin/dashboard') do
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link d-xl-none' do
= _('Admin Area')
- - if Feature.enabled?(:user_mode_in_session)
- - if header_link?(:admin_mode)
- = nav_link(controller: 'admin/sessions') do
- = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
- = _('Leave admin mode')
- - elsif current_user.admin?
- = nav_link(controller: 'admin/sessions') do
- = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
- = _('Enter admin mode')
+ - if Feature.enabled?(:user_mode_in_session)
+ - if header_link?(:admin_mode)
+ = nav_link(controller: 'admin/sessions') do
+ = link_to destroy_admin_session_path, class: 'd-lg-none lock-open-icon' do
+ = _('Leave Admin Mode')
+ - elsif current_user.admin?
+ = nav_link(controller: 'admin/sessions') do
+ = link_to new_admin_session_path, class: 'd-lg-none lock-icon' do
+ = _('Enter Admin Mode')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, class: 'admin-icon' do
@@ -74,6 +74,15 @@
= link_to admin_root_path, class: 'admin-icon qa-admin-area-link', title: _('Admin Area'), aria: { label: _('Admin Area') }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= sprite_icon('admin', size: 18)
+ - if Feature.enabled?(:user_mode_in_session)
+ - if header_link?(:admin_mode)
+ = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+ = link_to destroy_admin_session_path, title: _('Leave Admin Mode'), aria: { label: _('Leave Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = sprite_icon('lock-open', size: 18)
+ - elsif current_user.admin?
+ = nav_link(controller: 'admin/sessions', html_options: { class: "d-none d-lg-block d-xl-block"}) do
+ = link_to new_admin_session_path, title: _('Enter Admin Mode'), aria: { label: _('Enter Admin Mode') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body' } do
+ = sprite_icon('lock', size: 18)
-# Shortcut to Dashboard > Projects
- if dashboard_nav_link?(:projects)
diff --git a/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
new file mode 100644
index 00000000000..546e6bc6b63
--- /dev/null
+++ b/changelogs/unreleased/34299-enable-color-chip-asciidoc.yml
@@ -0,0 +1,5 @@
+---
+title: Enable the color chip in AsciiDoc documents
+merge_request: 18723
+author:
+type: added
diff --git a/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
new file mode 100644
index 00000000000..b727bc7f85e
--- /dev/null
+++ b/changelogs/unreleased/34320-error-when-uploading-a-few-designs-in-a-row.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Error when uploading a few designs in a row
+merge_request: 18811
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
new file mode 100644
index 00000000000..4014b5e7ab2
--- /dev/null
+++ b/changelogs/unreleased/fix-admin-mode-ui-buttons-missing-on-small-screens.yml
@@ -0,0 +1,5 @@
+---
+title: Fix missing admin mode UI buttons on bigger screen sizes
+merge_request: 18585
+author: Diego Louzán
+type: fixed
diff --git a/lib/banzai/pipeline/ascii_doc_pipeline.rb b/lib/banzai/pipeline/ascii_doc_pipeline.rb
index 82b99d3de4a..2e8d2bd23b0 100644
--- a/lib/banzai/pipeline/ascii_doc_pipeline.rb
+++ b/lib/banzai/pipeline/ascii_doc_pipeline.rb
@@ -10,6 +10,7 @@ module Banzai
Filter::SyntaxHighlightFilter,
Filter::ExternalLinkFilter,
Filter::PlantumlFilter,
+ Filter::ColorFilter,
Filter::AsciiDocPostProcessingFilter
]
end
diff --git a/lib/gitlab/metrics/dashboard/errors.rb b/lib/gitlab/metrics/dashboard/errors.rb
index d41bd2c43c7..264ea0488e7 100644
--- a/lib/gitlab/metrics/dashboard/errors.rb
+++ b/lib/gitlab/metrics/dashboard/errors.rb
@@ -9,6 +9,7 @@ module Gitlab
module Errors
DashboardProcessingError = Class.new(StandardError)
PanelNotFoundError = Class.new(StandardError)
+ MissingIntegrationError = Class.new(StandardError)
LayoutError = Class.new(DashboardProcessingError)
MissingQueryError = Class.new(DashboardProcessingError)
@@ -22,6 +23,10 @@ module Gitlab
error("#{dashboard_path} could not be found.", :not_found)
when PanelNotFoundError
error(error.message, :not_found)
+ when ::Grafana::Client::Error
+ error(error.message, :service_unavailable)
+ when MissingIntegrationError
+ error('Proxy support for this API is not available currently', :bad_request)
else
raise error
end
diff --git a/lib/gitlab/metrics/dashboard/processor.rb b/lib/gitlab/metrics/dashboard/processor.rb
index bfdee76a818..9566e5afb9a 100644
--- a/lib/gitlab/metrics/dashboard/processor.rb
+++ b/lib/gitlab/metrics/dashboard/processor.rb
@@ -17,7 +17,10 @@ module Gitlab
# Returns a new dashboard hash with the results of
# running transforms on the dashboard.
+ # @return [Hash, nil]
def process
+ return unless @dashboard
+
@dashboard.deep_symbolize_keys.tap do |dashboard|
@sequence.each do |stage|
stage.new(@project, dashboard, @params).transform!
diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
new file mode 100644
index 00000000000..ce75c54d014
--- /dev/null
+++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb
@@ -0,0 +1,224 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Metrics
+ module Dashboard
+ module Stages
+ class GrafanaFormatter < BaseStage
+ include Gitlab::Utils::StrongMemoize
+
+ CHART_TYPE = 'area-chart'
+ PROXY_PATH = 'api/v1/query_range'
+
+ # Reformats the specified panel in the Gitlab
+ # dashboard-yml format
+ def transform!
+ InputFormatValidator.new(
+ grafana_dashboard,
+ datasource,
+ panel,
+ query_params
+ ).validate!
+
+ new_dashboard = formatted_dashboard
+
+ dashboard.clear
+ dashboard.merge!(new_dashboard)
+ end
+
+ private
+
+ def formatted_dashboard
+ { panel_groups: [{ panels: [formatted_panel] }] }
+ end
+
+ def formatted_panel
+ {
+ title: panel[:title],
+ type: CHART_TYPE,
+ y_label: '', # Grafana panels do not include a Y-Axis label
+ metrics: panel[:targets].map.with_index do |target, idx|
+ formatted_metric(target, idx)
+ end
+ }
+ end
+
+ def formatted_metric(metric, idx)
+ {
+ id: "#{metric[:legendFormat]}_#{idx}",
+ query_range: format_query(metric),
+ label: replace_variables(metric[:legendFormat]),
+ prometheus_endpoint_path: prometheus_endpoint_for_metric(metric)
+ }.compact
+ end
+
+ # Panel specified by the url from the Grafana dashboard
+ def panel
+ strong_memoize(:panel) do
+ grafana_dashboard[:dashboard][:panels].find do |panel|
+ panel[:id].to_s == query_params[:panelId]
+ end
+ end
+ end
+
+ # Grafana url query parameters. Includes information
+ # on which panel to select and time range.
+ def query_params
+ strong_memoize(:query_params) do
+ Gitlab::Metrics::Dashboard::Url.parse_query(grafana_url)
+ end
+ end
+
+ # Endpoint which will return prometheus metric data
+ # for the metric
+ def prometheus_endpoint_for_metric(metric)
+ Gitlab::Routing.url_helpers.project_grafana_api_path(
+ project,
+ datasource_id: datasource[:id],
+ proxy_path: PROXY_PATH,
+ query: format_query(metric)
+ )
+ end
+
+ # Reformats query for compatibility with prometheus api.
+ def format_query(metric)
+ expression = remove_new_lines(metric[:expr])
+ expression = replace_variables(expression)
+ expression = replace_global_variables(expression, metric)
+
+ expression
+ end
+
+ # Accomodates instance-defined Grafana variables.
+ # These are variables defined by users, and values
+ # must be provided in the query parameters.
+ def replace_variables(expression)
+ return expression unless grafana_dashboard[:dashboard][:templating]
+
+ grafana_dashboard[:dashboard][:templating][:list]
+ .sort_by { |variable| variable[:name].length }
+ .each do |variable|
+ variable_value = query_params[:"var-#{variable[:name]}"]
+
+ expression = expression.gsub("$#{variable[:name]}", variable_value)
+ expression = expression.gsub("[[#{variable[:name]}]]", variable_value)
+ expression = expression.gsub("{{#{variable[:name]}}}", variable_value)
+ end
+
+ expression
+ end
+
+ # Replaces Grafana global built-in variables with values.
+ # Only $__interval and $__from and $__to are supported.
+ #
+ # See https://grafana.com/docs/reference/templating/#global-built-in-variables
+ def replace_global_variables(expression, metric)
+ expression = expression.gsub('$__interval', metric[:interval]) if metric[:interval]
+ expression = expression.gsub('$__from', query_params[:from])
+ expression = expression.gsub('$__to', query_params[:to])
+
+ expression
+ end
+
+ # Removes new lines from expression.
+ def remove_new_lines(expression)
+ expression.gsub(/\R+/, '')
+ end
+
+ # Grafana datasource object corresponding to the
+ # specified dashboard
+ def datasource
+ params[:datasource]
+ end
+
+ # The specified Grafana dashboard
+ def grafana_dashboard
+ params[:grafana_dashboard]
+ end
+
+ # The URL specifying which Grafana panel to embed
+ def grafana_url
+ params[:grafana_url]
+ end
+ end
+
+ class InputFormatValidator
+ include ::Gitlab::Metrics::Dashboard::Errors
+
+ attr_reader :grafana_dashboard, :datasource, :panel, :query_params
+
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS = %w(
+ $__interval_ms
+ $__timeFilter
+ $__name
+ $timeFilter
+ $interval
+ ).freeze
+
+ def initialize(grafana_dashboard, datasource, panel, query_params)
+ @grafana_dashboard = grafana_dashboard
+ @datasource = datasource
+ @panel = panel
+ @query_params = query_params
+ end
+
+ def validate!
+ validate_query_params!
+ validate_datasource!
+ validate_panel_type!
+ validate_variable_definitions!
+ validate_global_variables!
+ end
+
+ private
+
+ def validate_datasource!
+ return if datasource[:access] == 'proxy' && datasource[:type] == 'prometheus'
+
+ raise_error 'Only Prometheus datasources with proxy access in Grafana are supported.'
+ end
+
+ def validate_query_params!
+ return if [:panelId, :from, :to].all? { |param| query_params.include?(param) }
+
+ raise_error 'Grafana query parameters must include panelId, from, and to.'
+ end
+
+ def validate_panel_type!
+ return if panel[:type] == 'graph' && panel[:lines]
+
+ raise_error 'Panel type must be a line graph.'
+ end
+
+ def validate_variable_definitions!
+ return unless grafana_dashboard[:dashboard][:templating]
+
+ return if grafana_dashboard[:dashboard][:templating][:list].all? do |variable|
+ query_params[:"var-#{variable[:name]}"].present?
+ end
+
+ raise_error 'All Grafana variables must be defined in the query parameters.'
+ end
+
+ def validate_global_variables!
+ return unless panel_contains_unsupported_vars?
+
+ raise_error 'Prometheus must not include'
+ end
+
+ def panel_contains_unsupported_vars?
+ panel[:targets].any? do |target|
+ UNSUPPORTED_GRAFANA_GLOBAL_VARS.any? do |variable|
+ target[:expr].include?(variable)
+ end
+ end
+ end
+
+ def raise_error(message)
+ raise DashboardProcessingError.new(message)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/grafana/client.rb b/lib/grafana/client.rb
index 0765630f9bb..b419f79bace 100644
--- a/lib/grafana/client.rb
+++ b/lib/grafana/client.rb
@@ -11,6 +11,18 @@ module Grafana
@token = token
end
+ # @param uid [String] Unique identifier for a Grafana dashboard
+ def get_dashboard(uid:)
+ http_get("#{@api_url}/api/dashboards/uid/#{uid}")
+ end
+
+ # @param name [String] Unique identifier for a Grafana datasource
+ def get_datasource(name:)
+ # CGI#escape formats strings such that the Grafana endpoint
+ # will not recognize the dashboard name. Preferring URI#escape.
+ http_get("#{@api_url}/api/datasources/name/#{URI.escape(name)}") # rubocop:disable Lint/UriEscapeUnescape
+ end
+
# @param datasource_id [String] Grafana ID for the datasource
# @param proxy_path [String] Path to proxy - ex) 'api/v1/query_range'
def proxy_datasource(datasource_id:, proxy_path:, query: {})
@@ -57,7 +69,7 @@ module Grafana
def handle_response(response)
return response if response.code == 200
- raise_error "Grafana response status code: #{response.code}"
+ raise_error "Grafana response status code: #{response.code}, Message: #{response.body}"
end
def raise_error(message)
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 74d52dad839..e3860825b73 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -6099,13 +6099,13 @@ msgstr ""
msgid "Ensure your %{linkStart}environment is part of the deploy stage%{linkEnd} of your CI pipeline to track deployments to your cluster."
msgstr ""
-msgid "Enter IP address range"
+msgid "Enter Admin Mode"
msgstr ""
-msgid "Enter a number"
+msgid "Enter IP address range"
msgstr ""
-msgid "Enter admin mode"
+msgid "Enter a number"
msgstr ""
msgid "Enter at least three characters to search"
@@ -9680,7 +9680,7 @@ msgstr ""
msgid "Leave"
msgstr ""
-msgid "Leave admin mode"
+msgid "Leave Admin Mode"
msgstr ""
msgid "Leave edit mode? All unsaved changes will be lost."
diff --git a/spec/factories/grafana_integrations.rb b/spec/factories/grafana_integrations.rb
index c19417f5a90..4eb3bee8b28 100644
--- a/spec/factories/grafana_integrations.rb
+++ b/spec/factories/grafana_integrations.rb
@@ -3,7 +3,7 @@
FactoryBot.define do
factory :grafana_integration, class: GrafanaIntegration do
project
- grafana_url { 'https://grafana.com' }
+ grafana_url { 'https://grafana.example.com' }
token { SecureRandom.hex(10) }
end
end
diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb
index e1c9364067a..99a6165cfc9 100644
--- a/spec/features/admin/admin_settings_spec.rb
+++ b/spec/features/admin/admin_settings_spec.rb
@@ -5,6 +5,7 @@ require 'spec_helper'
describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_mock_admin_mode do
include StubENV
include TermsHelper
+ include MobileHelpers
let(:admin) { create(:admin) }
@@ -450,6 +451,32 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
expect(page).to have_link(text: 'Support', href: new_support_url)
end
end
+
+ it 'Shows admin dashboard links on bigger screen' do
+ visit root_dashboard_path
+
+ page.within '.navbar' do
+ expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+ end
+
+ it 'Relocates admin dashboard links to dropdown list on smaller screen', :js do
+ resize_screen_xs
+ visit root_dashboard_path
+
+ page.within '.navbar' do
+ expect(page).not_to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).not_to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+
+ find('.header-more').click
+
+ page.within '.navbar' do
+ expect(page).to have_link(text: 'Admin Area', href: admin_root_path, visible: true)
+ expect(page).to have_link(text: 'Leave Admin Mode', href: destroy_admin_session_path, visible: true)
+ end
+ end
end
context 'when in admin_mode' do
@@ -462,7 +489,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
it 'can leave admin mode' do
page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list
- click_on 'Leave admin mode', match: :first
+ click_on 'Leave Admin Mode', match: :first
expect(page).to have_link(href: new_admin_session_path)
end
@@ -481,7 +508,7 @@ describe 'Admin updates settings', :clean_gitlab_redis_shared_state, :do_not_moc
before do
page.within('.navbar-sub-nav') do
# Select first, link is also included in mobile view list
- click_on 'Leave admin mode', match: :first
+ click_on 'Leave Admin Mode', match: :first
end
end
diff --git a/spec/fixtures/grafana/dashboard_response.json b/spec/fixtures/grafana/dashboard_response.json
new file mode 100644
index 00000000000..4743ec39b44
--- /dev/null
+++ b/spec/fixtures/grafana/dashboard_response.json
@@ -0,0 +1,764 @@
+{
+ "meta": {
+ "type": "db",
+ "canSave": true,
+ "canEdit": true,
+ "canAdmin": true,
+ "canStar": true,
+ "slug": "gitlab-omnibus-redis",
+ "url": "/-/grafana/d/XDaNK6amz/gitlab-omnibus-redis",
+ "expires": "0001-01-01T00:00:00Z",
+ "created": "2019-10-04T13:43:20Z",
+ "updated": "2019-10-04T13:43:20Z",
+ "updatedBy": "Anonymous",
+ "createdBy": "Anonymous",
+ "version": 1,
+ "hasAcl": false,
+ "isFolder": false,
+ "folderId": 1,
+ "folderTitle": "GitLab Omnibus",
+ "folderUrl": "/-/grafana/dashboards/f/l2EpNh2Zk/gitlab-omnibus",
+ "provisioned": true,
+ "provisionedExternalId": "redis.json"
+ },
+ "dashboard": {
+ "annotations": {
+ "list": [
+ {
+ "builtIn": 1,
+ "datasource": "-- Grafana --",
+ "enable": true,
+ "hide": true,
+ "iconColor": "rgba(0, 211, 255, 1)",
+ "name": "Annotations \u0026 Alerts",
+ "type": "dashboard"
+ }
+ ]
+ },
+ "description": "GitLab Omnibus dashboard for Redis servers",
+ "editable": true,
+ "gnetId": 763,
+ "graphTooltip": 0,
+ "id": 3,
+ "iteration": 1556027798221,
+ "links": [],
+ "panels": [
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+ "datasource": "GitLab Omnibus",
+ "decimals": 0,
+ "editable": true,
+ "error": false,
+ "format": "dtdurations",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 3, "w": 4, "x": 0, "y": 0 },
+ "id": 9,
+ "interval": null,
+ "isNew": true,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": false
+ },
+ "tableColumn": "addr",
+ "targets": [
+ {
+ "expr": "avg(time() - redis_start_time_seconds{instance=~\"$instance\"})",
+ "format": "time_series",
+ "instant": true,
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 1800
+ }
+ ],
+ "thresholds": "",
+ "title": "Uptime",
+ "type": "singlestat",
+ "valueFontSize": "70%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "current"
+ },
+ {
+ "cacheTimeout": null,
+ "colorBackground": false,
+ "colorValue": false,
+ "colors": ["rgba(245, 54, 54, 0.9)", "rgba(237, 129, 40, 0.89)", "rgba(50, 172, 45, 0.97)"],
+ "datasource": "GitLab Omnibus",
+ "decimals": 0,
+ "editable": true,
+ "error": false,
+ "format": "none",
+ "gauge": {
+ "maxValue": 100,
+ "minValue": 0,
+ "show": false,
+ "thresholdLabels": false,
+ "thresholdMarkers": true
+ },
+ "gridPos": { "h": 3, "w": 4, "x": 4, "y": 0 },
+ "hideTimeOverride": true,
+ "id": 12,
+ "interval": null,
+ "isNew": true,
+ "links": [],
+ "mappingType": 1,
+ "mappingTypes": [
+ { "name": "value to text", "value": 1 },
+ { "name": "range to text", "value": 2 }
+ ],
+ "maxDataPoints": 100,
+ "nullPointMode": "connected",
+ "nullText": null,
+ "postfix": "",
+ "postfixFontSize": "50%",
+ "prefix": "",
+ "prefixFontSize": "50%",
+ "rangeMaps": [{ "from": "null", "text": "N/A", "to": "null" }],
+ "sparkline": {
+ "fillColor": "rgba(31, 118, 189, 0.18)",
+ "full": false,
+ "lineColor": "rgb(31, 120, 193)",
+ "show": true
+ },
+ "tableColumn": "",
+ "targets": [
+ {
+ "expr": "sum(\n avg_over_time(redis_connected_clients{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "",
+ "refId": "A",
+ "step": 2
+ }
+ ],
+ "thresholds": "",
+ "timeFrom": "1m",
+ "timeShift": null,
+ "title": "Clients",
+ "type": "singlestat",
+ "valueFontSize": "80%",
+ "valueMaps": [{ "op": "=", "text": "N/A", "value": "null" }],
+ "valueName": "avg"
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 6, "w": 8, "x": 8, "y": 0 },
+ "id": 2,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": false,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_commands_processed_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "",
+ "metric": "A",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Commands Executed",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "reqps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "decimals": 2,
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 6, "w": 8, "x": 16, "y": 0 },
+ "id": 1,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": false,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": true,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_keyspace_hits_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "hide": false,
+ "interval": "1m",
+ "intervalFactor": 1,
+ "legendFormat": "hits",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(\n rate(redis_keyspace_misses_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "hide": false,
+ "interval": "1m",
+ "intervalFactor": 1,
+ "legendFormat": "misses",
+ "metric": "",
+ "refId": "B",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Hits, Misses per Second",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": "", "logBase": 1, "max": null, "min": 0, "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": { "max": "#BF1B00" },
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 10, "w": 8, "x": 0, "y": 3 },
+ "id": 7,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "hideEmpty": false,
+ "hideZero": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "null as zero",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [{ "alias": "/max - .*/", "dashes": true }],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "redis_memory_used_bytes{instance=~\"$instance\"}",
+ "format": "time_series",
+ "intervalFactor": 2,
+ "legendFormat": "used - {{instance}}",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "redis_config_maxmemory{instance=~\"$instance\"} \u003e 0",
+ "format": "time_series",
+ "hide": false,
+ "intervalFactor": 2,
+ "legendFormat": "max - {{instance}}",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Memory Usage",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "bytes", "label": null, "logBase": 1, "max": null, "min": 0, "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {
+ "evicts": "#890F02",
+ "memcached_items_evicted_total{instance=\"172.17.0.1:9150\",job=\"prometheus\"}": "#890F02",
+ "reclaims": "#3F6833"
+ },
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 8, "y": 6 },
+ "id": 8,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [{ "alias": "reclaims", "yaxis": 2 }],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(rate(redis_expired_keys_total{instance=~\"$instance\"}[$__interval]))",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "expired",
+ "metric": "",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(rate(redis_evicted_keys_total{instance=~\"$instance\"}[$__interval]))",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "evicted",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Expired / Evicted",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 1,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 16, "y": 6 },
+ "id": 10,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": false,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "In",
+ "refId": "A",
+ "step": 240
+ },
+ {
+ "expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "Out",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Network I/O",
+ "tooltip": { "msResolution": true, "shared": true, "sort": 0, "value_type": "cumulative" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "Bps", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 8,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 16, "x": 0, "y": 13 },
+ "id": 14,
+ "isNew": true,
+ "legend": {
+ "alignAsTable": true,
+ "avg": true,
+ "current": true,
+ "max": true,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 1,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum without (instance) (\n rate(redis_commands_total{instance=~\"$instance\"}[$__interval])\n) \u003e 0",
+ "format": "time_series",
+ "interval": "1m",
+ "intervalFactor": 2,
+ "legendFormat": "{{ cmd }}",
+ "metric": "redis_command_calls_total",
+ "refId": "A",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Command Calls / sec",
+ "tooltip": { "msResolution": true, "shared": true, "sort": 2, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 7,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 8, "x": 16, "y": 13 },
+ "id": 13,
+ "isNew": true,
+ "legend": {
+ "avg": false,
+ "current": false,
+ "max": false,
+ "min": false,
+ "show": true,
+ "total": false,
+ "values": false
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum(redis_db_keys{instance=~\"$instance\"} - redis_db_keys_expiring{instance=~\"$instance\"}) ",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "not expiring",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ },
+ {
+ "expr": "sum(redis_db_keys_expiring{instance=~\"$instance\"})",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "expiring",
+ "metric": "",
+ "refId": "B",
+ "step": 240
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Expiring vs Not-Expiring Keys",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ },
+ {
+ "aliasColors": {},
+ "bars": false,
+ "dashLength": 10,
+ "dashes": false,
+ "datasource": "GitLab Omnibus",
+ "editable": true,
+ "error": false,
+ "fill": 7,
+ "grid": {},
+ "gridPos": { "h": 7, "w": 16, "x": 0, "y": 20 },
+ "id": 5,
+ "isNew": true,
+ "legend": {
+ "alignAsTable": true,
+ "avg": false,
+ "current": true,
+ "max": false,
+ "min": false,
+ "rightSide": true,
+ "show": true,
+ "total": false,
+ "values": true
+ },
+ "lines": true,
+ "linewidth": 2,
+ "links": [],
+ "nullPointMode": "connected",
+ "paceLength": 10,
+ "percentage": false,
+ "pointradius": 5,
+ "points": false,
+ "renderer": "flot",
+ "seriesOverrides": [],
+ "spaceLength": 10,
+ "stack": true,
+ "steppedLine": false,
+ "targets": [
+ {
+ "expr": "sum by (db) (\n redis_db_keys{instance=~\"$instance\"}\n)",
+ "format": "time_series",
+ "interval": "",
+ "intervalFactor": 2,
+ "legendFormat": "{{ db }} ",
+ "refId": "A",
+ "step": 240,
+ "target": ""
+ }
+ ],
+ "thresholds": [],
+ "timeFrom": null,
+ "timeRegions": [],
+ "timeShift": null,
+ "title": "Items per DB",
+ "tooltip": { "msResolution": false, "shared": true, "sort": 0, "value_type": "individual" },
+ "type": "graph",
+ "xaxis": { "buckets": null, "mode": "time", "name": null, "show": true, "values": [] },
+ "yaxes": [
+ { "format": "none", "label": null, "logBase": 1, "max": null, "min": "0", "show": true },
+ { "format": "short", "label": null, "logBase": 1, "max": null, "min": null, "show": true }
+ ],
+ "yaxis": { "align": false, "alignLevel": null }
+ }
+ ],
+ "refresh": "1m",
+ "schemaVersion": 18,
+ "style": "dark",
+ "tags": ["redis"],
+ "templating": {
+ "list": [
+ {
+ "allValue": null,
+ "current": { "tags": [], "text": "All", "value": "$__all" },
+ "datasource": "GitLab Omnibus",
+ "definition": "",
+ "hide": 0,
+ "includeAll": true,
+ "label": null,
+ "multi": false,
+ "name": "instance",
+ "options": [],
+ "query": "label_values(up{job=\"redis\"}, instance)",
+ "refresh": 1,
+ "regex": "",
+ "skipUrlSync": false,
+ "sort": 0,
+ "tagValuesQuery": "",
+ "tags": [],
+ "tagsQuery": "",
+ "type": "query",
+ "useTags": false
+ }
+ ]
+ },
+ "time": { "from": "now-24h", "to": "now" },
+ "timepicker": {
+ "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
+ "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"]
+ },
+ "timezone": "",
+ "title": "GitLab Omnibus - Redis",
+ "uid": "XDaNK6amz",
+ "version": 1
+ }
+}
diff --git a/spec/fixtures/grafana/datasource_response.json b/spec/fixtures/grafana/datasource_response.json
new file mode 100644
index 00000000000..07c075beb35
--- /dev/null
+++ b/spec/fixtures/grafana/datasource_response.json
@@ -0,0 +1,21 @@
+{
+ "id": 1,
+ "orgId": 1,
+ "name": "GitLab Omnibus",
+ "type": "prometheus",
+ "typeLogoUrl": "",
+ "access": "proxy",
+ "url": "http://localhost:9090",
+ "password": "",
+ "user": "",
+ "database": "",
+ "basicAuth": false,
+ "basicAuthUser": "",
+ "basicAuthPassword": "",
+ "withCredentials": false,
+ "isDefault": true,
+ "jsonData": {},
+ "secureJsonFields": {},
+ "version": 1,
+ "readOnly": true
+}
diff --git a/spec/fixtures/grafana/expected_grafana_embed.json b/spec/fixtures/grafana/expected_grafana_embed.json
new file mode 100644
index 00000000000..72fb5477b9e
--- /dev/null
+++ b/spec/fixtures/grafana/expected_grafana_embed.json
@@ -0,0 +1,27 @@
+{
+ "panel_groups": [
+ {
+ "panels": [
+ {
+ "title": "Network I/O",
+ "type": "area-chart",
+ "y_label": "",
+ "metrics": [
+ {
+ "id": "In_0",
+ "query_range": "sum( rate(redis_net_input_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+ "label": "In",
+ "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_input_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ },
+ {
+ "id": "Out_1",
+ "query_range": "sum( rate(redis_net_output_bytes_total{instance=~\"localhost:9121\"}[1m]))",
+ "label": "Out",
+ "prometheus_endpoint_path": "/foo/bar/-/grafana/proxy/1/api/v1/query_range?query=sum%28++rate%28redis_net_output_bytes_total%7Binstance%3D~%22localhost%3A9121%22%7D%5B1m%5D%29%29"
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/spec/fixtures/grafana/simplified_dashboard_response.json b/spec/fixtures/grafana/simplified_dashboard_response.json
new file mode 100644
index 00000000000..b450fda082b
--- /dev/null
+++ b/spec/fixtures/grafana/simplified_dashboard_response.json
@@ -0,0 +1,40 @@
+{
+ "dashboard": {
+ "panels": [
+ {
+ "datasource": "GitLab Omnibus",
+ "id": 8,
+ "lines": true,
+ "targets": [
+ {
+ "expr": "sum(\n rate(redis_net_input_bytes_total{instance=~\"$instance\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "legendFormat": "In",
+ "refId": "A"
+ },
+ {
+ "expr": "sum(\n rate(redis_net_output_bytes_total{instance=~\"[[instance]]\"}[$__interval])\n)",
+ "format": "time_series",
+ "interval": "1m",
+ "legendFormat": "Out",
+ "refId": "B"
+ }
+ ],
+ "title": "Network I/O",
+ "type": "graph",
+ "yaxes": [{ "format": "Bps" }, { "format": "short" }]
+ }
+ ],
+ "templating": {
+ "list": [
+ {
+ "current": {
+ "value": "localhost:9121"
+ },
+ "name": "instance"
+ }
+ ]
+ }
+ }
+}
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
index 9c1be32645a..ac40f2dcd13 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/metrics.json
@@ -1,7 +1,6 @@
{
"type": "object",
"required": [
- "unit",
"label",
"prometheus_endpoint_path"
],
diff --git a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
index 1548daacd64..a16f1ef592f 100644
--- a/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
+++ b/spec/fixtures/lib/gitlab/metrics/dashboard/schemas/panels.json
@@ -3,7 +3,6 @@
"required": [
"title",
"y_label",
- "weight",
"metrics"
],
"properties": {
diff --git a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
index e2ce1869810..4fa136bc405 100644
--- a/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
+++ b/spec/lib/gitlab/metrics/dashboard/processor_spec.rb
@@ -25,6 +25,14 @@ describe Gitlab::Metrics::Dashboard::Processor do
end
end
+ context 'when the dashboard is not present' do
+ let(:dashboard_yml) { nil }
+
+ it 'returns nil' do
+ expect(dashboard).to be_nil
+ end
+ end
+
context 'when dashboard config corresponds to common metrics' do
let!(:common_metric) { create(:prometheus_metric, :common, identifier: 'metric_a1') }
diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
new file mode 100644
index 00000000000..5c2ec6dae6b
--- /dev/null
+++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb
@@ -0,0 +1,106 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do
+ include GrafanaApiHelpers
+
+ let_it_be(:namespace) { create(:namespace, name: 'foo') }
+ let_it_be(:project) { create(:project, namespace: namespace, name: 'bar') }
+
+ describe '#transform!' do
+ let(:grafana_dashboard) { JSON.parse(fixture_file('grafana/simplified_dashboard_response.json'), symbolize_names: true) }
+ let(:datasource) { JSON.parse(fixture_file('grafana/datasource_response.json'), symbolize_names: true) }
+
+ let(:dashboard) { described_class.new(project, {}, params).transform! }
+
+ let(:params) do
+ {
+ grafana_dashboard: grafana_dashboard,
+ datasource: datasource,
+ grafana_url: valid_grafana_dashboard_link('https://grafana.example.com')
+ }
+ end
+
+ context 'when the query and resources are configured correctly' do
+ let(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) }
+
+ it 'generates a gitlab-yml formatted dashboard' do
+ expect(dashboard).to eq(expected_dashboard)
+ end
+ end
+
+ context 'when the inputs are invalid' do
+ shared_examples_for 'processing error' do
+ it 'raises a processing error' do
+ expect { dashboard }
+ .to raise_error(Gitlab::Metrics::Dashboard::Stages::InputFormatValidator::DashboardProcessingError)
+ end
+ end
+
+ context 'when the datasource is not proxyable' do
+ before do
+ params[:datasource][:access] = 'not-proxy'
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "panelId" is not specified' do
+ before do
+ params[:grafana_url].gsub!('panelId=8', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "from" is not specified' do
+ before do
+ params[:grafana_url].gsub!('from=1570397739557', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when query param "to" is not specified' do
+ before do
+ params[:grafana_url].gsub!('to=1570484139557', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the panel is not a graph' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:type] = 'singlestat'
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the panel is not a line graph' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:lines] = false
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the query dashboard includes undefined variables' do
+ before do
+ params[:grafana_url].gsub!('&var-instance=localhost:9121', '')
+ end
+
+ it_behaves_like 'processing error'
+ end
+
+ context 'when the expression contains unsupported global variables' do
+ before do
+ params[:grafana_dashboard][:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])'
+ end
+
+ it_behaves_like 'processing error'
+ end
+ end
+ end
+end
diff --git a/spec/lib/grafana/client_spec.rb b/spec/lib/grafana/client_spec.rb
index bd93a3c59a2..699344e940e 100644
--- a/spec/lib/grafana/client_spec.rb
+++ b/spec/lib/grafana/client_spec.rb
@@ -35,7 +35,7 @@ describe Grafana::Client do
it 'does not follow redirects' do
expect { subject }.to raise_exception(
Grafana::Client::Error,
- 'Grafana response status code: 302'
+ 'Grafana response status code: 302, Message: {}'
)
expect(redirect_req_stub).to have_been_requested
@@ -67,6 +67,30 @@ describe Grafana::Client do
end
end
+ describe '#get_dashboard' do
+ let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/dashboards/uid/FndfgnX' }
+
+ subject do
+ client.get_dashboard(uid: 'FndfgnX')
+ end
+
+ it_behaves_like 'calls grafana api'
+ it_behaves_like 'no redirects'
+ it_behaves_like 'handles exceptions'
+ end
+
+ describe '#get_datasource' do
+ let(:grafana_api_url) { 'https://grafanatest.com/-/grafana-project/api/datasources/name/Test%20Name' }
+
+ subject do
+ client.get_datasource(name: 'Test Name')
+ end
+
+ it_behaves_like 'calls grafana api'
+ it_behaves_like 'no redirects'
+ it_behaves_like 'handles exceptions'
+ end
+
describe '#proxy_datasource' do
let(:grafana_api_url) do
'https://grafanatest.com/-/grafana-project/' \
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index f9c8b42afa8..d1e20cb1770 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -602,7 +602,7 @@ describe API::Branches do
post api(route, user), params: { branch: 'new_design3', ref: 'foo' }
expect(response).to have_gitlab_http_status(400)
- expect(json_response['message']).to eq('Invalid reference name')
+ expect(json_response['message']).to eq('Invalid reference name: new_design3')
end
end
diff --git a/spec/services/create_branch_service_spec.rb b/spec/services/create_branch_service_spec.rb
index 0d34c7f9a82..9661173c9e7 100644
--- a/spec/services/create_branch_service_spec.rb
+++ b/spec/services/create_branch_service_spec.rb
@@ -22,5 +22,20 @@ describe CreateBranchService do
expect(project.repository.branch_exists?('my-feature')).to be_truthy
end
end
+
+ context 'when creating a branch fails' do
+ let(:project) { create(:project_empty_repo) }
+
+ before do
+ allow(project.repository).to receive(:add_branch).and_return(false)
+ end
+
+ it 'retruns an error with the branch name' do
+ result = service.execute('my-feature', 'master')
+
+ expect(result[:status]).to eq(:error)
+ expect(result[:message]).to eq("Invalid reference name: my-feature")
+ end
+ end
end
end
diff --git a/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
new file mode 100644
index 00000000000..f200c636aac
--- /dev/null
+++ b/spec/services/metrics/dashboard/grafana_metric_embed_service_spec.rb
@@ -0,0 +1,177 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Metrics::Dashboard::GrafanaMetricEmbedService do
+ include MetricsDashboardHelpers
+ include ReactiveCachingHelpers
+ include GrafanaApiHelpers
+
+ let_it_be(:project) { build(:project) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:grafana_integration) { create(:grafana_integration, project: project) }
+
+ let(:grafana_url) do
+ valid_grafana_dashboard_link(grafana_integration.grafana_url)
+ end
+
+ before do
+ project.add_maintainer(user)
+ end
+
+ describe '.valid_params?' do
+ let(:valid_params) { { embedded: true, grafana_url: grafana_url } }
+
+ subject { described_class.valid_params?(params) }
+
+ let(:params) { valid_params }
+
+ it { is_expected.to be_truthy }
+
+ context 'not embedded' do
+ let(:params) { valid_params.except(:embedded) }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'undefined grafana_url' do
+ let(:params) { valid_params.except(:grafana_url) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '.from_cache' do
+ let(:params) { [project.id, user.id, grafana_url] }
+
+ subject { described_class.from_cache(*params) }
+
+ it 'initializes an instance of GrafanaMetricEmbedService' do
+ expect(subject).to be_an_instance_of(described_class)
+ expect(subject.project).to eq(project)
+ expect(subject.current_user).to eq(user)
+ expect(subject.params[:grafana_url]).to eq(grafana_url)
+ end
+ end
+
+ describe '#get_dashboard', :use_clean_rails_memory_store_caching do
+ let(:service_params) do
+ [
+ project,
+ user,
+ {
+ embedded: true,
+ grafana_url: grafana_url
+ }
+ ]
+ end
+
+ let(:service) { described_class.new(*service_params) }
+ let(:service_call) { service.get_dashboard }
+
+ context 'without caching' do
+ before do
+ synchronous_reactive_cache(service)
+ end
+
+ it_behaves_like 'raises error for users with insufficient permissions'
+
+ context 'without a grafana integration' do
+ before do
+ allow(project).to receive(:grafana_integration).and_return(nil)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :bad_request
+ end
+
+ context 'when grafana cannot be reached' do
+ before do
+ allow(grafana_integration.client).to receive(:get_dashboard).and_raise(::Grafana::Client::Error)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :service_unavailable
+ end
+
+ context 'when panelId is missing' do
+ let(:grafana_url) do
+ grafana_integration.grafana_url +
+ '/d/XDaNK6amz/gitlab-omnibus-redis' \
+ '?from=1570397739557&to=1570484139557'
+ end
+
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when uid is missing' do
+ let(:grafana_url) { grafana_integration.grafana_url + '/d/' }
+
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the dashboard response contains misconfigured json' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url, body: '')
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the datasource response contains misconfigured json' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ stub_datasource_request(grafana_integration.grafana_url, body: '')
+ end
+
+ it_behaves_like 'misconfigured dashboard service response', :unprocessable_entity
+ end
+
+ context 'when the embed was created successfully' do
+ before do
+ stub_dashboard_request(grafana_integration.grafana_url)
+ stub_datasource_request(grafana_integration.grafana_url)
+ end
+
+ it_behaves_like 'valid embedded dashboard service response'
+ end
+ end
+
+ context 'with caching', :use_clean_rails_memory_store_caching do
+ let(:cache_params) { [project.id, user.id, grafana_url] }
+
+ context 'when value not present in cache' do
+ it 'returns nil' do
+ expect(ReactiveCachingWorker)
+ .to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(service_call).to eq(nil)
+ end
+ end
+
+ context 'when value present in cache' do
+ let(:return_value) { { 'http_status' => :ok, 'dashboard' => '{}' } }
+
+ before do
+ stub_reactive_cache(service, return_value, cache_params)
+ end
+
+ it 'returns cached value' do
+ expect(ReactiveCachingWorker)
+ .not_to receive(:perform_async)
+ .with(service.class, service.id, *cache_params)
+
+ expect(service_call[:http_status]).to eq(return_value[:http_status])
+ expect(service_call[:dashboard]).to eq(return_value[:dashboard])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/helpers/grafana_api_helpers.rb b/spec/support/helpers/grafana_api_helpers.rb
new file mode 100644
index 00000000000..b212cbf2943
--- /dev/null
+++ b/spec/support/helpers/grafana_api_helpers.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module GrafanaApiHelpers
+ def valid_grafana_dashboard_link(base_url)
+ base_url +
+ '/d/XDaNK6amz/gitlab-omnibus-redis' \
+ '?from=1570397739557&to=1570484139557' \
+ '&var-instance=localhost:9121&panelId=8'
+ end
+
+ def stub_dashboard_request(base_url, path: '/api/dashboards/uid/XDaNK6amz', body: nil)
+ body ||= fixture_file('grafana/dashboard_response.json')
+
+ stub_request(:get, "#{base_url}#{path}")
+ .to_return(
+ status: 200,
+ body: body,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+
+ def stub_datasource_request(base_url, path: '/api/datasources/name/GitLab%20Omnibus', body: nil)
+ body ||= fixture_file('grafana/datasource_response.json')
+
+ stub_request(:get, "#{base_url}#{path}")
+ .to_return(
+ status: 200,
+ body: body,
+ headers: { 'Content-Type' => 'application/json' }
+ )
+ end
+end
diff --git a/spec/support/helpers/login_helpers.rb b/spec/support/helpers/login_helpers.rb
index 7d5896e4eeb..1d42f26ad3e 100644
--- a/spec/support/helpers/login_helpers.rb
+++ b/spec/support/helpers/login_helpers.rb
@@ -53,7 +53,7 @@ module LoginHelpers
fill_in 'password', with: user.password
- click_button 'Enter admin mode'
+ click_button 'Enter Admin Mode'
end
def gitlab_sign_in_via(provider, user, uid, saml_response = nil)