提交 c417764f 编写于 作者: G GitLab Bot

Add latest changes from gitlab-org/gitlab@master

上级 cceb99c0
......@@ -88,7 +88,7 @@ export default {
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
<div class="dropdown-title text-center">
<div v-if="isSidebar" class="dropdown-title text-center">
<span class="alert-title">{{ s__('AlertManagement|Assign status') }}</span>
<gl-button
:aria-label="__('Close')"
......
......@@ -4,8 +4,11 @@ import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '../user_avatar/user_avatar_image.vue';
import { glEmojiTag } from '../../../emoji';
const MAX_SKELETON_LINES = 4;
export default {
name: 'UserPopover',
maxSkeletonLines: MAX_SKELETON_LINES,
components: {
Icon,
GlPopover,
......@@ -22,11 +25,6 @@ export default {
required: true,
default: null,
},
loaded: {
type: Boolean,
required: false,
default: false,
},
},
computed: {
statusHtml() {
......@@ -42,14 +40,8 @@ export default {
return '';
},
nameIsLoading() {
return !this.user.name;
},
workInformationIsLoading() {
return !this.user.loaded && this.user.workInformation === null;
},
locationIsLoading() {
return !this.user.loaded && this.user.location === null;
userIsLoading() {
return !this.user?.loaded;
},
},
};
......@@ -63,49 +55,43 @@ export default {
<user-avatar-image :img-src="user.avatarUrl" :size="60" css-classes="mr-2" />
</div>
<div class="p-1 w-100">
<h5 class="m-0">
<span v-if="user.name">{{ user.name }}</span>
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</h5>
<div class="text-secondary mb-2">
<span v-if="user.username">@{{ user.username }}</span>
<gl-skeleton-loading v-else :lines="1" class="animation-container-small mb-1" />
</div>
<div class="text-secondary">
<div v-if="user.bio" class="d-flex mb-1">
<icon name="profile" class="category-icon flex-shrink-0" />
<span ref="bio" class="ml-1">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="d-flex mb-1">
<icon
v-show="!workInformationIsLoading"
name="work"
class="category-icon flex-shrink-0"
/>
<span ref="workInformation" class="ml-1">{{ user.workInformation }}</span>
</div>
<template v-if="userIsLoading">
<!-- `gl-skeleton-loading` does not support equal length lines -->
<!-- This can be migrated to `gl-skeleton-loader` when https://gitlab.com/gitlab-org/gitlab-ui/-/issues/872 is completed -->
<gl-skeleton-loading
v-if="workInformationIsLoading"
v-for="n in $options.maxSkeletonLines"
:key="n"
:lines="1"
class="animation-container-small mb-1"
/>
</div>
<div class="js-location text-secondary d-flex">
<icon
v-show="!locationIsLoading && user.location"
name="location"
class="category-icon flex-shrink-0"
/>
<span v-if="user.location" class="ml-1">{{ user.location }}</span>
<gl-skeleton-loading
v-if="locationIsLoading"
:lines="1"
class="animation-container-small mb-1"
/>
</div>
<div v-if="statusHtml" class="js-user-status mt-2">
<span v-html="statusHtml"></span>
</div>
</template>
<template v-else>
<div class="mb-2">
<h5 class="m-0">
{{ user.name }}
</h5>
<span class="text-secondary">@{{ user.username }}</span>
</div>
<div class="text-secondary">
<div v-if="user.bio" class="d-flex mb-1">
<icon name="profile" class="category-icon flex-shrink-0" />
<span ref="bio" class="ml-1">{{ user.bio }}</span>
</div>
<div v-if="user.workInformation" class="d-flex mb-1">
<icon name="work" class="category-icon flex-shrink-0" />
<span ref="workInformation" class="ml-1">{{ user.workInformation }}</span>
</div>
</div>
<div class="js-location text-secondary d-flex">
<div v-if="user.location">
<icon name="location" class="category-icon flex-shrink-0" />
<span class="ml-1">{{ user.location }}</span>
</div>
</div>
<div v-if="statusHtml" class="js-user-status mt-2">
<span v-html="statusHtml"></span>
</div>
</template>
</div>
</div>
</gl-popover>
......
......@@ -22,6 +22,7 @@ class ApplicationController < ActionController::Base
include Impersonation
include Gitlab::Logging::CloudflareHelper
include Gitlab::Utils::StrongMemoize
include ControllerWithFeatureCategory
before_action :authenticate_user!, except: [:route_not_found]
before_action :enforce_terms!, if: :should_enforce_terms?
......
# frozen_string_literal: true
module ControllerWithFeatureCategory
extend ActiveSupport::Concern
include Gitlab::ClassAttributes
class_methods do
def feature_category(category, config = {})
validate_config!(config)
category_config = Config.new(category, config[:only], config[:except], config[:if], config[:unless])
# Add the config to the beginning. That way, the last defined one takes precedence.
feature_category_configuration.unshift(category_config)
end
def feature_category_for_action(action)
category_config = feature_category_configuration.find { |config| config.matches?(action) }
category_config&.category || superclass_feature_category_for_action(action)
end
private
def validate_config!(config)
invalid_keys = config.keys - [:only, :except, :if, :unless]
if invalid_keys.any?
raise ArgumentError, "unknown arguments: #{invalid_keys} "
end
if config.key?(:only) && config.key?(:except)
raise ArgumentError, "cannot configure both `only` and `except`"
end
end
def feature_category_configuration
class_attributes[:feature_category_config] ||= []
end
def superclass_feature_category_for_action(action)
return unless superclass.respond_to?(:feature_category_for_action)
superclass.feature_category_for_action(action)
end
end
end
# frozen_string_literal: true
module ControllerWithFeatureCategory
class Config
attr_reader :category
def initialize(category, only, except, if_proc, unless_proc)
@category = category.to_sym
@only, @except = only&.map(&:to_s), except&.map(&:to_s)
@if_proc, @unless_proc = if_proc, unless_proc
end
def matches?(action)
included?(action) && !excluded?(action) &&
if_proc?(action) && !unless_proc?(action)
end
private
attr_reader :only, :except, :if_proc, :unless_proc
def if_proc?(action)
if_proc.nil? || if_proc.call(action)
end
def unless_proc?(action)
unless_proc.present? && unless_proc.call(action)
end
def included?(action)
only.nil? || only.include?(action)
end
def excluded?(action)
except.present? && except.include?(action)
end
end
end
......@@ -45,6 +45,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
feature_category :source_code_management,
unless: -> (action) { action.ends_with?("_reports") }
feature_category :code_testing,
only: [:test_reports, :coverage_reports, :terraform_reports]
feature_category :accessibility_testing,
only: [:accessibility_reports]
def index
@merge_requests = @issuables
......
......@@ -60,6 +60,7 @@ module AlertManagement
metadata << list_item('Service', service) if service
metadata << list_item('Monitoring tool', monitoring_tool) if monitoring_tool
metadata << list_item('Hosts', host_links) if hosts.any?
metadata << list_item('Description', description) if description.present?
metadata.join(MARKDOWN_LINE_BREAK)
end
......
......@@ -2,6 +2,7 @@
module WorkerAttributes
extend ActiveSupport::Concern
include Gitlab::ClassAttributes
# Resource boundaries that workers can declare through the
# `resource_boundary` attribute
......@@ -30,24 +31,24 @@ module WorkerAttributes
}.stringify_keys.freeze
class_methods do
def feature_category(value)
def feature_category(value, *extras)
raise "Invalid category. Use `feature_category_not_owned!` to mark a worker as not owned" if value == :not_owned
worker_attributes[:feature_category] = value
class_attributes[:feature_category] = value
end
# Special case: mark this work as not associated with a feature category
# this should be used for cross-cutting concerns, such as mailer workers.
def feature_category_not_owned!
worker_attributes[:feature_category] = :not_owned
class_attributes[:feature_category] = :not_owned
end
def get_feature_category
get_worker_attribute(:feature_category)
get_class_attribute(:feature_category)
end
def feature_category_not_owned?
get_worker_attribute(:feature_category) == :not_owned
get_feature_category == :not_owned
end
# This should be set to :high for jobs that need to be run
......@@ -61,11 +62,11 @@ module WorkerAttributes
def urgency(urgency)
raise "Invalid urgency: #{urgency}" unless VALID_URGENCIES.include?(urgency)
worker_attributes[:urgency] = urgency
class_attributes[:urgency] = urgency
end
def get_urgency
worker_attributes[:urgency] || :low
class_attributes[:urgency] || :low
end
# Set this attribute on a job when it will call to services outside of the
......@@ -73,85 +74,64 @@ module WorkerAttributes
# doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies for
# details
def worker_has_external_dependencies!
worker_attributes[:external_dependencies] = true
class_attributes[:external_dependencies] = true
end
# Returns a truthy value if the worker has external dependencies.
# See doc/development/sidekiq_style_guide.md#Jobs-with-External-Dependencies
# for details
def worker_has_external_dependencies?
worker_attributes[:external_dependencies]
class_attributes[:external_dependencies]
end
def worker_resource_boundary(boundary)
raise "Invalid boundary" unless VALID_RESOURCE_BOUNDARIES.include? boundary
worker_attributes[:resource_boundary] = boundary
class_attributes[:resource_boundary] = boundary
end
def get_worker_resource_boundary
worker_attributes[:resource_boundary] || :unknown
class_attributes[:resource_boundary] || :unknown
end
def idempotent!
worker_attributes[:idempotent] = true
class_attributes[:idempotent] = true
end
def idempotent?
worker_attributes[:idempotent]
class_attributes[:idempotent]
end
def weight(value)
worker_attributes[:weight] = value
class_attributes[:weight] = value
end
def get_weight
worker_attributes[:weight] ||
class_attributes[:weight] ||
NAMESPACE_WEIGHTS[queue_namespace] ||
1
end
def tags(*values)
worker_attributes[:tags] = values
class_attributes[:tags] = values
end
def get_tags
Array(worker_attributes[:tags])
Array(class_attributes[:tags])
end
def deduplicate(strategy, options = {})
worker_attributes[:deduplication_strategy] = strategy
worker_attributes[:deduplication_options] = options
class_attributes[:deduplication_strategy] = strategy
class_attributes[:deduplication_options] = options
end
def get_deduplicate_strategy
worker_attributes[:deduplication_strategy] ||
class_attributes[:deduplication_strategy] ||
Gitlab::SidekiqMiddleware::DuplicateJobs::DuplicateJob::DEFAULT_STRATEGY
end
def get_deduplication_options
worker_attributes[:deduplication_options] || {}
end
protected
# Returns a worker attribute declared on this class or its parent class.
# This approach allows declared attributes to be inherited by
# child classes.
def get_worker_attribute(name)
worker_attributes[name] || superclass_worker_attributes(name)
end
private
def worker_attributes
@attributes ||= {}
end
def superclass_worker_attributes(name)
return unless superclass.include? WorkerAttributes
superclass.get_worker_attribute(name)
class_attributes[:deduplication_options] || {}
end
end
end
---
title: Move monitor stage usage activity to CE
merge_request: 36067
author:
type: changed
---
title: Rework hardening CI pipelines usage data queries with an index
merge_request: 35494
author:
type: performance
---
title: Fix rendering alert issue description field.
merge_request: 35862
author:
type: fixed
---
title: Hide dropdown header on list view
merge_request: 35954
author:
type: other
# frozen_string_literal: true
def gitlab_danger
@gitlab_danger ||= GitlabDanger.new(helper.gitlab_helper)
end
docs_paths_to_review = helper.changes_by_category[:docs]
unless docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
'from the Technical Writing team.'
return if docs_paths_to_review.empty?
message 'This merge request adds or changes files that require a review ' \
'from the Technical Writing team.'
return unless gitlab_danger.ci?
if GitlabDanger.new(helper.gitlab_helper).ci?
markdown(<<~MARKDOWN)
## Documentation review
markdown(<<~MARKDOWN)
## Documentation review
The following files require a review from a technical writer:
The following files require a review from a technical writer:
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
* #{docs_paths_to_review.map { |path| "`#{path}`" }.join("\n* ")}
The review does not need to block merging this merge request. See the:
The review does not need to block merging this merge request. See the:
- [DevOps stages](https://about.gitlab.com/handbook/product/categories/#devops-stages) for the appropriate technical writer for this review.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN
- [Technical Writers assignments](https://about.gitlab.com/handbook/engineering/technical-writing/#designated-technical-writers) for the appropriate technical writer for this review.
- [Documentation workflows](https://docs.gitlab.com/ee/development/documentation/workflow.html) for information on when to assign a merge request for review.
MARKDOWN
unless gitlab.mr_labels.include?('documentation')
warn 'This merge request is missing the ~documentation label.'
end
end
unless gitlab.mr_labels.include?('documentation')
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
labels: (gitlab.mr_labels + ['documentation']).join(','))
end
......@@ -62,6 +62,8 @@ changes = helper.changes_by_category
# Ignore any files that are known but uncategorized. Prompt for any unknown files
changes.delete(:none)
# To reinstate roulette for documentation, remove this line.
changes.delete(:docs)
categories = changes.keys - [:unknown]
# Ensure to spin for database reviewer/maintainer when ~database is applied (e.g. to review SQL queries)
......
# frozen_string_literal: true
class AddIndexOnUserIdAndCreatedAtWhereSourceToCiPipelines < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
add_concurrent_index :ci_pipelines, [:user_id, :created_at, :config_source]
end
def down
remove_concurrent_index :ci_pipelines, [:user_id, :created_at, :config_source]
end
end
......@@ -18764,6 +18764,8 @@ CREATE INDEX index_ci_pipelines_on_project_idandrefandiddesc ON public.ci_pipeli
CREATE INDEX index_ci_pipelines_on_status ON public.ci_pipelines USING btree (status);
CREATE INDEX index_ci_pipelines_on_user_id_and_created_at_and_config_source ON public.ci_pipelines USING btree (user_id, created_at, config_source);
CREATE INDEX index_ci_pipelines_on_user_id_and_created_at_and_source ON public.ci_pipelines USING btree (user_id, created_at, source);
CREATE UNIQUE INDEX index_ci_refs_on_project_id_and_ref_path ON public.ci_refs USING btree (project_id, ref_path);
......@@ -23573,6 +23575,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200630110826
20200702123805
20200703154822
20200704143633
20200706005325
\.
......@@ -260,10 +260,6 @@ For example, in a Ruby application, you would need to have this script:
Then, when your app is deployed via GitLab CI/CD, those variables should get
replaced with their real values.
NOTE: **Note:**
Future enhancements [are planned](https://gitlab.com/gitlab-org/gitlab/-/issues/11322)
to make this process even easier.
### Determining merge request ID
The visual review tools retrieve the merge request ID from the `data-merge-request-id`
......
......@@ -648,6 +648,13 @@ appear to be associated to any of the services running, since they all appear to
| `projects_slack_slash_active` | `usage_activity_by_stage` | `configure` | | EE | Unique projects with Slack '/' commands enabled |
| `projects_with_prometheus_alerts` | `usage_activity_by_stage` | `configure` | | EE | Projects with Prometheus enabled and no alerts |
| `deploy_keys` | `usage_activity_by_stage` | `create` | | | |
| `clusters` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
| `clusters_applications_prometheus` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
| `operations_dashboard_default_dashboard` | `usage_activity_by_stage` | `monitor` | | CE+EE | |
| `operations_dashboard_users_with_projects_added` | `usage_activity_by_stage` | `monitor` | | EE | |
| `projects_prometheus_active` | `usage_activity_by_stage` | `monitor` | | EE | |
| `projects_with_error_tracking_enabled` | `usage_activity_by_stage` | `monitor` | | EE | |
| `projects_with_tracing_enabled` | `usage_activity_by_stage` | `monitor` | | EE | |
| `keys` | `usage_activity_by_stage` | `create` | | | |
| `projects_jira_dvcs_server_active` | `usage_activity_by_stage` | `plan` | | | |
| `service_desk_enabled_projects` | `usage_activity_by_stage` | `plan` | | | |
......
......@@ -554,7 +554,7 @@ Some domains cannot be restricted. These are the most popular public email domai
To enable this feature:
1. Navigate to the group's **Settings > General** page.
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field. You can enter multiple domains by separating each domain with a comma (,).
1. Expand the **Permissions, LFS, 2FA** section, and enter the domain names into **Restrict membership by email** field.
1. Click **Save changes**.
This will enable the domain-checking for all new users added to the group from this moment on.
......
# frozen_string_literal: true
module Gitlab
module ClassAttributes
extend ActiveSupport::Concern
class_methods do
protected
# Returns an attribute declared on this class or its parent class.
# This approach allows declared attributes to be inherited by
# child classes.
def get_class_attribute(name)
class_attributes[name] || superclass_attributes(name)
end
private
def class_attributes
@class_attributes ||= {}
end
def superclass_attributes(name)
return unless superclass.include? Gitlab::ClassAttributes
superclass.get_class_attribute(name)
end
end
end
end
......@@ -102,8 +102,8 @@ module Gitlab
}.freeze
# First-match win, so be sure to put more specific regex at the top...
CATEGORIES = {
%r{\Adoc/} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :none, # To reinstate roulette for documentation, set to `:docs`.
%r{\Adoc/} => :docs,
%r{\A(CONTRIBUTING|LICENSE|MAINTENANCE|PHILOSOPHY|PROCESS|README)(\.md)?\z} => :docs,
%r{\A(ee/)?app/(assets|views)/} => :frontend,
%r{\A(ee/)?public/} => :frontend,
......
......@@ -9,7 +9,7 @@ module Gitlab
end
def labels
{ controller: @worker_class.name, action: 'perform' }
{ controller: @worker_class.name, action: 'perform', feature_category: @worker_class.try(:get_feature_category).to_s }
end
end
end
......
......@@ -7,7 +7,7 @@ module Gitlab
include Gitlab::Metrics::Methods
# base labels shared among all transactions
BASE_LABELS = { controller: nil, action: nil }.freeze
BASE_LABELS = { controller: nil, action: nil, feature_category: nil }.freeze
# labels that potentially contain sensitive information and will be filtered
FILTERED_LABELS = [:branch, :path].freeze
......
......@@ -32,6 +32,10 @@ module Gitlab
action = "#{controller.action_name}"
# Try to get the feature category, but don't fail when the controller is
# not an ApplicationController.
feature_category = controller.class.try(:feature_category_for_action, action).to_s
# Devise exposes a method called "request_format" that does the below.
# However, this method is not available to all controllers (e.g. certain
# Doorkeeper controllers). As such we use the underlying code directly.
......@@ -45,7 +49,7 @@ module Gitlab
action = "#{action}.#{suffix}"
end
{ controller: controller.class.name, action: action }
{ controller: controller.class.name, action: action, feature_category: feature_category }
end
def labels_from_endpoint
......@@ -61,7 +65,10 @@ module Gitlab
if route
path = endpoint_paths_cache[route.request_method][route.path]
{ controller: 'Grape', action: "#{route.request_method} #{path}" }
# Feature categories will be added for grape endpoints in
# https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/462
{ controller: 'Grape', action: "#{route.request_method} #{path}", feature_category: '' }
end
end
......
......@@ -490,9 +490,17 @@ module Gitlab
{}
end
# rubocop: disable CodeReuse/ActiveRecord
def usage_activity_by_stage_monitor(time_period)
{}
{
clusters: distinct_count(::Clusters::Cluster.where(time_period), :user_id),
clusters_applications_prometheus: cluster_applications_user_distinct_count(::Clusters::Applications::Prometheus, time_period),
operations_dashboard_default_dashboard: count(::User.active.with_dashboard('operations').where(time_period),
start: user_minimum_id,
finish: user_maximum_id)
}
end
# rubocop: enable CodeReuse/ActiveRecord
def usage_activity_by_stage_package(time_period)
{}
......
......@@ -1301,6 +1301,9 @@ msgstr ""
msgid "Add"
msgstr ""
msgid "Add \"%{value}\" to allowlist"
msgstr ""
msgid "Add %d issue"
msgid_plural "Add %d issues"
msgstr[0] ""
......@@ -9373,9 +9376,6 @@ msgstr ""
msgid "Exactly one of %{attributes} is required"
msgstr ""
msgid "Example: <code>acme.com,acme.co.in,acme.uk</code>."
msgstr ""
msgid "Example: @sub\\.company\\.com$"
msgstr ""
......@@ -14918,7 +14918,7 @@ msgstr ""
msgid "MrDeploymentActions|Stop environment"
msgstr ""
msgid "Multiple domains are supported with comma delimiters."
msgid "Multiple domains are supported."
msgstr ""
msgid "Multiple issue boards"
......@@ -19639,7 +19639,7 @@ msgstr ""
msgid "Restrict access by IP address"
msgstr ""
msgid "Restrict membership by email"
msgid "Restrict membership by email domain"
msgstr ""
msgid "Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information."
......
# frozen_string_literal: true
require "fast_spec_helper"
require "rspec-parameterized"
require_relative "../../../../app/controllers/concerns/controller_with_feature_category/config"
RSpec.describe ControllerWithFeatureCategory::Config do
describe "#matches?" do
using RSpec::Parameterized::TableSyntax
where(:only_actions, :except_actions, :if_proc, :unless_proc, :test_action, :expected) do
nil | nil | nil | nil | "action" | true
[:included] | nil | nil | nil | "action" | false
[:included] | nil | nil | nil | "included" | true
nil | [:excluded] | nil | nil | "excluded" | false
nil | nil | true | nil | "action" | true
[:included] | nil | true | nil | "action" | false
[:included] | nil | true | nil | "included" | true
nil | [:excluded] | true | nil | "excluded" | false
nil | nil | false | nil | "action" | false
[:included] | nil | false | nil | "action" | false
[:included] | nil | false | nil | "included" | false
nil | [:excluded] | false | nil | "excluded" | false
nil | nil | nil | true | "action" | false
[:included] | nil | nil | true | "action" | false
[:included] | nil | nil | true | "included" | false
nil | [:excluded] | nil | true | "excluded" | false
nil | nil | nil | false | "action" | true
[:included] | nil | nil | false | "action" | false
[:included] | nil | nil | false | "included" | true
nil | [:excluded] | nil | false | "excluded" | false
nil | nil | true | false | "action" | true
[:included] | nil | true | false | "action" | false
[:included] | nil | true | false | "included" | true
nil | [:excluded] | true | false | "excluded" | false
nil | nil | false | true | "action" | false
[:included] | nil | false | true | "action" | false
[:included] | nil | false | true | "included" | false
nil | [:excluded] | false | true | "excluded" | false
end
with_them do
let(:config) do
if_to_proc = if_proc.nil? ? nil : -> (_) { if_proc }
unless_to_proc = unless_proc.nil? ? nil : -> (_) { unless_proc }
described_class.new(:category, only_actions, except_actions, if_to_proc, unless_to_proc)
end
specify { expect(config.matches?(test_action)).to be(expected) }
end
end
end
# frozen_string_literal: true
require 'fast_spec_helper'
require_relative "../../../app/controllers/concerns/controller_with_feature_category"
require_relative "../../../app/controllers/concerns/controller_with_feature_category/config"
RSpec.describe ControllerWithFeatureCategory do
describe ".feature_category_for_action" do
let(:base_controller) do
Class.new do
include ControllerWithFeatureCategory
end
end
let(:controller) do
Class.new(base_controller) do
feature_category :baz
feature_category :foo, except: %w(update edit)
feature_category :bar, only: %w(index show)
feature_category :quux, only: %w(destroy)
feature_category :quuz, only: %w(destroy)
end
end
let(:subclass) do
Class.new(controller) do
feature_category :qux, only: %w(index)
end
end
it "is nil when nothing was defined" do
expect(base_controller.feature_category_for_action("hello")).to be_nil
end
it "returns the expected category", :aggregate_failures do
expect(controller.feature_category_for_action("update")).to eq(:baz)
expect(controller.feature_category_for_action("hello")).to eq(:foo)
expect(controller.feature_category_for_action("index")).to eq(:bar)
end
it "returns the closest match for categories defined in subclasses" do
expect(subclass.feature_category_for_action("index")).to eq(:qux)
expect(subclass.feature_category_for_action("show")).to eq(:bar)
end
it "returns the last defined feature category when multiple match" do
expect(controller.feature_category_for_action("destroy")).to eq(:quuz)
end
it "raises an error when using including and excluding the same action" do
expect do
Class.new(base_controller) do
feature_category :hello, only: [:world], except: [:world]
end
end.to raise_error(%r(cannot configure both `only` and `except`))
end
it "raises an error when using unknown arguments" do
expect do
Class.new(base_controller) do
feature_category :hello, hello: :world
end
end.to raise_error(%r(unknown arguments))
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe "Every controller" do
context "feature categories" do
let_it_be(:feature_categories) do
YAML.load_file(Rails.root.join('config', 'feature_categories.yml')).map(&:to_sym).to_set
end
let_it_be(:controller_actions) do
# This will return tuples of all controller actions defined in the routes
# Only for controllers inheriting ApplicationController
# Excluding controllers from gems (OAuth, Sidekiq)
Rails.application.routes.routes
.map { |route| route.required_defaults.presence }
.compact
.select { |route| route[:controller].present? && route[:action].present? }
.map { |route| [constantize_controller(route[:controller]), route[:action]] }
.reject { |route| route.first.nil? || !route.first.include?(ControllerWithFeatureCategory) }
end
let_it_be(:routes_without_category) do
controller_actions.map do |controller, action|
"#{controller}##{action}" unless controller.feature_category_for_action(action)
end.compact
end
it "has feature categories" do
pending("We'll work on defining categories for all controllers: "\
"https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/463")
expect(routes_without_category).to be_empty, "#{routes_without_category.first(10)} did not have a category"
end
it "completed controllers don't get new routes without categories" do
completed_controllers = [Projects::MergeRequestsController].map(&:to_s)
newly_introduced_missing_category = routes_without_category.select do |route|
completed_controllers.any? { |controller| route.start_with?(controller) }
end
expect(newly_introduced_missing_category).to be_empty
end
it "recognizes the feature categories" do
routes_unknown_category = controller_actions.map do |controller, action|
used_category = controller.feature_category_for_action(action)
next unless used_category
next if used_category == :not_owned
["#{controller}##{action}", used_category] unless feature_categories.include?(used_category)
end.compact
expect(routes_unknown_category).to be_empty, "#{routes_unknown_category.first(10)} had an unknown category"
end
it "doesn't define or exclude categories on removed actions", :aggregate_failures do
controller_actions.group_by(&:first).each do |controller, controller_action|
existing_actions = controller_action.map(&:last)
used_actions = actions_defined_in_feature_category_config(controller)
non_existing_used_actions = used_actions - existing_actions
expect(non_existing_used_actions).to be_empty,
"#{controller} used #{non_existing_used_actions} to define feature category, but the route does not exist"
end
end
end
def constantize_controller(name)
"#{name.camelize}Controller".constantize
rescue NameError
nil # some controllers, like the omniauth ones are dynamic
end
def actions_defined_in_feature_category_config(controller)
feature_category_configs = controller.send(:class_attributes)[:feature_category_config]
feature_category_configs.map do |config|
Array(config.send(:only)) + Array(config.send(:except))
end.flatten.uniq.map(&:to_s)
end
end
......@@ -207,6 +207,15 @@ describe('AlertManagementList', () => {
expect(findStatusDropdown().exists()).toBe(true);
});
it('does not display a dropdown status header', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
});
expect(findStatusDropdown().contains('.dropdown-title')).toBe(false);
});
it('shows correct severity icons', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
......
......@@ -5,7 +5,7 @@ import { GlDropdownItem } from '@gitlab/ui';
import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql';
import mockAlerts from '../mocks/alerts.json';
import mockAlerts from '../../mocks/alerts.json';
const mockAlert = mockAlerts[0];
......
......@@ -3,7 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import AlertSidebar from '~/alert_management/components/alert_sidebar.vue';
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
import mockAlerts from '../mocks/alerts.json';
import mockAlerts from '../../mocks/alerts.json';
const mockAlert = mockAlerts[0];
......
import { mount } from '@vue/test-utils';
import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql';
import Tracking from '~/tracking';
import mockAlerts from '../mocks/alerts.json';
import mockAlerts from '../../mocks/alerts.json';
const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Status', () => {
let wrapper;
const findStatusDropdown = () => wrapper.find(GlDropdown);
const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
......@@ -41,18 +42,7 @@ describe('Alert Details Sidebar Status', () => {
}
});
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
errors: [],
alert: {
status: 'acknowledged',
},
},
},
};
describe('Alert Sidebar Dropdown Status', () => {
beforeEach(() => {
mountComponent({
data: { alert: mockAlert },
......@@ -61,46 +51,78 @@ describe('Alert Details Sidebar Status', () => {
});
});
it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findStatusDropdownItem().vm.$emit('click');
it('displays status dropdown', () => {
expect(findStatusDropdown().exists()).toBe(true);
});
it('displays the dropdown status header', () => {
expect(findStatusDropdown().contains('.dropdown-title')).toBe(true);
});
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateAlertStatus,
variables: {
iid: '1527542',
status: 'TRIGGERED',
projectPath: 'projectPath',
describe('updating the alert status', () => {
const mockUpdatedMutationResult = {
data: {
updateAlertStatus: {
errors: [],
alert: {
status: 'acknowledged',
},
},
},
};
beforeEach(() => {
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
loading: false,
});
});
});
it('stops updating when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findStatusDropdownItem().vm.$emit('click');
expect(findStatusLoadingIcon().exists()).toBe(false);
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
});
});
it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
findStatusDropdownItem().vm.$emit('click');
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alert: mockAlert },
loading: false,
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: updateAlertStatus,
variables: {
iid: '1527542',
status: 'TRIGGERED',
projectPath: 'projectPath',
},
});
});
it('stops updating when the request fails', () => {
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
findStatusDropdownItem().vm.$emit('click');
expect(findStatusLoadingIcon().exists()).toBe(false);
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
});
});
it('should track alert status updates', () => {
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findStatusDropdownItem().vm.$emit('click');
const status = findStatusDropdownItem().text();
setImmediate(() => {
const { category, action, label } = trackAlertStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, { label, property: status });
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: { alert: mockAlert },
loading: false,
});
});
it('should track alert status updates', () => {
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findStatusDropdownItem().vm.$emit('click');
const status = findStatusDropdownItem().text();
setImmediate(() => {
const { category, action, label } = trackAlertStatusUpdateOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action, {
label,
property: status,
});
});
});
});
});
......
import { shallowMount } from '@vue/test-utils';
import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
import mockAlerts from '../mocks/alerts.json';
import mockAlerts from '../../mocks/alerts.json';
const mockAlert = mockAlerts[1];
......
......@@ -4,7 +4,6 @@ import UserPopover from '~/vue_shared/components/user_popover/user_popover.vue';
import Icon from '~/vue_shared/components/icon.vue';
const DEFAULT_PROPS = {
loaded: true,
user: {
username: 'root',
name: 'Administrator',
......@@ -12,6 +11,7 @@ const DEFAULT_PROPS = {
bio: null,
workInformation: null,
status: null,
loaded: true,
},
};
......@@ -46,28 +46,21 @@ describe('User Popover Component', () => {
});
};
describe('Empty', () => {
beforeEach(() => {
createWrapper(
{},
{
propsData: {
target: findTarget(),
user: {
name: null,
username: null,
location: null,
bio: null,
workInformation: null,
status: null,
},
},
describe('when user is loading', () => {
it('displays skeleton loaders', () => {
createWrapper({
user: {
name: null,
username: null,
location: null,
bio: null,
workInformation: null,
status: null,
loaded: false,
},
);
});
});
it('should return skeleton loaders', () => {
expect(wrapper.find(GlSkeletonLoading).exists()).toBe(true);
expect(wrapper.findAll(GlSkeletonLoading)).toHaveLength(4);
});
});
......
# frozen_string_literal: true
require 'fast_spec_helper'
RSpec.describe Gitlab::ClassAttributes do
let(:klass) do
Class.new do
include Gitlab::ClassAttributes
def self.get_attribute(name)
get_class_attribute(name)
end
def self.set_attribute(name, value)
class_attributes[name] = value
end
end
end
let(:subclass) { Class.new(klass) }
describe ".get_class_attribute" do
it "returns values set on the class" do
klass.set_attribute(:foo, :bar)
expect(klass.get_attribute(:foo)).to eq(:bar)
end
it "returns values set on a superclass" do
klass.set_attribute(:foo, :bar)
expect(subclass.get_attribute(:foo)).to eq(:bar)
end
it "returns values from the subclass over attributes from a superclass" do
klass.set_attribute(:foo, :baz)
subclass.set_attribute(:foo, :bar)
expect(subclass.get_attribute(:foo)).to eq(:bar)
end
end
end
......@@ -167,13 +167,13 @@ RSpec.describe Gitlab::Danger::Helper do
describe '#categories_for_file' do
where(:path, :expected_categories) do
'doc/foo' | [:none]
'CONTRIBUTING.md' | [:none]
'LICENSE' | [:none]
'MAINTENANCE.md' | [:none]
'PHILOSOPHY.md' | [:none]
'PROCESS.md' | [:none]
'README.md' | [:none]
'doc/foo' | [:docs]
'CONTRIBUTING.md' | [:docs]
'LICENSE' | [:docs]
'MAINTENANCE.md' | [:docs]
'PHILOSOPHY.md' | [:docs]
'PROCESS.md' | [:docs]
'README.md' | [:docs]
'ee/doc/foo' | [:unknown]
'ee/README' | [:unknown]
......
......@@ -9,7 +9,16 @@ RSpec.describe Gitlab::Metrics::BackgroundTransaction do
describe '#label' do
it 'returns labels based on class name' do
expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform')
expect(subject.labels).to eq(controller: 'TestWorker', action: 'perform', feature_category: '')
end
it 'contains only the labels defined for metrics' do
expect(subject.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys)
end
it 'includes the feature category if there is one' do
expect(test_worker_class).to receive(:get_feature_category).and_return('source_code_management')
expect(subject.labels).to include(feature_category: 'source_code_management')
end
end
end
......@@ -70,6 +70,9 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
describe '#labels' do
let(:request) { double(:request, format: double(:format, ref: :html)) }
let(:controller_class) { double(:controller_class, name: 'TestController') }
context 'when request goes to Grape endpoint' do
before do
route = double(:route, request_method: 'GET', path: '/:version/projects/:id/archive(.:format)')
......@@ -77,8 +80,13 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
env['api.endpoint'] = endpoint
end
it 'provides labels with the method and path of the route in the grape endpoint' do
expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive' })
expect(transaction.labels).to eq({ controller: 'Grape', action: 'GET /projects/:id/archive', feature_category: '' })
end
it 'contains only the labels defined for transactions' do
expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys)
end
it 'does not provide labels if route infos are missing' do
......@@ -92,24 +100,25 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
end
context 'when request goes to ActionController' do
let(:request) { double(:request, format: double(:format, ref: :html)) }
before do
klass = double(:klass, name: 'TestController')
controller = double(:controller, class: klass, action_name: 'show', request: request)
controller = double(:controller, class: controller_class, action_name: 'show', request: request)
env['action_controller.instance'] = controller
end
it 'tags a transaction with the name and action of a controller' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
end
it 'contains only the labels defined for transactions' do
expect(transaction.labels.keys).to contain_exactly(*described_class.superclass::BASE_LABELS.keys)
end
context 'when the request content type is not :html' do
let(:request) { double(:request, format: double(:format, ref: :json)) }
it 'appends the mime type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show.json', feature_category: '' })
end
end
......@@ -117,7 +126,14 @@ RSpec.describe Gitlab::Metrics::WebTransaction do
let(:request) { double(:request, format: double(:format, ref: 'http://example.com')) }
it 'does not append the MIME type to the transaction action' do
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show' })
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: '' })
end
end
context 'when the feature category is known' do
it 'includes it in the feature category label' do
expect(controller_class).to receive(:feature_category_for_action).with('show').and_return(:source_code_management)
expect(transaction.labels).to eq({ controller: 'TestController', action: 'show', feature_category: "source_code_management" })
end
end
end
......
......@@ -93,6 +93,28 @@ RSpec.describe Gitlab::UsageData, :aggregate_failures do
end
end
context 'for monitor' do
it 'includes accurate usage_activity_by_stage data' do
for_defined_days_back do
user = create(:user, dashboard: 'operations')
cluster = create(:cluster, user: user)
create(:project, creator: user)
create(:clusters_applications_prometheus, :installed, cluster: cluster)
end
expect(described_class.uncached_data[:usage_activity_by_stage][:monitor]).to include(
clusters: 2,
clusters_applications_prometheus: 2,
operations_dashboard_default_dashboard: 2
)
expect(described_class.uncached_data[:usage_activity_by_stage_monthly][:monitor]).to include(
clusters: 1,
clusters_applications_prometheus: 1,
operations_dashboard_default_dashboard: 1
)
end
end
it 'ensures recorded_at is set before any other usage data calculation' do
%i(alt_usage_data redis_usage_data distinct_count count).each do |method|
expect(described_class).not_to receive(method)
......
......@@ -12,6 +12,8 @@ RSpec.describe MergeRequest do
subject { create(:merge_request) }
describe 'associations' do
subject { build_stubbed(:merge_request) }
it { is_expected.to belong_to(:target_project).class_name('Project') }
it { is_expected.to belong_to(:source_project).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
......
......@@ -12,7 +12,7 @@ RSpec.describe AlertManagement::AlertPresenter do
}
end
let_it_be(:alert) do
create(:alert_management_alert, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload)
create(:alert_management_alert, :with_description, :with_host, :with_service, :with_monitoring_tool, project: project, payload: generic_payload)
end
subject(:presenter) { described_class.new(alert) }
......@@ -29,7 +29,8 @@ RSpec.describe AlertManagement::AlertPresenter do
**Severity:** #{presenter.severity}#{markdown_line_break}
**Service:** #{alert.service}#{markdown_line_break}
**Monitoring tool:** #{alert.monitoring_tool}#{markdown_line_break}
**Hosts:** #{alert.hosts.join(' ')}
**Hosts:** #{alert.hosts.join(' ')}#{markdown_line_break}
**Description:** #{alert.description}
#### Alert Details
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册