diff options
53 files changed, 800 insertions, 347 deletions
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue index b90f441b8ec..5e41a155ef6 100644 --- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue +++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue @@ -122,9 +122,14 @@ export default { this.$store.subscribeAction({ after: this.handleVuexActionDispatch, }); + + document.addEventListener('click', this.handleDocumentClick); + }, + beforeDestroy() { + document.removeEventListener('click', this.handleDocumentClick); }, methods: { - ...mapActions(['setInitialState']), + ...mapActions(['setInitialState', 'toggleDropdownContents']), /** * This method differentiates between * dispatched actions and calls necessary method. @@ -138,6 +143,22 @@ export default { this.handleDropdownClose(state.labels.filter(label => label.touched)); } }, + /** + * This method listens for document-wide click event + * and toggle dropdown if user clicks anywhere outside + * the dropdown while dropdown is visible. + */ + handleDocumentClick({ target }) { + if ( + this.showDropdownButton && + this.showDropdownContents && + !target?.classList.contains('js-sidebar-dropdown-toggle') && + !this.$refs.dropdownButtonCollapsed?.$el.contains(target) && + !this.$refs.dropdownContents?.$el.contains(target) + ) { + this.toggleDropdownContents(); + } + }, handleDropdownClose(labels) { // Only emit label updates if there are any labels to update // on UI. @@ -156,6 +177,7 @@ export default { <div v-if="!dropdownOnly"> <dropdown-value-collapsed v-if="allowLabelCreate" + ref="dropdownButtonCollapsed" :labels="selectedLabels" @onValueClick="handleCollapsedValueClick" /> @@ -167,7 +189,7 @@ export default { <slot></slot> </dropdown-value> <dropdown-button v-show="showDropdownButton" /> - <dropdown-contents v-if="showDropdownButton && showDropdownContents" /> + <dropdown-contents v-if="showDropdownButton && showDropdownContents" ref="dropdownContents" /> </div> </div> </template> diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss index 408ca249be2..20846502e85 100644 --- a/app/assets/stylesheets/framework/common.scss +++ b/app/assets/stylesheets/framework/common.scss @@ -413,6 +413,7 @@ img.emoji { .prepend-left-20 { margin-left: 20px; } .prepend-left-32 { margin-left: 32px; } .prepend-left-64 { margin-left: 64px; } +.append-right-2 { margin-right: 2px; } .append-right-4 { margin-right: 4px; } .append-right-5 { margin-right: 5px; } .append-right-8 { margin-right: 8px; } @@ -424,6 +425,7 @@ img.emoji { .append-right-48 { margin-right: 48px; } .prepend-right-32 { margin-right: 32px; } .append-bottom-0 { margin-bottom: 0; } +.append-bottom-2 { margin-bottom: 2px; } .append-bottom-4 { margin-bottom: $gl-padding-4; } .append-bottom-5 { margin-bottom: 5px; } .append-bottom-8 { margin-bottom: $grid-size; } diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb index 711be67f8f9..039991e07a2 100644 --- a/app/controllers/dashboard/projects_controller.rb +++ b/app/controllers/dashboard/projects_controller.rb @@ -33,7 +33,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController # rubocop: disable CodeReuse/ActiveRecord def starred @projects = load_projects(params.merge(starred: true)) - .includes(:forked_from_project, :tags).page(params[:page]) + .includes(:forked_from_project, :tags) @groups = [] @@ -51,7 +51,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController private def projects - @projects ||= load_projects(params.merge(non_public: true)).page(params[:page]) + @projects ||= load_projects(params.merge(non_public: true)) end def render_projects @@ -73,6 +73,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController .execute .includes(:route, :creator, :group, namespace: [:route, :owner]) .preload(:project_feature) + .page(finder_params[:page]) prepare_projects_for_rendering(projects) end diff --git a/app/models/issue.rb b/app/models/issue.rb index d3f597c0bda..ef65b002816 100644 --- a/app/models/issue.rb +++ b/app/models/issue.rb @@ -67,6 +67,7 @@ class Issue < ApplicationRecord scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) } scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) } scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) } + scope :order_closed_date_desc, -> { reorder(closed_at: :desc) } scope :preload_associated_models, -> { preload(:labels, project: :namespace) } scope :with_api_entity_associations, -> { preload(:timelogs, :assignees, :author, :notes, :labels, project: [:route, { namespace: :route }] ) } diff --git a/app/models/service.rb b/app/models/service.rb index 91597c51fca..8f1772e67f9 100644 --- a/app/models/service.rb +++ b/app/models/service.rb @@ -32,9 +32,12 @@ class Service < ApplicationRecord belongs_to :project, inverse_of: :services has_one :service_hook - validates :project_id, presence: true, unless: -> { template? } + validates :project_id, presence: true, unless: -> { template? || instance? } + validates :project_id, absence: true, if: -> { instance? } validates :type, presence: true validates :template, uniqueness: { scope: :type }, if: -> { template? } + validates :instance, uniqueness: { scope: :type }, if: -> { instance? } + validate :validate_is_instance_or_template scope :visible, -> { where.not(type: 'GitlabIssueTrackerService') } scope :issue_trackers, -> { where(category: 'issue_tracker') } @@ -326,6 +329,10 @@ class Service < ApplicationRecord private + def validate_is_instance_or_template + errors.add(:template, 'The service should be a service template or instance-level integration') if template? && instance? + end + def cache_project_has_external_issue_tracker if project && !project.destroyed? project.cache_has_external_issue_tracker diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb index 699fa17cb65..337710b60e0 100644 --- a/app/services/boards/issues/list_service.rb +++ b/app/services/boards/issues/list_service.rb @@ -10,6 +10,8 @@ module Boards end def execute + return fetch_issues.order_closed_date_desc if list&.closed? + fetch_issues.order_by_position_and_priority(with_cte: can_attempt_search_optimization?) end diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb index 3ad3a2c609e..274057b8262 100644 --- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb +++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb @@ -138,7 +138,9 @@ module Metrics end # Identifies the name of the datasource for a dashboard - # based on the panelId query parameter found in the url + # based on the panelId query parameter found in the url. + # + # If no panel is specified, defaults to the first valid panel. class DatasourceNameParser def initialize(grafana_url, grafana_dashboard) @grafana_url, @grafana_dashboard = grafana_url, grafana_dashboard @@ -146,15 +148,29 @@ module Metrics def parse @grafana_dashboard[:dashboard][:panels] - .find { |panel| panel[:id].to_s == query_params[:panelId] } + .find { |panel| panel_id ? matching_panel?(panel) : valid_panel?(panel) } .try(:[], :datasource) end private + def panel_id + query_params[:panelId] + end + def query_params Gitlab::Metrics::Dashboard::Url.parse_query(@grafana_url) end + + def matching_panel?(panel) + panel[:id].to_s == panel_id + end + + def valid_panel?(panel) + ::Grafana::Validator + .new(@grafana_dashboard, nil, panel, query_params) + .valid? + end end end end diff --git a/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml b/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml new file mode 100644 index 00000000000..a015ef1b132 --- /dev/null +++ b/changelogs/unreleased/19880-sort-closed-issues-by-recently-closed.yml @@ -0,0 +1,5 @@ +--- +title: Sort closed issues on issue boards using time of closing +merge_request: 23442 +author: briankabiro +type: changed diff --git a/changelogs/unreleased/204801-add-instance-to-services.yml b/changelogs/unreleased/204801-add-instance-to-services.yml new file mode 100644 index 00000000000..458644d7dd6 --- /dev/null +++ b/changelogs/unreleased/204801-add-instance-to-services.yml @@ -0,0 +1,5 @@ +--- +title: Add instance column to services table +merge_request: 25714 +author: +type: other diff --git a/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml b/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml new file mode 100644 index 00000000000..afba03b584a --- /dev/null +++ b/changelogs/unreleased/206899-move-system-metrics-chart-group-to-the-top-of-the-default-dashbord.yml @@ -0,0 +1,5 @@ +--- +title: Put System Metrics chart group first in default dashboard +merge_request: 26355 +author: +type: other diff --git a/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml b/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml new file mode 100644 index 00000000000..e2014464d8c --- /dev/null +++ b/changelogs/unreleased/208887-optimize-project-counters-projects_with_repositories_enabled.yml @@ -0,0 +1,5 @@ +--- +title: Optimize Project counters with respository enabled counter +merge_request: 26698 +author: +type: performance diff --git a/changelogs/unreleased/fix-missing-rss-feed-events.yml b/changelogs/unreleased/fix-missing-rss-feed-events.yml deleted file mode 100644 index 9fbacc6d739..00000000000 --- a/changelogs/unreleased/fix-missing-rss-feed-events.yml +++ /dev/null @@ -1,5 +0,0 @@ ----
-title: Fix missing RSS feed events
-merge_request: 19524
-author:
-type: fixed
diff --git a/changelogs/unreleased/sy-grafana-default-panel.yml b/changelogs/unreleased/sy-grafana-default-panel.yml new file mode 100644 index 00000000000..9ad5c824b32 --- /dev/null +++ b/changelogs/unreleased/sy-grafana-default-panel.yml @@ -0,0 +1,5 @@ +--- +title: Default to first valid panel in unspecified Grafana embeds +merge_request: 21932 +author: +type: changed diff --git a/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml b/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml new file mode 100644 index 00000000000..27e11028441 --- /dev/null +++ b/changelogs/unreleased/udpate-cluster-application-image-to-0-11.yml @@ -0,0 +1,6 @@ +--- +title: Update cluster-applications image to v0.11 with a runner bugfix, updated cert-manager, + and vault as a new app +merge_request: 26842 +author: +type: changed diff --git a/config/prometheus/common_metrics.yml b/config/prometheus/common_metrics.yml index 6eae29c3906..85833cc1968 100644 --- a/config/prometheus/common_metrics.yml +++ b/config/prometheus/common_metrics.yml @@ -1,6 +1,74 @@ dashboard: 'Environment metrics' priority: 1 panel_groups: +- group: System metrics (Kubernetes) + priority: 15 + panels: + - title: "Memory Usage (Total)" + type: "area-chart" + y_label: "Total Memory Used (GB)" + weight: 4 + metrics: + - id: system_metrics_kubernetes_container_memory_total + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024' + label: Total (GB) + unit: GB + - title: "Core Usage (Total)" + type: "area-chart" + y_label: "Total Cores" + weight: 3 + metrics: + - id: system_metrics_kubernetes_container_cores_total + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' + label: Total (cores) + unit: "cores" + - title: "Memory Usage (Pod average)" + type: "line-chart" + y_label: "Memory Used per Pod (MB)" + weight: 2 + metrics: + - id: system_metrics_kubernetes_container_memory_average + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' + label: Pod average (MB) + unit: MB + - title: "Canary: Memory Usage (Pod Average)" + type: "line-chart" + y_label: "Memory Used per Pod (MB)" + weight: 2 + metrics: + - id: system_metrics_kubernetes_container_memory_average_canary + query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' + label: Pod average (MB) + unit: MB + track: canary + - title: "Core Usage (Pod Average)" + type: "line-chart" + y_label: "Cores per Pod" + weight: 1 + metrics: + - id: system_metrics_kubernetes_container_core_usage + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' + label: Pod average (cores) + unit: "cores" + - title: "Canary: Core Usage (Pod Average)" + type: "line-chart" + y_label: "Cores per Pod" + weight: 1 + metrics: + - id: system_metrics_kubernetes_container_core_usage_canary + query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' + label: Pod average (cores) + unit: "cores" + track: canary + - title: "Knative function invocations" + type: "area-chart" + y_label: "Invocations" + weight: 1 + metrics: + - id: system_metrics_knative_function_invocation_count + query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))' + label: invocations / minute + unit: requests # NGINX Ingress metrics for pre-0.16.0 versions - group: Response metrics (NGINX Ingress VTS) priority: 10 @@ -150,79 +218,3 @@ panel_groups: query_range: 'sum(rate(nginx_server_requests{code="5xx", %{environment_filter}}[2m]))' label: HTTP Errors unit: "errors / sec" -- group: System metrics (Kubernetes) - priority: 5 - panels: - - title: "Memory Usage (Total)" - type: "area-chart" - y_label: "Total Memory Used (GB)" - y_axis: - format: "gibibytes" - weight: 4 - metrics: - - id: system_metrics_kubernetes_container_memory_total - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024' - label: Total (GB) - unit: GB - - title: "Core Usage (Total)" - type: "area-chart" - y_label: "Total Cores" - weight: 3 - metrics: - - id: system_metrics_kubernetes_container_cores_total - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job)' - label: Total (cores) - unit: "cores" - - title: "Memory Usage (Pod average)" - type: "line-chart" - y_label: "Memory Used per Pod (MB)" - y_axis: - format: "mebibytes" - weight: 2 - metrics: - - id: system_metrics_kubernetes_container_memory_average - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' - label: Pod average (MB) - unit: MB - - title: "Canary: Memory Usage (Pod Average)" - type: "line-chart" - y_label: "Memory Used per Pod (MB)" - y_axis: - format: "mebibytes" - weight: 2 - metrics: - - id: system_metrics_kubernetes_container_memory_average_canary - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024' - label: Pod average (MB) - unit: MB - track: canary - - title: "Core Usage (Pod Average)" - type: "line-chart" - y_label: "Cores per Pod" - weight: 1 - metrics: - - id: system_metrics_kubernetes_container_core_usage - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' - label: Pod average (cores) - unit: "cores" - - title: "Canary: Core Usage (Pod Average)" - type: "line-chart" - y_label: "Cores per Pod" - weight: 1 - metrics: - - id: system_metrics_kubernetes_container_core_usage_canary - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-canary-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))' - label: Pod average (cores) - unit: "cores" - track: canary - - title: "Knative function invocations" - type: "area-chart" - y_label: "Invocations" - y_axis: - precision: 0 - weight: 1 - metrics: - - id: system_metrics_knative_function_invocation_count - query_range: 'sum(ceil(rate(istio_requests_total{destination_service_namespace="%{kube_namespace}", destination_service=~"%{function_name}.*"}[1m])*60))' - label: invocations / minute - unit: requests diff --git a/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb b/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb new file mode 100644 index 00000000000..a2093db0b3b --- /dev/null +++ b/db/migrate/20200309140540_add_index_on_project_id_and_repository_access_level_to_project_features.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +class AddIndexOnProjectIdAndRepositoryAccessLevelToProjectFeatures < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + INDEX_NAME = 'index_project_features_on_project_id_ral_20' + + disable_ddl_transaction! + + def up + add_concurrent_index :project_features, :project_id, where: 'repository_access_level = 20', name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :project_features, INDEX_NAME + end +end diff --git a/db/migrate/20200310132654_add_instance_to_services.rb b/db/migrate/20200310132654_add_instance_to_services.rb new file mode 100644 index 00000000000..85b16a4094c --- /dev/null +++ b/db/migrate/20200310132654_add_instance_to_services.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddInstanceToServices < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_column_with_default(:services, :instance, :boolean, default: false) + end + + def down + remove_column(:services, :instance) + end +end diff --git a/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb b/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb new file mode 100644 index 00000000000..1a60c521b71 --- /dev/null +++ b/db/migrate/20200310135823_add_index_to_service_unique_instance_per_type.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +class AddIndexToServiceUniqueInstancePerType < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + disable_ddl_transaction! + + def up + add_concurrent_index(:services, [:type, :instance], unique: true, where: 'instance IS TRUE') + end + + def down + remove_concurrent_index(:services, [:type, :instance]) + end +end diff --git a/db/schema.rb b/db/schema.rb index 62cce205424..2b726073035 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 2020_03_09_195710) do +ActiveRecord::Schema.define(version: 2020_03_10_135823) do # These are extensions that must be enabled in order to support this database enable_extension "pg_trgm" @@ -3265,6 +3265,7 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do t.integer "forking_access_level" t.index ["project_id"], name: "index_project_features_on_project_id", unique: true t.index ["project_id"], name: "index_project_features_on_project_id_bal_20", where: "(builds_access_level = 20)" + t.index ["project_id"], name: "index_project_features_on_project_id_ral_20", where: "(repository_access_level = 20)" end create_table "project_group_links", id: :serial, force: :cascade do |t| @@ -3939,8 +3940,10 @@ ActiveRecord::Schema.define(version: 2020_03_09_195710) do t.string "description", limit: 500 t.boolean "comment_on_event_enabled", default: true, null: false t.boolean "template", default: false + t.boolean "instance", default: false, null: false t.index ["project_id"], name: "index_services_on_project_id" t.index ["template"], name: "index_services_on_template" + t.index ["type", "instance"], name: "index_services_on_type_and_instance", unique: true, where: "(instance IS TRUE)" t.index ["type", "template"], name: "index_services_on_type_and_template", unique: true, where: "(template IS TRUE)" t.index ["type"], name: "index_services_on_type" end diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f47f07953d7..b6caff283fc 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2024,6 +2024,11 @@ type Epic implements Noteable { hasIssues: Boolean! """ + Current health status of the epic + """ + healthStatus: EpicHealthStatus + + """ ID of the epic """ id: ID! @@ -2350,6 +2355,26 @@ type EpicEdge { } """ +Health status of child issues +""" +type EpicHealthStatus { + """ + Number of issues at risk + """ + issuesAtRisk: Int + + """ + Number of issues that need attention + """ + issuesNeedingAttention: Int + + """ + Number of issues on track + """ + issuesOnTrack: Int +} + +""" Relationship between an epic and an issue """ type EpicIssue implements Noteable { diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index 3472a8bf742..0f0abf25047 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5187,6 +5187,20 @@ "deprecationReason": null }, { + "name": "healthStatus", + "description": "Current health status of the epic", + "args": [ + + ], + "type": { + "kind": "OBJECT", + "name": "EpicHealthStatus", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { "name": "id", "description": "ID of the epic", "args": [ @@ -13086,6 +13100,61 @@ }, { "kind": "OBJECT", + "name": "EpicHealthStatus", + "description": "Health status of child issues", + "fields": [ + { + "name": "issuesAtRisk", + "description": "Number of issues at risk", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuesNeedingAttention", + "description": "Number of issues that need attention", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, + { + "name": "issuesOnTrack", + "description": "Number of issues on track", + "args": [ + + ], + "type": { + "kind": "SCALAR", + "name": "Int", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + } + ], + "inputFields": null, + "interfaces": [ + + ], + "enumValues": null, + "possibleTypes": null + }, + { + "kind": "OBJECT", "name": "TimelogConnection", "description": "The connection type for Timelog.", "fields": [ diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index df85e13d194..a85553ce4aa 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -327,6 +327,7 @@ Represents an epic. | `group` | Group! | Group to which the epic belongs | | `hasChildren` | Boolean! | Indicates if the epic has children | | `hasIssues` | Boolean! | Indicates if the epic has direct issues | +| `healthStatus` | EpicHealthStatus | Current health status of the epic | | `id` | ID! | ID of the epic | | `iid` | ID! | Internal ID of the epic | | `parent` | Epic | Parent epic of the epic | @@ -377,6 +378,16 @@ Total weight of open and closed descendant issues | `closedIssues` | Int | Total weight of completed (closed) issues in this epic, including epic descendants | | `openedIssues` | Int | Total weight of opened issues in this epic, including epic descendants | +## EpicHealthStatus + +Health status of child issues + +| Name | Type | Description | +| --- | ---- | ---------- | +| `issuesAtRisk` | Int | Number of issues at risk | +| `issuesNeedingAttention` | Int | Number of issues that need attention | +| `issuesOnTrack` | Int | Number of issues on track | + ## EpicIssue Relationship between an epic and an issue diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md index f769560d67f..67b1f7eb1f1 100644 --- a/doc/development/documentation/styleguide.md +++ b/doc/development/documentation/styleguide.md @@ -261,15 +261,6 @@ Do not include the same information in multiple places. [Link to a SSOT instead. Some features are also objects. For example, "GitLab's Merge Requests support X" and "Create a new merge request for Z." -- Use common contractions when it helps create a friendly and informal tone, especially in tutorials and [UIs](https://design.gitlab.com/content/punctuation/#contractions). - - Do use contractions like: _it's_, _can't_, _wouldn't_, _you're_, _you've_, _haven't_, don't, _we're_, _that's_, and _won't_. Contractions in instructional documentation such as tutorials can help create a friendly and informal tone. - - Avoid less common contractions such as: _he'd_, _it'll_, _should've_, and _there'd_. - - Do not use contractions in reference documentation. Examples: - - You cannot set a limit higher than 1000. - - For `parameter1`, the default is 10. - - Do not use contractions with a proper noun and a verb, such as _GitLab's creating X_. - - Avoid using contractions when you need to emphasize a negative, such as "Do **not** install X with Y." - - Avoid use of the future tense: - Instead of "after you execute this command, GitLab will display the result", use "after you execute this command, GitLab displays the result". - Only use the future tense to convey when the action or result will actually occur at a future time. @@ -286,6 +277,58 @@ as even native users of English might misunderstand them. - Instead of "e.g.", use "for example," "such as," "for instance," or "like." - Instead of "etc.", either use "and so on" or consider editing it out, since it can be vague. +### Contractions + +- Use common contractions when it helps create a friendly and informal tone, especially in tutorials, instructional documentation, and [UIs](https://design.gitlab.com/content/punctuation/#contractions). + +| Do | Don't | +|----------|-----------| +| it's | it is | +| can't | cannot | +| wouldn't | would not | +| you're | you are | +| you've | you have | +| haven't | have not | +| don't | do not | +| we're | we are | +| that's' | that is | +| won't | will not | + +- Avoid less common contractions: + +| Do | Don't | +|--------------|-------------| +| he would | he'd | +| it will | it'll | +| should have | should've | +| there would | there'd | + +- Do not use contractions with a proper noun and a verb. For example: + +| Do | Don't | +|----------------------|---------------------| +| GitLab is creating X | GitLab's creating X | + +- Do not use contractions when you need to emphasize a negative. For example: + +| Do | Don't | +|-----------------------------|----------------------------| +| Do **not** install X with Y | **Don't** install X with Y | + +- Do not use contractions in reference documentation. For example: + +| Do | Don't | +|------------------------------------------|----------------------------| +| Do **not** set a limit greater than 1000 | **Don't** set a limit greater than 1000 | +| For `parameter1`, the default is 10 | For `parameter1`, the default's 10 | + +- Avoid contractions in error messages. Examples: + +| Do | Don't | +|------------------------------------------|----------------------------| +| Requests to localhost are not allowed | Requests to localhost aren't allowed | +| Specified URL cannot be used | Specified URL can't be used | + ## Text - [Write in Markdown](#markdown). diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png b/doc/user/group/epics/img/epic_view_roadmap_v12.3.png Binary files differdeleted file mode 100644 index a17c56c618b..00000000000 --- a/doc/user/group/epics/img/epic_view_roadmap_v12.3.png +++ /dev/null diff --git a/doc/user/group/epics/img/epic_view_roadmap_v12_9.png b/doc/user/group/epics/img/epic_view_roadmap_v12_9.png Binary files differnew file mode 100644 index 00000000000..b85f1806123 --- /dev/null +++ b/doc/user/group/epics/img/epic_view_roadmap_v12_9.png diff --git a/doc/user/group/epics/index.md b/doc/user/group/epics/index.md index 421a43ba818..eae989b220b 100644 --- a/doc/user/group/epics/index.md +++ b/doc/user/group/epics/index.md @@ -182,7 +182,7 @@ If your epic contains one or more [child epics](#multi-level-child-epics-ultimat have a [start or due date](#start-date-and-due-date), a [roadmap](../roadmap/index.md) view of the child epics is listed under the parent epic. -![Child epics roadmap](img/epic_view_roadmap_v12.3.png) +![Child epics roadmap](img/epic_view_roadmap_v12_9.png) ## Reordering issues and child epics diff --git a/doc/user/group/roadmap/img/roadmap_view.png b/doc/user/group/roadmap/img/roadmap_view.png Binary files differdeleted file mode 100644 index 2be3849ba1b..00000000000 --- a/doc/user/group/roadmap/img/roadmap_view.png +++ /dev/null diff --git a/doc/user/group/roadmap/img/roadmap_view_v12_9.png b/doc/user/group/roadmap/img/roadmap_view_v12_9.png Binary files differnew file mode 100644 index 00000000000..3aa8cbb8332 --- /dev/null +++ b/doc/user/group/roadmap/img/roadmap_view_v12_9.png diff --git a/doc/user/group/roadmap/index.md b/doc/user/group/roadmap/index.md index a72cd990706..6eca9e84ce9 100644 --- a/doc/user/group/roadmap/index.md +++ b/doc/user/group/roadmap/index.md @@ -10,7 +10,12 @@ An Epic within a group containing **Start date** and/or **Due date** can be visualized in a form of a timeline (e.g. a Gantt chart). The Epics Roadmap page shows such a visualization for all the epics which are under a group and/or its subgroups. -![roadmap view](img/roadmap_view.png) +> [Introduced](https://gitlab.com/gitlab-org/gitlab/issues/5164) in GitLab 12.9. + +On the epic bars, you can see their title, progress, and completed weight percentage. +When you hover over an epic bar, a popover appears with its description, start and due dates, and weight completed. + +![roadmap view](img/roadmap_view_v12_9.png) A dropdown allows you to show only open or closed epics. By default, all epics are shown. @@ -68,11 +73,7 @@ the timeline header represent the days of the week. ## Timeline bar for an epic -The timeline bar indicates the approximate position of an epic based on its start -and due date. If an epic doesn't have a due date, the timeline bar fades -away towards the future. Similarly, if an epic doesn't have a start date, the -timeline bar becomes more visible as it approaches the epic's due date on the -timeline. +The timeline bar indicates the approximate position of an epic based on its start and due date. <!-- ## Troubleshooting diff --git a/doc/user/project/integrations/prometheus.md b/doc/user/project/integrations/prometheus.md index e1790bfc30c..3c2ca7e6e59 100644 --- a/doc/user/project/integrations/prometheus.md +++ b/doc/user/project/integrations/prometheus.md @@ -820,7 +820,7 @@ Prerequisites for embedding from a Grafana instance: ![Grafana Metric Panel](img/grafana_panel_v12_5.png) 1. In the upper-left corner of the page, select a specific value for each variable required for the queries in the chart. ![Select Query Variables](img/select_query_variables_v12_5.png) -1. In Grafana, click on a panel's title, then click **Share** to open the panel's sharing dialog to the **Link** tab. +1. In Grafana, click on a panel's title, then click **Share** to open the panel's sharing dialog to the **Link** tab. If you click the _dashboard's_ share panel instead, GitLab will attempt to embed the first supported panel on the dashboard (if available). 1. If your Prometheus queries use Grafana's custom template variables, ensure that "Template variables" option is toggled to **On**. Of Grafana global template variables, only `$__interval`, `$__from`, and `$__to` are currently supported. Toggle **On** the "Current time range" option to specify the time range of the chart. Otherwise, the default range will be the last 8 hours. ![Grafana Sharing Dialog](img/grafana_sharing_dialog_v12_5.png) 1. Click **Copy** to copy the URL to the clipboard. diff --git a/lib/banzai/filter/inline_grafana_metrics_filter.rb b/lib/banzai/filter/inline_grafana_metrics_filter.rb index 60a16b164af..07bde9858e8 100644 --- a/lib/banzai/filter/inline_grafana_metrics_filter.rb +++ b/lib/banzai/filter/inline_grafana_metrics_filter.rb @@ -17,8 +17,6 @@ module Banzai def embed_params(node) query_params = Gitlab::Metrics::Dashboard::Url.parse_query(node['href']) - return unless query_params.include?(:panelId) - time_window = Grafana::TimeWindow.new(query_params[:from], query_params[:to]) url = url_with_window(node['href'], query_params, time_window.in_milliseconds) diff --git a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml index ef77bbf5626..48458142f1c 100644 --- a/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml +++ b/lib/gitlab/ci/templates/Managed-Cluster-Applications.gitlab-ci.yml @@ -1,6 +1,6 @@ apply: stage: deploy - image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.9.0" + image: "registry.gitlab.com/gitlab-org/cluster-integration/cluster-applications:v0.11.0" environment: name: production variables: @@ -15,6 +15,7 @@ apply: JUPYTERHUB_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/jupyterhub/values.yaml PROMETHEUS_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/prometheus/values.yaml ELASTIC_STACK_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/elastic-stack/values.yaml + VAULT_VALUES_FILE: $CI_PROJECT_DIR/.gitlab/managed-apps/vault/values.yaml script: - gitlab-managed-apps /usr/local/share/gitlab-managed-apps/helmfile.yaml only: diff --git a/lib/gitlab/import_export/project/import_export.yml b/lib/gitlab/import_export/project/import_export.yml index b7b61df4cd6..aa6085de4f9 100644 --- a/lib/gitlab/import_export/project/import_export.yml +++ b/lib/gitlab/import_export/project/import_export.yml @@ -248,6 +248,7 @@ excluded_attributes: - :token_encrypted services: - :template + - :instance error_tracking_setting: - :encrypted_token - :encrypted_token_iv diff --git a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb index ce75c54d014..c90c1e3f0bc 100644 --- a/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb +++ b/lib/gitlab/metrics/dashboard/stages/grafana_formatter.rb @@ -13,12 +13,7 @@ module Gitlab # Reformats the specified panel in the Gitlab # dashboard-yml format def transform! - InputFormatValidator.new( - grafana_dashboard, - datasource, - panel, - query_params - ).validate! + validate_input! new_dashboard = formatted_dashboard @@ -28,6 +23,17 @@ module Gitlab private + def validate_input! + ::Grafana::Validator.new( + grafana_dashboard, + datasource, + panel, + query_params + ).validate! + rescue ::Grafana::Validator::Error => e + raise ::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError, e.message + end + def formatted_dashboard { panel_groups: [{ panels: [formatted_panel] }] } end @@ -56,11 +62,25 @@ module Gitlab def panel strong_memoize(:panel) do grafana_dashboard[:dashboard][:panels].find do |panel| - panel[:id].to_s == query_params[:panelId] + query_params[:panelId] ? matching_panel?(panel) : valid_panel?(panel) end end end + # Determines whether a given panel is the one + # specified by the linked grafana url + def matching_panel?(panel) + panel[:id].to_s == query_params[:panelId] + end + + # Determines whether any given panel has the potenial + # to return valid results from grafana/prometheus + def valid_panel?(panel) + ::Grafana::Validator + .new(grafana_dashboard, datasource, panel, query_params) + .valid? + end + # Grafana url query parameters. Includes information # on which panel to select and time range. def query_params @@ -141,83 +161,6 @@ module Gitlab 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 diff --git a/lib/grafana/validator.rb b/lib/grafana/validator.rb new file mode 100644 index 00000000000..760263f7ec9 --- /dev/null +++ b/lib/grafana/validator.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +# Performs checks on whether resources from Grafana can be handled +# We have certain restrictions on which formats we accept. +# Some are technical requirements, others are simplifications. +module Grafana + class Validator + Error = Class.new(StandardError) + + 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_panel_type! + validate_variable_definitions! + validate_global_variables! + validate_datasource! if datasource + end + + def valid? + validate! + + true + rescue ::Grafana::Validator::Error + false + end + + private + + # See defaults in Banzai::Filter::InlineGrafanaMetricsFilter. + def validate_query_params! + return if [:from, :to].all? { |param| query_params.include?(param) } + + raise_error 'Grafana query parameters must include from and to.' + end + + # We may choose to support other panel types in future. + def validate_panel_type! + return if panel && panel[:type] == 'graph' && panel[:lines] + + raise_error 'Panel type must be a line graph.' + end + + # We must require variable definitions to create valid prometheus queries. + 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 + + # We may choose to support further Grafana variables in future. + def validate_global_variables! + return unless panel_contains_unsupported_vars? + + raise_error "Prometheus must not include #{UNSUPPORTED_GRAFANA_GLOBAL_VARS}" + end + + # We may choose to support additional datasources in future. + 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 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 Validator::Error, message + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 210e4ca7c12..edff8701c58 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -220,6 +220,9 @@ msgstr "" msgid "%{commit_author_link} authored %{commit_timeago}" msgstr "" +msgid "%{completedWeight} of %{totalWeight} weight completed" +msgstr "" + msgid "%{cores} cores" msgstr "" @@ -600,6 +603,9 @@ msgid_plural "- Users" msgstr[0] "" msgstr[1] "" +msgid "- of - weight completed" +msgstr "" + msgid "- show less" msgstr "" @@ -7795,6 +7801,9 @@ msgstr "" msgid "Epics|An error occurred while saving the %{epicDateType} date" msgstr "" +msgid "Epics|An error occurred while updating labels." +msgstr "" + msgid "Epics|Are you sure you want to remove %{bStart}%{targetIssueTitle}%{bEnd} from %{bStart}%{parentEpicTitle}%{bEnd}?" msgstr "" @@ -9925,10 +9934,13 @@ msgstr "" msgid "Group: %{name}" msgstr "" -msgid "GroupRoadmap|%{startDateInWords} – %{endDateInWords}" +msgid "GroupRoadmap|%{dateWord} – No end date" +msgstr "" + +msgid "GroupRoadmap|%{startDateInWords} – %{endDateInWords}" msgstr "" -msgid "GroupRoadmap|From %{dateWord}" +msgid "GroupRoadmap|No start date – %{dateWord}" msgstr "" msgid "GroupRoadmap|Something went wrong while fetching epics" @@ -9949,9 +9961,6 @@ msgstr "" msgid "GroupRoadmap|To widen your search, change or remove filters; from %{startDate} to %{endDate}." msgstr "" -msgid "GroupRoadmap|Until %{dateWord}" -msgstr "" - msgid "GroupSAML|Certificate fingerprint" msgstr "" diff --git a/spec/controllers/boards/issues_controller_spec.rb b/spec/controllers/boards/issues_controller_spec.rb index 605fff60c31..41c37cb84e5 100644 --- a/spec/controllers/boards/issues_controller_spec.rb +++ b/spec/controllers/boards/issues_controller_spec.rb @@ -57,6 +57,18 @@ describe Boards::IssuesController do expect(development.issues.map(&:relative_position)).not_to include(nil) end + it 'returns issues by closed_at in descending order in closed list' do + create(:closed_issue, project: project, title: 'New Issue 1', closed_at: 1.day.ago) + create(:closed_issue, project: project, title: 'New Issue 2', closed_at: 1.week.ago) + + list_issues user: user, board: board, list: board.lists.last.id + + expect(response).to have_gitlab_http_status(:ok) + expect(json_response['issues'].length).to eq(2) + expect(json_response['issues'][0]['title']).to eq('New Issue 1') + expect(json_response['issues'][1]['title']).to eq('New Issue 2') + end + it 'avoids N+1 database queries' do create(:labeled_issue, project: project, labels: [development]) control_count = ActiveRecord::QueryRecorder.new { list_issues(user: user, board: board, list: list2) }.count diff --git a/spec/controllers/dashboard/projects_controller_spec.rb b/spec/controllers/dashboard/projects_controller_spec.rb index 7ae31fc67aa..a13b56deb23 100644 --- a/spec/controllers/dashboard/projects_controller_spec.rb +++ b/spec/controllers/dashboard/projects_controller_spec.rb @@ -86,58 +86,11 @@ describe Dashboard::ProjectsController do end describe 'GET /starred.json' do - subject { get :starred, format: :json } - - let(:projects) { create_list(:project, 2, creator: user) } - before do - allow(Kaminari.config).to receive(:default_per_page).and_return(1) - - projects.each do |project| - project.add_developer(user) - create(:users_star_project, project_id: project.id, user_id: user.id) - end - end - - it 'returns success' do - subject - - expect(response).to have_gitlab_http_status(:ok) + get :starred, format: :json end - it 'paginates the records' do - subject - - expect(assigns(:projects).count).to eq(1) - end - end - end - - context 'atom requests' do - let(:user) { create(:user) } - - before do - sign_in(user) - end - - describe '#index' do - context 'project pagination' do - let(:projects) { create_list(:project, 2, creator: user) } - - before do - allow(Kaminari.config).to receive(:default_per_page).and_return(1) - - projects.each do |project| - project.add_developer(user) - end - end - - it 'does not paginate projects, even if page number is passed' do - get :index, format: :atom - - expect(assigns(:events).count).to eq(2) - end - end + it { is_expected.to respond_with(:success) } end end end diff --git a/spec/factories/services.rb b/spec/factories/services.rb index ffa51abf26f..ebab370ccf6 100644 --- a/spec/factories/services.rb +++ b/spec/factories/services.rb @@ -4,6 +4,11 @@ FactoryBot.define do factory :service do project type { 'Service' } + + trait :instance do + project { nil } + instance { true } + end end factory :custom_issue_tracker_service, class: 'CustomIssueTrackerService' do diff --git a/spec/features/boards/issue_ordering_spec.rb b/spec/features/boards/issue_ordering_spec.rb index 62abd914fcb..4c723ddf324 100644 --- a/spec/features/boards/issue_ordering_spec.rb +++ b/spec/features/boards/issue_ordering_spec.rb @@ -47,6 +47,31 @@ describe 'Issue Boards', :js do end end + context 'closed issues' do + let!(:issue7) { create(:closed_issue, project: project, title: 'Closed issue 1', closed_at: 1.day.ago) } + let!(:issue8) { create(:closed_issue, project: project, title: 'Closed issue 2', closed_at: 1.week.ago) } + let!(:issue9) { create(:closed_issue, project: project, title: 'Closed issue 3', closed_at: 2.weeks.ago) } + + before do + visit project_board_path(project, board) + wait_for_requests + + expect(page).to have_selector('.board', count: 3) + end + + it 'orders issues by closed_at' do + wait_for_requests + + page.within(find('.board:nth-child(3)')) do + first, second, third = all('.board-card').to_a + + expect(first).to have_content(issue7.title) + expect(second).to have_content(issue8.title) + expect(third).to have_content(issue9.title) + end + end + end + context 'ordering in list' do before do visit project_board_path(project, board) diff --git a/spec/fixtures/lib/gitlab/import_export/light/project.json b/spec/fixtures/lib/gitlab/import_export/light/project.json index 2971ca0f0f8..51e2e9ac623 100644 --- a/spec/fixtures/lib/gitlab/import_export/light/project.json +++ b/spec/fixtures/lib/gitlab/import_export/light/project.json @@ -111,6 +111,28 @@ "active": false, "properties": {}, "template": true, + "instance": false, + "push_events": true, + "issues_events": true, + "merge_requests_events": true, + "tag_push_events": true, + "note_events": true, + "job_events": true, + "type": "TeamcityService", + "category": "ci", + "default": false, + "wiki_page_events": true + }, + { + "id": 101, + "title": "JetBrains TeamCity CI", + "project_id": 5, + "created_at": "2016-06-14T15:01:51.315Z", + "updated_at": "2016-06-14T15:01:51.315Z", + "active": false, + "properties": {}, + "template": false, + "instance": true, "push_events": true, "issues_events": true, "merge_requests_events": true, diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js index 797c52cd31b..02b59d46c71 100644 --- a/spec/frontend/monitoring/components/charts/time_series_spec.js +++ b/spec/frontend/monitoring/components/charts/time_series_spec.js @@ -69,7 +69,7 @@ describe('Time series component', () => { mockedQueryResultFixture, ); // dashboard is a dynamically generated fixture and stored at environment_metrics_dashboard.json - [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[0].panels; + [mockGraphData] = store.state.monitoringDashboard.dashboard.panelGroups[1].panels; }); describe('general functions', () => { diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js index 777181df10e..5a14ffc03f2 100644 --- a/spec/frontend/monitoring/store/getters_spec.js +++ b/spec/frontend/monitoring/store/getters_spec.js @@ -89,8 +89,8 @@ describe('Monitoring store Getters', () => { expect(getMetricStates()).toEqual([metricStates.OK]); // Filtered by groups - expect(getMetricStates(state.dashboard.panelGroups[0].key)).toEqual([metricStates.OK]); - expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([]); + expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([metricStates.OK]); + expect(getMetricStates(state.dashboard.panelGroups[2].key)).toEqual([]); }); it('on multiple metrics errors', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](state, metricsDashboardPayload); @@ -118,18 +118,18 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixture); // An error in 2 groups mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[0].panels[1].metrics[0].metricId, + metricId: groups[1].panels[1].metrics[0].metricId, }); mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, { - metricId: groups[1].panels[0].metrics[0].metricId, + metricId: groups[2].panels[0].metrics[0].metricId, }); expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]); - expect(getMetricStates(groups[0].key)).toEqual([ + expect(getMetricStates(groups[1].key)).toEqual([ metricStates.OK, metricStates.UNKNOWN_ERROR, ]); - expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]); + expect(getMetricStates(groups[2].key)).toEqual([metricStates.UNKNOWN_ERROR]); }); }); }); @@ -210,13 +210,13 @@ describe('Monitoring store Getters', () => { mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, mockedQueryResultFixtureStatusCode); // First group has metrics - expect(metricsWithData(state.dashboard.panelGroups[0].key)).toEqual([ + expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([ mockedQueryResultFixture.metricId, mockedQueryResultFixtureStatusCode.metricId, ]); // Second group has no metrics - expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([]); + expect(metricsWithData(state.dashboard.panelGroups[2].key)).toEqual([]); }); }); }); diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js index a94de5494e5..5a79b8ef49c 100644 --- a/spec/frontend/monitoring/store/mutations_spec.js +++ b/spec/frontend/monitoring/store/mutations_spec.js @@ -32,12 +32,13 @@ describe('Monitoring mutations', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); const groups = getGroups(); - expect(groups[0].key).toBe('response-metrics-nginx-ingress-vts-0'); - expect(groups[1].key).toBe('response-metrics-nginx-ingress-1'); + expect(groups[0].key).toBe('system-metrics-kubernetes-0'); + expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1'); + expect(groups[2].key).toBe('response-metrics-nginx-ingress-2'); }); it('normalizes values', () => { mutations[types.RECEIVE_METRICS_DATA_SUCCESS](stateCopy, payload); - const expectedLabel = '5xx Errors (%)'; + const expectedLabel = 'Pod average (MB)'; const { label, queryRange } = getGroups()[0].panels[2].metrics[0]; expect(label).toEqual(expectedLabel); @@ -51,7 +52,7 @@ describe('Monitoring mutations', () => { expect(groups).toBeDefined(); expect(groups).toHaveLength(6); - expect(groups[0].panels).toHaveLength(3); + expect(groups[0].panels).toHaveLength(7); expect(groups[0].panels[0].metrics).toHaveLength(1); expect(groups[0].panels[1].metrics).toHaveLength(1); expect(groups[0].panels[2].metrics).toHaveLength(1); @@ -65,9 +66,12 @@ describe('Monitoring mutations', () => { const groups = getGroups(); expect(groups[0].panels[0].metrics[0].metricId).toEqual( - 'undefined_response_metrics_nginx_ingress_throughput_status_code', + 'undefined_system_metrics_kubernetes_container_memory_total', ); expect(groups[1].panels[0].metrics[0].metricId).toEqual( + 'undefined_response_metrics_nginx_ingress_throughput_status_code', + ); + expect(groups[2].panels[0].metrics[0].metricId).toEqual( 'undefined_response_metrics_nginx_ingress_16_throughput_status_code', ); }); @@ -135,7 +139,7 @@ describe('Monitoring mutations', () => { }, ]; const dashboard = metricsDashboardPayload; - const getMetric = () => stateCopy.dashboard.panelGroups[0].panels[0].metrics[0]; + const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0]; describe('REQUEST_METRIC_RESULT', () => { beforeEach(() => { diff --git a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb index 2092b9e9db8..28bf5bd3e92 100644 --- a/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb +++ b/spec/lib/banzai/filter/inline_grafana_metrics_filter_spec.rb @@ -46,11 +46,9 @@ describe Banzai::Filter::InlineGrafanaMetricsFilter do end context 'when "panelId" parameter is missing' do - let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' } + let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis?from=1570397739557&to=1570484139557' } - it 'leaves the markdown unchanged' do - expect(unescape(doc.to_s)).to eq(input) - end + it_behaves_like 'a metrics embed filter' end context 'when time window parameters are missing' do @@ -86,6 +84,14 @@ describe Banzai::Filter::InlineGrafanaMetricsFilter do end end + context 'when no parameters are provided' do + let(:dashboard_path) { '/d/XDaNK6amz/gitlab-omnibus-redis' } + + it 'inserts a placeholder' do + expect(embed_url).to be_present + end + end + private # Nokogiri escapes the URLs, but we don't care about that diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index 7bc17b804df..9c2b202d5bb 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -703,6 +703,12 @@ describe Gitlab::ImportExport::Project::TreeRestorer do expect(project.services.where(template: true).count).to eq(0) end + it 'does not import any instance services' do + expect(restored_project_json).to eq(true) + + expect(project.services.where(instance: true).count).to eq(0) + end + it 'imports labels' do create(:group_label, name: 'Another label', group: project.group) diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml index bbd83975f11..91b88349ee0 100644 --- a/spec/lib/gitlab/import_export/safe_model_attributes.yml +++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml @@ -459,6 +459,7 @@ Service: - active - properties - template +- instance - push_events - issues_events - commit_events diff --git a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb index 5c2ec6dae6b..e41004bb57e 100644 --- a/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb +++ b/spec/lib/gitlab/metrics/dashboard/stages/grafana_formatter_spec.rb @@ -11,8 +11,9 @@ describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do 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(:expected_dashboard) { JSON.parse(fixture_file('grafana/expected_grafana_embed.json'), symbolize_names: true) } - let(:dashboard) { described_class.new(project, {}, params).transform! } + subject(:dashboard) { described_class.new(project, {}, params).transform! } let(:params) do { @@ -23,83 +24,34 @@ describe Gitlab::Metrics::Dashboard::Stages::GrafanaFormatter do 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 + it { is_expected.to eq expected_dashboard } 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' + context 'when a panelId is not included in the grafana_url' do + before do + params[:grafana_url].gsub('&panelId=8', '') 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 + it { is_expected.to eq expected_dashboard } - context 'when query param "to" is not specified' do + context 'when there is also no valid panel in the dashboard' do before do - params[:grafana_url].gsub!('to=1570484139557', '') + params[:grafana_dashboard][:dashboard][:panels] = [] 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' + it 'raises a processing error' do + expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) end - - it_behaves_like 'processing error' end + 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' + context 'when an input is invalid' do + before do + params[:datasource][:access] = 'not-proxy' 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' + it 'raises a processing error' do + expect { dashboard }.to raise_error(::Gitlab::Metrics::Dashboard::Errors::DashboardProcessingError) end end end diff --git a/spec/lib/grafana/validator_spec.rb b/spec/lib/grafana/validator_spec.rb new file mode 100644 index 00000000000..603e27fd0c0 --- /dev/null +++ b/spec/lib/grafana/validator_spec.rb @@ -0,0 +1,119 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Grafana::Validator 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(:panel) { grafana_dashboard[:dashboard][:panels].first } + + let(:query_params) do + { + from: '1570397739557', + to: '1570484139557', + panelId: '8', + 'var-instance': 'localhost:9121' + } + end + + describe 'validate!' do + shared_examples_for 'processing error' do |message| + it 'raises a processing error' do + expect { subject } + .to raise_error(::Grafana::Validator::Error, message) + end + end + + subject { described_class.new(grafana_dashboard, datasource, panel, query_params).validate! } + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + + context 'when query param "from" is not specified' do + before do + query_params.delete(:from) + end + + it_behaves_like 'processing error', 'Grafana query parameters must include from and to.' + end + + context 'when query param "to" is not specified' do + before do + query_params.delete(:to) + end + + it_behaves_like 'processing error', 'Grafana query parameters must include from and to.' + end + + context 'when the panel is not provided' do + let(:panel) { nil } + + it_behaves_like 'processing error', 'Panel type must be a line graph.' + end + + context 'when the panel is not a graph' do + before do + panel[:type] = 'singlestat' + end + + it_behaves_like 'processing error', 'Panel type must be a line graph.' + end + + context 'when the panel is not a line graph' do + before do + panel[:lines] = false + end + + it_behaves_like 'processing error', 'Panel type must be a line graph.' + end + + context 'when the query dashboard includes undefined variables' do + before do + query_params.delete(:'var-instance') + end + + it_behaves_like 'processing error', 'All Grafana variables must be defined in the query parameters.' + end + + context 'when the expression contains unsupported global variables' do + before do + grafana_dashboard[:dashboard][:panels][0][:targets][0][:expr] = 'sum(important_metric[$__interval_ms])' + end + + it_behaves_like 'processing error', "Prometheus must not include #{described_class::UNSUPPORTED_GRAFANA_GLOBAL_VARS}" + end + + context 'when the datasource is not proxyable' do + before do + datasource[:access] = 'not-proxy' + end + + it_behaves_like 'processing error', 'Only Prometheus datasources with proxy access in Grafana are supported.' + end + + # Skipping datasource validation allows for checks to be + # run without a secondary call to Grafana API + context 'when the datasource is not provided' do + let(:datasource) { nil } + + it 'does not raise an error' do + expect { subject }.not_to raise_error + end + end + end + + describe 'valid?' do + subject { described_class.new(grafana_dashboard, datasource, panel, query_params).valid? } + + context 'with valid arguments' do + it { is_expected.to be true } + end + + context 'with invalid arguments' do + let(:query_params) { {} } + + it { is_expected.to be false } + end + end +end diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb index 2bc8bdaed7c..825e5f5902f 100644 --- a/spec/models/service_spec.rb +++ b/spec/models/service_spec.rb @@ -18,6 +18,20 @@ describe Service do expect(build(:service, project_id: nil, template: false)).to be_invalid end + it 'validates presence of project_id if not instance', :aggregate_failures do + expect(build(:service, project_id: nil, instance: true)).to be_valid + expect(build(:service, project_id: nil, instance: false)).to be_invalid + end + + it 'validates absence of project_id if instance', :aggregate_failures do + expect(build(:service, project_id: nil, instance: true)).to be_valid + expect(build(:service, instance: true)).to be_invalid + end + + it 'validates service is template or instance' do + expect(build(:service, project_id: nil, template: true, instance: true)).to be_invalid + end + context 'with an existing service template' do before do create(:service, type: 'Service', template: true) @@ -27,6 +41,16 @@ describe Service do expect(build(:service, type: 'Service', template: true)).to be_invalid end end + + context 'with an existing instance service' do + before do + create(:service, :instance) + end + + it 'validates only one service instance per type' do + expect(build(:service, :instance)).to be_invalid + end + end end describe 'Scopes' do diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb index 931b67b2950..33538703e92 100644 --- a/spec/services/boards/issues/list_service_spec.rb +++ b/spec/services/boards/issues/list_service_spec.rb @@ -33,11 +33,11 @@ describe Boards::Issues::ListService do let!(:list1_issue3) { create(:labeled_issue, project: project, milestone: m1, labels: [development, p1]) } let!(:list2_issue1) { create(:labeled_issue, project: project, milestone: m1, labels: [testing]) } - let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) } - let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) } - let!(:closed_issue3) { create(:issue, :closed, project: project) } - let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) } - let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development]) } + let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug], closed_at: 1.day.ago) } + let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3], closed_at: 2.days.ago) } + let!(:closed_issue3) { create(:issue, :closed, project: project, closed_at: 1.week.ago) } + let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1], closed_at: 1.year.ago) } + let!(:closed_issue5) { create(:labeled_issue, :closed, project: project, labels: [development], closed_at: 2.years.ago) } let(:parent) { project } @@ -94,11 +94,11 @@ describe Boards::Issues::ListService do let!(:list1_issue3) { create(:labeled_issue, project: project1, milestone: m1, labels: [development, p1, p1_project1]) } let!(:list2_issue1) { create(:labeled_issue, project: project1, milestone: m1, labels: [testing]) } - let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) } - let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3, p3_project]) } - let!(:closed_issue3) { create(:issue, :closed, project: project1) } - let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1]) } - let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development]) } + let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug], closed_at: 1.day.ago) } + let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3, p3_project], closed_at: 2.days.ago) } + let!(:closed_issue3) { create(:issue, :closed, project: project1, closed_at: 1.week.ago) } + let!(:closed_issue4) { create(:labeled_issue, :closed, project: project1, labels: [p1, p1_project1], closed_at: 1.year.ago) } + let!(:closed_issue5) { create(:labeled_issue, :closed, project: project1, labels: [development], closed_at: 2.years.ago) } before do group.add_developer(user) diff --git a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb index ec1c58e5b67..756c4136059 100644 --- a/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_list_service_shared_examples.rb @@ -36,20 +36,22 @@ RSpec.shared_examples 'issues list service' do expect(issues).to eq [opened_issue2, reopened_issue1, opened_issue1] end - it 'returns closed issues when listing issues from Closed' do - params = { board_id: board.id, id: closed.id } + it 'returns opened issues that have label list applied when listing issues from a label list' do + params = { board_id: board.id, id: list1.id } issues = described_class.new(parent, user, params).execute - expect(issues).to eq [closed_issue4, closed_issue2, closed_issue5, closed_issue3, closed_issue1] + expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] end + end - it 'returns opened issues that have label list applied when listing issues from a label list' do - params = { board_id: board.id, id: list1.id } + context 'issues are ordered by date of closing' do + it 'returns closed issues when listing issues from Closed' do + params = { board_id: board.id, id: closed.id } issues = described_class.new(parent, user, params).execute - expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2] + expect(issues).to eq [closed_issue1, closed_issue2, closed_issue3, closed_issue4, closed_issue5] end end diff --git a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb index a4f81097ffa..48e4b4a18fd 100644 --- a/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb +++ b/spec/support/shared_examples/services/metrics/dashboard_shared_examples.rb @@ -67,6 +67,7 @@ RSpec.shared_examples 'valid dashboard cloning process' do |dashboard_template, it 'delegates commit creation to Files::CreateService', :aggregate_failures do service_instance = instance_double(::Files::CreateService) + allow(::Gitlab::Metrics::Dashboard::Processor).to receive(:new).and_return(double(process: file_content_hash)) expect(::Files::CreateService).to receive(:new).with(project, user, dashboard_attrs).and_return(service_instance) expect(service_instance).to receive(:execute).and_return(status: :success) |