Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-08-18 06:11:01 +00:00
parent 25d4a24f83
commit 514ace3632
40 changed files with 673 additions and 661 deletions

View File

@ -3,7 +3,7 @@ extends:
- plugin:@gitlab/i18n
- plugin:no-jquery/slim
- plugin:no-jquery/deprecated-3.4
- ./tooling/eslint-config/conditionally_ignore_ee.js
- ./tooling/eslint-config/conditionally_ignore.js
globals:
__webpack_public_path__: true
gl: false

View File

@ -17,8 +17,6 @@ import {
PAGINATION_SORT_FIELD_DURATION,
PAGINATION_SORT_DIRECTION_ASC,
PAGINATION_SORT_DIRECTION_DESC,
STAGE_TITLE_STAGING,
STAGE_TITLE_TEST,
} from '../constants';
import TotalTime from './total_time_component.vue';
@ -107,28 +105,12 @@ export default {
emptyStateTitleText() {
return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
},
isDefaultTestStage() {
const { selectedStage } = this;
return (
!selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_TEST
);
},
isDefaultStagingStage() {
const { selectedStage } = this;
return (
!selectedStage.custom && selectedStage.title?.toLowerCase().trim() === STAGE_TITLE_STAGING
);
},
isMergeRequestStage() {
const [firstEvent] = this.stageEvents;
return this.isMrLink(firstEvent.url);
},
workflowTitle() {
if (this.isDefaultTestStage) {
return WORKFLOW_COLUMN_TITLES.jobs;
} else if (this.isDefaultStagingStage) {
return WORKFLOW_COLUMN_TITLES.deployments;
} else if (this.isMergeRequestStage) {
if (this.isMergeRequestStage) {
return WORKFLOW_COLUMN_TITLES.mergeRequests;
}
return WORKFLOW_COLUMN_TITLES.issues;
@ -209,22 +191,6 @@ export default {
<div data-testid="vsa-stage-event">
<div v-if="item.id" data-testid="vsa-stage-content">
<p class="gl-m-0">
<template v-if="isDefaultTestStage">
<span
class="icon-build-status gl-vertical-align-middle gl-text-green-500"
data-testid="vsa-stage-event-build-status"
>
<gl-icon name="status_success" :size="14" />
</span>
<gl-link
class="gl-text-black-normal item-build-name"
data-testid="vsa-stage-event-build-name"
:href="item.url"
>
{{ item.name }}
</gl-link>
&middot;
</template>
<gl-link class="gl-text-black-normal pipeline-id" :href="item.url"
>#{{ item.id }}</gl-link
>
@ -246,12 +212,7 @@ export default {
>
</p>
<p class="gl-m-0">
<span v-if="isDefaultTestStage" data-testid="vsa-stage-event-build-status-date">
<gl-link class="gl-text-black-normal issue-date" :href="item.url">{{
item.date
}}</gl-link>
</span>
<span v-else data-testid="vsa-stage-event-build-author-and-date">
<span data-testid="vsa-stage-event-build-author-and-date">
<gl-link class="gl-text-black-normal build-date" :href="item.url">{{
item.date
}}</gl-link>

View File

@ -25,9 +25,6 @@ export const PAGINATION_SORT_FIELD_DURATION = 'duration';
export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
export const STAGE_TITLE_STAGING = 'staging';
export const STAGE_TITLE_TEST = 'test';
export const I18N_VSA_ERROR_STAGES = __(
'There was an error fetching value stream analytics stages.',
);

View File

@ -9,6 +9,7 @@ export const FILTER_ANY = 'Any';
export const FILTER_CURRENT = 'Current';
export const FILTER_UPCOMING = 'Upcoming';
export const FILTER_STARTED = 'Started';
export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
@ -27,8 +28,6 @@ export const DEFAULT_ITERATIONS = DEFAULT_NONE_ANY.concat([
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
]);
export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
{ value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
{ value: FILTER_STARTED, text: __(FILTER_STARTED) },

View File

@ -8,7 +8,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { DEBOUNCE_DELAY } from '../constants';
import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@ -89,6 +89,14 @@ export default {
activeTokenValue() {
return this.getActiveTokenValue(this.suggestions, this.value.data);
},
availableDefaultSuggestions() {
if (this.value.operator === OPERATOR_IS_NOT) {
return this.defaultSuggestions.filter(
(suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
);
}
return this.defaultSuggestions;
},
/**
* Return all the suggestions when searchKey is present
* otherwise return only the suggestions which aren't
@ -104,7 +112,7 @@ export default {
);
},
showDefaultSuggestions() {
return this.defaultSuggestions.length;
return this.availableDefaultSuggestions.length;
},
showRecentSuggestions() {
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
@ -180,7 +188,7 @@ export default {
<template v-if="showSuggestions" #suggestions>
<template v-if="showDefaultSuggestions">
<gl-filtered-search-suggestion
v-for="token in defaultSuggestions"
v-for="token in availableDefaultSuggestions"
:key="token.value"
:value="token.value"
>

View File

@ -8,7 +8,7 @@ import {
import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY } from '../constants';
import { DEBOUNCE_DELAY, DEFAULT_NONE_ANY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
export default {
separator: '::&',
@ -48,6 +48,14 @@ export default {
defaultEpics() {
return this.config.defaultEpics || DEFAULT_NONE_ANY;
},
availableDefaultEpics() {
if (this.value.operator === OPERATOR_IS_NOT) {
return this.defaultEpics.filter(
(suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
);
}
return this.defaultEpics;
},
activeEpic() {
if (this.currentValue && this.epics.length) {
// Check if current value is an epic ID.
@ -127,13 +135,13 @@ export default {
</template>
<template #suggestions>
<gl-filtered-search-suggestion
v-for="epic in defaultEpics"
v-for="epic in availableDefaultEpics"
:key="epic.value"
:value="epic.value"
>
{{ epic.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultEpics.length" />
<gl-dropdown-divider v-if="availableDefaultEpics.length" />
<gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">

View File

@ -5,7 +5,7 @@ import createFlash from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { DEFAULT_LABELS } from '../constants';
import { DEFAULT_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
@ -38,7 +38,7 @@ export default {
},
computed: {
defaultLabels() {
return this.config.defaultLabels || DEFAULT_LABELS;
return this.config.defaultLabels || DEFAULT_NONE_ANY;
},
},
methods: {

View File

@ -46,7 +46,7 @@ module AlertManagement
def by_status(collection)
values = AlertManagement::Alert.status_names & Array(params[:status])
values.present? ? collection.for_status(values) : collection
values.present? ? collection.with_status(values) : collection
end
def by_search(collection)

View File

@ -13,20 +13,7 @@ module AlertManagement
include Presentable
include Gitlab::Utils::StrongMemoize
include Referable
STATUSES = {
triggered: 0,
acknowledged: 1,
resolved: 2,
ignored: 3
}.freeze
STATUS_DESCRIPTIONS = {
triggered: 'Investigation has not started',
acknowledged: 'Someone is actively investigating the problem',
resolved: 'No further work is required',
ignored: 'No action will be taken on the alert'
}.freeze
include ::IncidentManagement::Escalatable
belongs_to :project
belongs_to :issue, optional: true
@ -44,6 +31,9 @@ module AlertManagement
sha_attribute :fingerprint
# Allow :ended_at to be managed by Escalatable
alias_attribute :resolved_at, :ended_at
TITLE_MAX_LENGTH = 200
DESCRIPTION_MAX_LENGTH = 1_000
SERVICE_MAX_LENGTH = 100
@ -57,7 +47,6 @@ module AlertManagement
validates :project, presence: true
validates :events, presence: true
validates :severity, presence: true
validates :status, presence: true
validates :started_at, presence: true
validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project,
@ -80,52 +69,10 @@ module AlertManagement
threat_monitoring: 1
}
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
state :acknowledged, value: STATUSES[:acknowledged]
state :resolved, value: STATUSES[:resolved] do
validates :ended_at, presence: true
end
state :ignored, value: STATUSES[:ignored]
state :triggered, :acknowledged, :ignored do
validates :ended_at, absence: true
end
event :trigger do
transition any => :triggered
end
event :acknowledge do
transition any => :acknowledged
end
event :resolve do
transition any => :resolved
end
event :ignore do
transition any => :ignored
end
before_transition to: [:triggered, :acknowledged, :ignored] do |alert, _transition|
alert.ended_at = nil
end
before_transition to: :resolved do |alert, transition|
ended_at = transition.args.first
alert.ended_at = ended_at || Time.current
end
end
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_status, -> (status) { with_status(status) }
scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
scope :for_environment, -> (environment) { where(environment: environment) }
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
@ -146,36 +93,14 @@ module AlertManagement
scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
scope :counts_by_project_id, -> { group(:project_id).count }
alias_method :state, :status_name
def self.state_machine_statuses
@state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
end
private_class_method :state_machine_statuses
def self.status_value(name)
state_machine_statuses[name]
end
def self.status_name(raw_status)
state_machine_statuses.key(raw_status)
end
def self.counts_by_status
group(:status).count.transform_keys { |k| status_name(k) }
end
def self.status_names
@status_names ||= state_machine_statuses.keys
end
def self.sort_by_attribute(method)
case method.to_s
when 'started_at_asc' then order_start_time(:asc)
@ -229,15 +154,6 @@ module AlertManagement
self.class.open_status?(status_name)
end
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
def change_status_to(new_status)
event = status_event_for(new_status)
event && fire_status_event(event)
end
def prometheus?
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
end

View File

@ -0,0 +1,104 @@
# frozen_string_literal: true
module IncidentManagement
# Shared functionality for a `#status` field, representing
# whether action is required. In EE, this corresponds
# to paging functionality with EscalationPolicies.
#
# This module is only responsible for setting the status and
# possible status-related timestamps (EX triggered_at/resolved_at)
# for the implementing class. The relationships between these
# values and other related timestamps/logic should be managed from
# the object class itself. (EX Alert#ended_at = Alert#resolved_at)
module Escalatable
extend ActiveSupport::Concern
STATUSES = {
triggered: 0,
acknowledged: 1,
resolved: 2,
ignored: 3
}.freeze
STATUS_DESCRIPTIONS = {
triggered: 'Investigation has not started',
acknowledged: 'Someone is actively investigating the problem',
resolved: 'The problem has been addressed',
ignored: 'No action will be taken'
}.freeze
included do
validates :status, presence: true
# Ascending sort order sorts statuses: Ignored > Resolved > Acknowledged > Triggered
# Descending sort order sorts statuses: Triggered > Acknowledged > Resolved > Ignored
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
scope :order_status, -> (sort_order) { order(status: sort_order == :asc ? :desc : :asc) }
state_machine :status, initial: :triggered do
state :triggered, value: STATUSES[:triggered]
state :acknowledged, value: STATUSES[:acknowledged]
state :resolved, value: STATUSES[:resolved] do
validates :resolved_at, presence: true
end
state :ignored, value: STATUSES[:ignored]
state :triggered, :acknowledged, :ignored do
validates :resolved_at, absence: true
end
event :trigger do
transition any => :triggered
end
event :acknowledge do
transition any => :acknowledged
end
event :resolve do
transition any => :resolved
end
event :ignore do
transition any => :ignored
end
before_transition to: [:triggered, :acknowledged, :ignored] do |escalatable, _transition|
escalatable.resolved_at = nil
end
before_transition to: :resolved do |escalatable, transition|
resolved_at = transition.args.first
escalatable.resolved_at = resolved_at || Time.current
end
end
class << self
def status_value(name)
state_machine_statuses[name]
end
def status_name(raw_status)
state_machine_statuses.key(raw_status)
end
def status_names
@status_names ||= state_machine_statuses.keys
end
private
def state_machine_statuses
@state_machine_statuses ||= state_machines[:status].states.to_h { |s| [s.name, s.value] }
end
end
def status_event_for(status)
self.class.state_machines[:status].events.transitions_for(self, to: status.to_s.to_sym).first&.event
end
end
end
end

View File

@ -0,0 +1,15 @@
# frozen_string_literal: true
module IncidentManagement
class IssuableEscalationStatus < ApplicationRecord
include ::IncidentManagement::Escalatable
self.table_name = 'incident_management_issuable_escalation_statuses'
belongs_to :issue
validates :issue, presence: true, uniqueness: true
end
end
IncidentManagement::IssuableEscalationStatus.prepend_mod_with('IncidentManagement::IssuableEscalationStatus')

View File

@ -77,6 +77,7 @@ class Issue < ApplicationRecord
has_one :issuable_severity
has_one :sentry_issue
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
has_one :incident_management_issuable_escalation_status, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events

View File

@ -32,26 +32,28 @@ module DraftNotes
review = Review.create!(author: current_user, merge_request: merge_request, project: project)
draft_notes.map do |draft_note|
created_notes = draft_notes.map do |draft_note|
draft_note.review = review
create_note_from_draft(draft_note)
create_note_from_draft(draft_note, skip_capture_diff_note_position: true)
end
capture_diff_note_positions(created_notes)
draft_notes.delete_all
set_reviewed
notification_service.async.new_review(review)
MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
end
def create_note_from_draft(draft)
def create_note_from_draft(draft, skip_capture_diff_note_position: false)
# Make sure the diff file is unfolded in order to find the correct line
# codes.
draft.diff_file&.unfold_diff_lines(draft.original_position)
note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute
set_discussion_resolve_status(note, draft)
note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute(
skip_capture_diff_note_position: skip_capture_diff_note_position
)
set_discussion_resolve_status(note, draft)
note
end
@ -70,5 +72,19 @@ module DraftNotes
def set_reviewed
::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
end
def capture_diff_note_positions(notes)
paths = notes.flat_map do |note|
note.diff_file&.paths if note.diff_note?
end
return if paths.empty?
capture_service = Discussions::CaptureDiffNotePositionService.new(merge_request, paths.compact)
notes.each do |note|
capture_service.execute(note.discussion) if note.diff_note? && note.start_of_discussion?
end
end
end
end

View File

@ -4,7 +4,7 @@ module Notes
class CreateService < ::Notes::BaseService
include IncidentManagement::UsageData
def execute
def execute(skip_capture_diff_note_position: false)
note = Notes::BuildService.new(project, current_user, params.except(:merge_request_diff_head_sha)).execute
# n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440
@ -34,7 +34,7 @@ module Notes
end
end
when_saved(note) if note_saved
when_saved(note, skip_capture_diff_note_position: skip_capture_diff_note_position) if note_saved
end
note
@ -68,14 +68,14 @@ module Notes
end
end
def when_saved(note)
def when_saved(note, skip_capture_diff_note_position: false)
todo_service.new_note(note, current_user)
clear_noteable_diffs_cache(note)
Suggestions::CreateService.new(note).execute
increment_usage_counter(note)
track_event(note, current_user)
if note.for_merge_request? && note.diff_note? && note.start_of_discussion?
if !skip_capture_diff_note_position && note.for_merge_request? && note.diff_note? && note.start_of_discussion?
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
end
end

View File

@ -4,7 +4,8 @@
- @no_container = true
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
- @content_class = "issue-boards-content js-focus-mode-board"
- if board.to_type == "EpicBoard"
- is_epic_board = board.to_type == "EpicBoard"
- if is_epic_board
- breadcrumb_title _("Epic Boards")
- else
- breadcrumb_title _("Issue Boards")
@ -19,5 +20,6 @@
= render 'shared/issuable/search_bar', type: :boards, board: board
#board-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
= render "shared/boards/components/sidebar", group: group
- if !is_epic_board && !Feature.enabled?(:graphql_board_lists, default_enabled: :yaml)
= render "shared/boards/components/sidebar", group: group
%board-settings-sidebar

View File

@ -0,0 +1,20 @@
# frozen_string_literal: true
class CreateIncidentManagementIssuableEscalationStatuses < ActiveRecord::Migration[6.1]
ISSUE_IDX = 'index_uniq_im_issuable_escalation_statuses_on_issue_id'
POLICY_IDX = 'index_im_issuable_escalation_statuses_on_policy_id'
def change
create_table :incident_management_issuable_escalation_statuses do |t|
t.timestamps_with_timezone
t.references :issue, foreign_key: { on_delete: :cascade }, index: { unique: true, name: ISSUE_IDX }, null: false
t.references :policy, foreign_key: { to_table: :incident_management_escalation_policies, on_delete: :nullify }, index: { name: POLICY_IDX }
t.datetime_with_timezone :escalations_started_at
t.datetime_with_timezone :resolved_at
t.integer :status, default: 0, null: false, limit: 2
end
end
end

View File

@ -0,0 +1 @@
ce20c699d6e6d6baf812c926dde08485764faa2fdeb8af14808670bf692aab00

View File

@ -14020,6 +14020,26 @@ CREATE SEQUENCE incident_management_escalation_rules_id_seq
ALTER SEQUENCE incident_management_escalation_rules_id_seq OWNED BY incident_management_escalation_rules.id;
CREATE TABLE incident_management_issuable_escalation_statuses (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
issue_id bigint NOT NULL,
policy_id bigint,
escalations_started_at timestamp with time zone,
resolved_at timestamp with time zone,
status smallint DEFAULT 0 NOT NULL
);
CREATE SEQUENCE incident_management_issuable_escalation_statuses_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE incident_management_issuable_escalation_statuses_id_seq OWNED BY incident_management_issuable_escalation_statuses.id;
CREATE TABLE incident_management_oncall_participants (
id bigint NOT NULL,
oncall_rotation_id bigint NOT NULL,
@ -20443,6 +20463,8 @@ ALTER TABLE ONLY incident_management_escalation_policies ALTER COLUMN id SET DEF
ALTER TABLE ONLY incident_management_escalation_rules ALTER COLUMN id SET DEFAULT nextval('incident_management_escalation_rules_id_seq'::regclass);
ALTER TABLE ONLY incident_management_issuable_escalation_statuses ALTER COLUMN id SET DEFAULT nextval('incident_management_issuable_escalation_statuses_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_participants ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_participants_id_seq'::regclass);
ALTER TABLE ONLY incident_management_oncall_rotations ALTER COLUMN id SET DEFAULT nextval('incident_management_oncall_rotations_id_seq'::regclass);
@ -21855,6 +21877,9 @@ ALTER TABLE ONLY incident_management_escalation_policies
ALTER TABLE ONLY incident_management_escalation_rules
ADD CONSTRAINT incident_management_escalation_rules_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT incident_management_issuable_escalation_statuses_pkey PRIMARY KEY (id);
ALTER TABLE ONLY incident_management_oncall_participants
ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id);
@ -24103,6 +24128,8 @@ CREATE INDEX index_identities_on_saml_provider_id ON identities USING btree (sam
CREATE INDEX index_identities_on_user_id ON identities USING btree (user_id);
CREATE INDEX index_im_issuable_escalation_statuses_on_policy_id ON incident_management_issuable_escalation_statuses USING btree (policy_id);
CREATE UNIQUE INDEX index_im_oncall_schedules_on_project_id_and_iid ON incident_management_oncall_schedules USING btree (project_id, iid);
CREATE UNIQUE INDEX index_import_export_uploads_on_group_id ON import_export_uploads USING btree (group_id) WHERE (group_id IS NOT NULL);
@ -25423,6 +25450,8 @@ CREATE INDEX index_u2f_registrations_on_key_handle ON u2f_registrations USING bt
CREATE INDEX index_u2f_registrations_on_user_id ON u2f_registrations USING btree (user_id);
CREATE UNIQUE INDEX index_uniq_im_issuable_escalation_statuses_on_issue_id ON incident_management_issuable_escalation_statuses USING btree (issue_id);
CREATE UNIQUE INDEX index_unique_issue_metrics_issue_id ON issue_metrics USING btree (issue_id);
CREATE INDEX index_unit_test_failures_failed_at ON ci_unit_test_failures USING btree (failed_at DESC);
@ -27164,6 +27193,9 @@ ALTER TABLE ONLY dast_site_validations
ALTER TABLE ONLY vulnerability_findings_remediations
ADD CONSTRAINT fk_rails_28a8d0cf93 FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT fk_rails_29abffe3b9 FOREIGN KEY (policy_id) REFERENCES incident_management_escalation_policies(id) ON DELETE SET NULL;
ALTER TABLE ONLY resource_state_events
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
@ -28418,6 +28450,9 @@ ALTER TABLE incident_management_pending_alert_escalations
ALTER TABLE ONLY board_group_recent_visits
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
ALTER TABLE ONLY incident_management_issuable_escalation_statuses
ADD CONSTRAINT fk_rails_f4c811fd28 FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
ALTER TABLE ONLY resource_state_events
ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;

View File

@ -14601,8 +14601,8 @@ Alert status values.
| Value | Description |
| ----- | ----------- |
| <a id="alertmanagementstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. |
| <a id="alertmanagementstatusignored"></a>`IGNORED` | No action will be taken on the alert. |
| <a id="alertmanagementstatusresolved"></a>`RESOLVED` | No further work is required. |
| <a id="alertmanagementstatusignored"></a>`IGNORED` | No action will be taken. |
| <a id="alertmanagementstatusresolved"></a>`RESOLVED` | The problem has been addressed. |
| <a id="alertmanagementstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
### `ApiFuzzingScanMode`

View File

@ -1,9 +0,0 @@
---
redirect_to: 'execution_context_selection.md'
remove_date: '2021-08-14'
---
This file was moved to [another location](execution_context_selection.md).
<!-- This redirect file can be deleted after <2021-08-14>. -->
<!-- Before deletion, see: https://docs.gitlab.com/ee/development/documentation/#move-or-rename-a-page -->

View File

@ -51,10 +51,13 @@ To add a new application for your user:
1. In the left sidebar, select **Applications**.
1. Enter a **Name**, **Redirect URI** and OAuth 2 scopes as defined in [Authorized Applications](#authorized-applications).
The **Redirect URI** is the URL where users are sent after they authorize with GitLab.
1. Select **Save application**. GitLab displays:
1. Select **Save application**. GitLab provides:
- Application ID: OAuth 2 Client ID.
- Secret: OAuth 2 Client Secret.
- The OAuth 2 Client ID in the **Application ID** field.
- The OAuth 2 Client Secret, accessible:
- In the **Secret** field in GitLab 14.1 and earlier.
- Using the **Copy** button on the **Secret** field
[in GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/332844).
## Group owned applications
@ -66,10 +69,13 @@ To add a new application for a group:
1. On the left sidebar, select **Settings > Applications**.
1. Enter a **Name**, **Redirect URI** and OAuth 2 scopes as defined in [Authorized Applications](#authorized-applications).
The **Redirect URI** is the URL where users are sent after they authorize with GitLab.
1. Select **Save application**. GitLab displays:
1. Select **Save application**. GitLab provides:
- Application ID: OAuth 2 Client ID.
- Secret: OAuth 2 Client Secret.
- The OAuth 2 Client ID in the **Application ID** field.
- The OAuth 2 Client Secret, accessible:
- In the **Secret** field in GitLab 14.1 and earlier.
- Using the **Copy** button on the **Secret** field
[in GitLab 14.2 and later](https://gitlab.com/gitlab-org/gitlab/-/issues/332844).
## Instance-wide applications

View File

@ -38,36 +38,19 @@ module Gitlab
# rubocop: disable CodeReuse/ActiveRecord
def serialized_records
strong_memoize(:serialized_records) do
# special case (legacy): 'Test' and 'Staging' stages should show Ci::Build records
if default_test_stage? || default_staging_stage?
ci_build_join = mr_metrics_table
.join(build_table)
.on(mr_metrics_table[:pipeline_id].eq(build_table[:commit_id]))
.join_sources
records = ordered_and_limited_query.select(*columns, *time_columns)
records = ordered_and_limited_query
.joins(ci_build_join)
.select(build_table[:id], *time_columns)
yield records if block_given?
records = preload_associations(records)
yield records if block_given?
ci_build_records = preload_ci_build_associations(records)
AnalyticsBuildSerializer.new.represent(ci_build_records.map { |e| e['build'] })
else
records = ordered_and_limited_query.select(*columns, *time_columns)
yield records if block_given?
records = preload_associations(records)
records.map do |record|
project = record.project
attributes = record.attributes.merge({
project_path: project.path,
namespace_path: project.namespace.route.path,
author: record.author
})
serializer.represent(attributes)
end
records.map do |record|
project = record.project
attributes = record.attributes.merge({
project_path: project.path,
namespace_path: project.namespace.route.path,
author: record.author
})
serializer.represent(attributes)
end
end
end
@ -83,26 +66,10 @@ module Gitlab
end
end
def default_test_stage?
stage.matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_test_stage)
end
def default_staging_stage?
stage.matches_with_stage_params?(Gitlab::Analytics::CycleAnalytics::DefaultStages.params_for_staging_stage)
end
def serializer
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
end
# rubocop: disable CodeReuse/ActiveRecord
def preload_ci_build_associations(records)
results = records.map(&:attributes)
Gitlab::CycleAnalytics::Updater.update!(results, from: 'id', to: 'build', klass: ::Ci::Build.includes({ project: [:namespace], user: [], pipeline: [] }))
end
# rubocop: enable CodeReuse/ActiveRecord
def ordered_and_limited_query
strong_memoize(:ordered_and_limited_query) do
order_by(query, sort, direction, columns).page(page).per(per_page).without_count

View File

@ -35,6 +35,12 @@ module Gitlab
def hosts=(hosts)
@mutex.synchronize do
::Gitlab::Database::LoadBalancing::Logger.info(
event: :host_list_update,
message: "Updating the host list for service discovery",
host_list_length: hosts.length,
old_host_list_length: @hosts.length
)
@hosts = hosts
unsafe_shuffle
end

View File

@ -0,0 +1,25 @@
# frozen_string_literal: true
FactoryBot.define do
factory :incident_management_issuable_escalation_status, class: 'IncidentManagement::IssuableEscalationStatus' do
issue
triggered
trait :triggered do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:triggered) }
end
trait :acknowledged do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:acknowledged) }
end
trait :resolved do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:resolved) }
resolved_at { Time.current }
end
trait :ignored do
status { ::IncidentManagement::IssuableEscalationStatus.status_value(:ignored) }
end
end
end

View File

@ -84,13 +84,13 @@ RSpec.describe 'Value Stream Analytics', :js do
expect_merge_request_to_be_present
click_stage('Test')
expect_build_to_be_present
expect_merge_request_to_be_present
click_stage('Review')
expect_merge_request_to_be_present
click_stage('Staging')
expect_build_to_be_present
expect_merge_request_to_be_present
end
context "when I change the time period observed" do
@ -168,12 +168,6 @@ RSpec.describe 'Value Stream Analytics', :js do
expect(find(stage_table_selector)).to have_content("##{issue.iid}")
end
def expect_build_to_be_present
expect(find(stage_table_selector)).to have_content(@build.ref)
expect(find(stage_table_selector)).to have_content(@build.short_sha)
expect(find(stage_table_selector)).to have_content("##{@build.id}")
end
def expect_merge_request_to_be_present
expect(find(stage_table_selector)).to have_content(mr.title)
expect(find(stage_table_selector)).to have_content(mr.author.name)

View File

@ -135,8 +135,6 @@ export const convertedData = {
export const rawIssueEvents = stageFixtures.issue;
export const issueEvents = deepCamelCase(rawIssueEvents);
export const reviewEvents = deepCamelCase(stageFixtures.review);
export const testEvents = deepCamelCase(stageFixtures.test);
export const stagingEvents = deepCamelCase(stageFixtures.staging);
export const pathNavIssueMetric = 172800;

View File

@ -4,16 +4,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import StageTable from '~/cycle_analytics/components/stage_table.vue';
import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
import {
stagingEvents,
stagingStage,
issueEvents,
issueStage,
testEvents,
testStage,
reviewStage,
reviewEvents,
} from './mock_data';
import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data';
let wrapper = null;
let trackingSpy = null;
@ -22,12 +13,8 @@ const noDataSvgPath = 'path/to/no/data';
const emptyStateTitle = 'Too much data';
const notEnoughDataError = "We don't have enough data to show this stage.";
const issueEventItems = issueEvents.events;
const stagingEventItems = stagingEvents.events;
const testEventItems = testEvents.events;
const reviewEventItems = reviewEvents.events;
const [firstIssueEvent] = issueEventItems;
const [firstStagingEvent] = stagingEventItems;
const [firstTestEvent] = testEventItems;
const [firstReviewEvent] = reviewEventItems;
const pagination = { page: 1, hasNextPage: true };
@ -156,99 +143,6 @@ describe('StageTable', () => {
});
});
describe('staging event', () => {
beforeEach(() => {
wrapper = createComponent({
stageEvents: [{ ...firstStagingEvent }],
selectedStage: { ...stagingStage, custom: false },
});
});
it('will set the workflow title to "Deployments"', () => {
expect(findTableHead().text()).toContain('Deployments');
expect(findTableHead().text()).not.toContain('Issues');
});
it('will not render the event title', () => {
expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false);
});
it('will render the fork icon', () => {
expect(findIcon('fork').exists()).toBe(true);
});
it('will render the branch icon', () => {
expect(findIcon('commit').exists()).toBe(true);
});
it('will render the total time', () => {
expect(findStageTime().text()).toBe('2 mins');
});
it('will render the build shortSha', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe(
firstStagingEvent.shortSha,
);
});
it('will render the author and date', () => {
const content = wrapper.findByTestId('vsa-stage-event-build-author-and-date').text();
expect(content).toContain(firstStagingEvent.author.name);
expect(content).toContain(firstStagingEvent.date);
});
});
describe('test event', () => {
beforeEach(() => {
wrapper = createComponent({
stageEvents: [{ ...firstTestEvent }],
selectedStage: { ...testStage, custom: false },
});
});
it('will set the workflow title to "Jobs"', () => {
expect(findTableHead().text()).toContain('Jobs');
expect(findTableHead().text()).not.toContain('Issues');
});
it('will not render the event title', () => {
expect(wrapper.findByTestId('vsa-stage-event-title').exists()).toBe(false);
});
it('will render the fork icon', () => {
expect(findIcon('fork').exists()).toBe(true);
});
it('will render the branch icon', () => {
expect(findIcon('commit').exists()).toBe(true);
});
it('will render the total time', () => {
expect(findStageTime().text()).toBe('2 mins');
});
it('will render the build shortSha', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-sha').text()).toBe(
firstTestEvent.shortSha,
);
});
it('will render the build pipeline success icon', () => {
expect(wrapper.findByTestId('status_success-icon').exists()).toBe(true);
});
it('will render the build date', () => {
const content = wrapper.findByTestId('vsa-stage-event-build-status-date').text();
expect(content).toContain(firstTestEvent.date);
});
it('will render the build event name', () => {
expect(wrapper.findByTestId('vsa-stage-event-build-name').text()).toContain(
firstTestEvent.name,
);
});
});
describe('isLoading = true', () => {
beforeEach(() => {
wrapper = createComponent({ isLoading: true }, true);

View File

@ -5,7 +5,7 @@ import {
mockLabels,
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import { DEFAULT_LABELS } from '~/vue_shared/components/filtered_search_bar/constants';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import {
getRecentlyUsedSuggestions,
setTokenValueToRecentlyUsed,
@ -51,7 +51,7 @@ const mockProps = {
active: false,
suggestions: [],
suggestionsLoading: false,
defaultSuggestions: DEFAULT_LABELS,
defaultSuggestions: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: mockStorageKey,
};

View File

@ -13,10 +13,7 @@ import {
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
DEFAULT_LABELS,
DEFAULT_NONE_ANY,
} from '~/vue_shared/components/filtered_search_bar/constants';
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
@ -208,7 +205,7 @@ describe('LabelToken', () => {
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
});
it('renders `DEFAULT_LABELS` as default suggestions', () => {
it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
wrapper = createComponent({
active: true,
config: { ...mockLabelToken },
@ -220,8 +217,8 @@ describe('LabelToken', () => {
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
DEFAULT_LABELS.forEach((label, index) => {
expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
DEFAULT_NONE_ANY.forEach((label, index) => {
expect(suggestions.at(index).text()).toBe(label.text);
});
});

View File

@ -79,56 +79,6 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
include_context 'when records are loaded by maintainer'
end
describe 'special case' do
let(:mr1) { create(:merge_request, source_project: project, allow_broken: true, created_at: 20.days.ago) }
let(:mr2) { create(:merge_request, source_project: project, allow_broken: true, created_at: 19.days.ago) }
let(:ci_build1) { create(:ci_build) }
let(:ci_build2) { create(:ci_build) }
let(:default_stages) { Gitlab::Analytics::CycleAnalytics::DefaultStages }
let(:stage) { build(:cycle_analytics_project_stage, default_stages.params_for_test_stage.merge(project: project)) }
before do
mr1.metrics.update!({
merged_at: 5.days.ago,
first_deployed_to_production_at: 1.day.ago,
latest_build_started_at: 5.days.ago,
latest_build_finished_at: 1.day.ago,
pipeline: ci_build1.pipeline
})
mr2.metrics.update!({
merged_at: 10.days.ago,
first_deployed_to_production_at: 5.days.ago,
latest_build_started_at: 9.days.ago,
latest_build_finished_at: 7.days.ago,
pipeline: ci_build2.pipeline
})
project.add_user(user, Gitlab::Access::MAINTAINER)
end
context 'returns build records' do
shared_examples 'orders build records by `latest_build_finished_at`' do
it 'orders by `latest_build_finished_at`' do
build_ids = subject.map { |item| item[:id] }
expect(build_ids).to eq([ci_build1.id, ci_build2.id])
end
end
context 'when requesting records for default test stage' do
include_examples 'orders build records by `latest_build_finished_at`'
end
context 'when requesting records for default staging stage' do
before do
stage.assign_attributes(default_stages.params_for_staging_stage)
end
include_examples 'orders build records by `latest_build_finished_at`'
end
end
end
end
describe 'pagination' do

View File

@ -57,6 +57,7 @@ issues:
- issue_email_participants
- test_reports
- requirement
- incident_management_issuable_escalation_status
work_item_type:
- issues
events:

View File

@ -33,70 +33,6 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to validate_length_of(:service).is_at_most(100) }
it { is_expected.to validate_length_of(:monitoring_tool).is_at_most(100) }
context 'when status is triggered' do
subject { triggered_alert }
context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
before do
triggered_alert.ended_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is acknowledged' do
subject { acknowledged_alert }
context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
before do
acknowledged_alert.ended_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is resolved' do
subject { resolved_alert }
context 'when ended_at is blank' do
before do
resolved_alert.ended_at = nil
end
it { is_expected.to be_invalid }
end
context 'when ended_at is present' do
it { is_expected.to be_valid }
end
end
context 'when status is ignored' do
subject { ignored_alert }
context 'when ended_at is blank' do
it { is_expected.to be_valid }
end
context 'when ended_at is present' do
before do
ignored_alert.ended_at = Time.current
end
it { is_expected.to be_invalid }
end
end
describe 'fingerprint' do
let_it_be(:fingerprint) { 'fingerprint' }
let_it_be(:project3, refind: true) { create(:project) }
@ -112,30 +48,30 @@ RSpec.describe AlertManagement::Alert do
let_it_be(:existing_alert, refind: true) { create(:alert_management_alert, fingerprint: fingerprint, project: project3) }
# We are only validating uniqueness for non-resolved alerts
where(:existing_status, :new_status, :valid) do
:resolved | :triggered | true
:resolved | :acknowledged | true
:resolved | :ignored | true
:resolved | :resolved | true
:triggered | :triggered | false
:triggered | :acknowledged | false
:triggered | :ignored | false
:triggered | :resolved | true
:acknowledged | :triggered | false
:acknowledged | :acknowledged | false
:acknowledged | :ignored | false
:acknowledged | :resolved | true
:ignored | :triggered | false
:ignored | :acknowledged | false
:ignored | :ignored | false
:ignored | :resolved | true
where(:existing_status_event, :new_status, :valid) do
:resolve | :triggered | true
:resolve | :acknowledged | true
:resolve | :ignored | true
:resolve | :resolved | true
:trigger | :triggered | false
:trigger | :acknowledged | false
:trigger | :ignored | false
:trigger | :resolved | true
:acknowledge | :triggered | false
:acknowledge | :acknowledged | false
:acknowledge | :ignored | false
:acknowledge | :resolved | true
:ignore | :triggered | false
:ignore | :acknowledged | false
:ignore | :ignored | false
:ignore | :resolved | true
end
with_them do
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project3) }
before do
existing_alert.change_status_to(existing_status)
existing_alert.update!(status_event: existing_status_event)
end
if params[:valid]
@ -196,20 +132,6 @@ RSpec.describe AlertManagement::Alert do
it { is_expected.to match_array(triggered_alert) }
end
describe '.for_status' do
let(:status) { :resolved }
subject { AlertManagement::Alert.for_status(status) }
it { is_expected.to match_array(resolved_alert) }
context 'with multiple statuses' do
let(:status) { [:resolved, :ignored] }
it { is_expected.to match_array([resolved_alert, ignored_alert]) }
end
end
describe '.for_fingerprint' do
let(:fingerprint) { SecureRandom.hex }
let(:alert_with_fingerprint) { triggered_alert }
@ -302,41 +224,7 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '.status_value' do
using RSpec::Parameterized::TableSyntax
where(:status, :status_value) do
:triggered | 0
:acknowledged | 1
:resolved | 2
:ignored | 3
:unknown | nil
end
with_them do
it 'returns status value by its name' do
expect(described_class.status_value(status)).to eq(status_value)
end
end
end
describe '.status_name' do
using RSpec::Parameterized::TableSyntax
where(:raw_status, :status) do
0 | :triggered
1 | :acknowledged
2 | :resolved
3 | :ignored
-1 | nil
end
with_them do
it 'returns status name by its values' do
expect(described_class.status_name(raw_status)).to eq(status)
end
end
end
it_behaves_like 'a model including Escalatable'
describe '.counts_by_status' do
subject { described_class.counts_by_status }
@ -454,87 +342,19 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '#open?' do
it 'returns true when the status is open status' do
expect(triggered_alert.open?).to be true
expect(acknowledged_alert.open?).to be true
expect(resolved_alert.open?).to be false
expect(ignored_alert.open?).to be false
end
end
describe '#to_reference' do
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
describe '#trigger' do
subject { alert.trigger }
context 'when alert is in triggered state' do
let(:alert) { triggered_alert }
it 'does not change the alert status' do
expect { subject }.not_to change { alert.reload.status }
end
end
context 'when alert not in triggered state' do
let(:alert) { resolved_alert }
it 'changes the alert status to triggered' do
expect { subject }.to change { alert.triggered? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { alert.reload.ended_at }.to nil
end
end
end
describe '#acknowledge' do
subject { alert.acknowledge }
let(:alert) { resolved_alert }
it 'changes the alert status to acknowledged' do
expect { subject }.to change { alert.acknowledged? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { alert.reload.ended_at }.to nil
end
end
describe '#resolve' do
let!(:ended_at) { Time.current }
subject do
alert.ended_at = ended_at
alert.resolve
end
context 'when alert already resolved' do
let(:alert) { resolved_alert }
it 'does not change the alert status' do
expect { subject }.not_to change { resolved_alert.reload.status }
end
end
context 'when alert is not resolved' do
let(:alert) { triggered_alert }
it 'changes alert status to "resolved"' do
expect { subject }.to change { alert.resolved? }.to(true)
end
end
end
describe '#ignore' do
subject { alert.ignore }
let(:alert) { resolved_alert }
it 'changes the alert status to ignored' do
expect { subject }.to change { alert.ignored? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { alert.reload.ended_at }.to nil
end
end
describe '#register_new_event!' do
subject { alert.register_new_event! }
@ -545,53 +365,20 @@ RSpec.describe AlertManagement::Alert do
end
end
describe '#status_event_for' do
using RSpec::Parameterized::TableSyntax
describe '#resolved_at' do
subject { resolved_alert.resolved_at }
where(:for_status, :event) do
:triggered | :trigger
'triggered' | :trigger
:acknowledged | :acknowledge
'acknowledged' | :acknowledge
:resolved | :resolve
'resolved' | :resolve
:ignored | :ignore
'ignored' | :ignore
:unknown | nil
nil | nil
'' | nil
1 | nil
end
with_them do
let(:alert) { build(:alert_management_alert, project: project) }
it 'returns event by status name' do
expect(alert.status_event_for(for_status)).to eq(event)
end
end
it { is_expected.to eq(resolved_alert.ended_at) }
end
describe '#change_status_to' do
let_it_be_with_reload(:alert) { create(:alert_management_alert, project: project) }
describe '#resolved_at=' do
let(:resolve_time) { Time.current }
context 'with valid statuses' do
it 'changes the status to triggered' do
alert.acknowledge! # change to non-triggered status
expect { alert.change_status_to(:triggered) }.to change { alert.triggered? }.to(true)
end
it 'sets ended_at' do
triggered_alert.resolved_at = resolve_time
%i(acknowledged resolved ignored).each do |status|
it "changes the status to #{status}" do
expect { alert.change_status_to(status) }.to change { alert.public_send(:"#{status}?") }.to(true)
end
end
end
context 'with invalid status' do
it 'does not change the current status' do
expect { alert.change_status_to(nil) }.not_to change { alert.status }
end
expect(triggered_alert.ended_at).to eq(resolve_time)
expect(triggered_alert.resolved_at).to eq(resolve_time)
end
end
end

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe IncidentManagement::IssuableEscalationStatus do
let_it_be(:issue) { create(:issue) }
subject(:escalation_status) { build(:incident_management_issuable_escalation_status, issue: issue) }
it { is_expected.to be_valid }
describe 'associations' do
it { is_expected.to belong_to(:issue) }
end
describe 'validatons' do
it { is_expected.to validate_presence_of(:issue) }
it { is_expected.to validate_uniqueness_of(:issue) }
end
it_behaves_like 'a model including Escalatable'
end

View File

@ -33,6 +33,7 @@ RSpec.describe Issue do
it { is_expected.to have_many(:prometheus_alerts) }
it { is_expected.to have_many(:issue_email_participants) }
it { is_expected.to have_many(:timelogs).autosave(true) }
it { is_expected.to have_one(:incident_management_issuable_escalation_status) }
describe 'versions.most_recent' do
it 'returns the most recent version' do

View File

@ -8,6 +8,9 @@ RSpec.describe 'value stream analytics events' do
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
let(:first_issue_iid) { project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s }
let(:first_mr_iid) { project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s }
before do
project.add_developer(user)
@ -25,8 +28,6 @@ RSpec.describe 'value stream analytics events' do
it 'lists the issue events' do
get project_cycle_analytics_issue_path(project, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
@ -34,8 +35,6 @@ RSpec.describe 'value stream analytics events' do
it 'lists the plan events' do
get project_cycle_analytics_plan_path(project, format: :json)
first_issue_iid = project.issues.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
@ -45,8 +44,6 @@ RSpec.describe 'value stream analytics events' do
expect(json_response['events']).not_to be_empty
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@ -54,15 +51,15 @@ RSpec.describe 'value stream analytics events' do
get project_cycle_analytics_test_path(project, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
it 'lists the review events' do
get project_cycle_analytics_review_path(project, format: :json)
first_mr_iid = project.merge_requests.sort_by_attribute(:created_desc).pluck(:iid).first.to_s
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
end
@ -70,7 +67,8 @@ RSpec.describe 'value stream analytics events' do
get project_cycle_analytics_staging_path(project, format: :json)
expect(json_response['events']).not_to be_empty
expect(json_response['events'].first['date']).not_to be_empty
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
end
context 'with private project and builds' do

View File

@ -66,8 +66,8 @@ RSpec.describe DraftNotes::PublishService do
let(:commit_id) { nil }
before do
create(:draft_note, merge_request: merge_request, author: user, note: 'first note', commit_id: commit_id, position: position)
create(:draft_note, merge_request: merge_request, author: user, note: 'second note', commit_id: commit_id, position: position)
create(:draft_note_on_text_diff, merge_request: merge_request, author: user, note: 'first note', commit_id: commit_id, position: position)
create(:draft_note_on_text_diff, merge_request: merge_request, author: user, note: 'second note', commit_id: commit_id, position: position)
end
context 'when review fails to create' do
@ -127,6 +127,30 @@ RSpec.describe DraftNotes::PublishService do
publish
end
context 'capturing diff notes positions' do
before do
# Need to execute this to ensure that we'll be able to test creation of
# DiffNotePosition records as that only happens when the `MergeRequest#merge_ref_head`
# is present. This service creates that for the specified merge request.
MergeRequests::MergeToRefService.new(project: project, current_user: user).execute(merge_request)
end
it 'creates diff_note_positions for diff notes' do
publish
notes = merge_request.notes.order(id: :asc)
expect(notes.first.diff_note_positions).to be_any
expect(notes.last.diff_note_positions).to be_any
end
it 'does not requests a lot from Gitaly', :request_store do
# NOTE: This should be reduced as we work on reducing Gitaly calls.
# Gitaly requests shouldn't go above this threshold as much as possible
# as it may add more to the Gitaly N+1 issue we are experiencing.
expect { publish }.to change { Gitlab::GitalyClient.get_request_count }.by(11)
end
end
context 'commit_id is set' do
let(:commit_id) { commit.id }

View File

@ -185,6 +185,14 @@ RSpec.describe Notes::CreateService do
expect(note.note_diff_file).to be_present
expect(note.diff_note_positions).to be_present
end
context 'when skip_capture_diff_note_position execute option is set to true' do
it 'does not execute Discussions::CaptureDiffNotePositionService' do
expect(Discussions::CaptureDiffNotePositionService).not_to receive(:new)
described_class.new(project_with_repo, user, new_opts).execute(skip_capture_diff_note_position: true)
end
end
end
context 'when DiffNote is a reply' do

View File

@ -0,0 +1,246 @@
# frozen_string_literal: true
RSpec.shared_examples 'a model including Escalatable' do
# rubocop:disable Rails/SaveBang -- Usage of factory symbol as argument causes a false-positive
let_it_be(:escalatable_factory) { factory_from_class(described_class) }
let_it_be(:triggered_escalatable, reload: true) { create(escalatable_factory, :triggered) }
let_it_be(:acknowledged_escalatable, reload: true) { create(escalatable_factory, :acknowledged) }
let_it_be(:resolved_escalatable, reload: true) { create(escalatable_factory, :resolved) }
let_it_be(:ignored_escalatable, reload: true) { create(escalatable_factory, :ignored) }
context 'validations' do
it { is_expected.to validate_presence_of(:status) }
context 'when status is triggered' do
subject { triggered_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
triggered_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is acknowledged' do
subject { acknowledged_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
acknowledged_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
context 'when status is resolved' do
subject { resolved_escalatable }
context 'when resolved_at is blank' do
before do
resolved_escalatable.resolved_at = nil
end
it { is_expected.to be_invalid }
end
context 'when resolved_at is present' do
it { is_expected.to be_valid }
end
end
context 'when status is ignored' do
subject { ignored_escalatable }
context 'when resolved_at is blank' do
it { is_expected.to be_valid }
end
context 'when resolved_at is present' do
before do
ignored_escalatable.resolved_at = Time.current
end
it { is_expected.to be_invalid }
end
end
end
context 'scopes' do
let(:all_escalatables) { described_class.where(id: [triggered_escalatable, acknowledged_escalatable, ignored_escalatable, resolved_escalatable])}
describe '.order_status' do
subject { all_escalatables.order_status(order) }
context 'descending' do
let(:order) { :desc }
# Downward arrow in UI always corresponds to default sort
it { is_expected.to eq([triggered_escalatable, acknowledged_escalatable, resolved_escalatable, ignored_escalatable]) }
end
context 'ascending' do
let(:order) { :asc }
it { is_expected.to eq([ignored_escalatable, resolved_escalatable, acknowledged_escalatable, triggered_escalatable]) }
end
end
end
describe '.status_value' do
using RSpec::Parameterized::TableSyntax
where(:status, :status_value) do
:triggered | 0
:acknowledged | 1
:resolved | 2
:ignored | 3
:unknown | nil
end
with_them do
it 'returns status value by its name' do
expect(described_class.status_value(status)).to eq(status_value)
end
end
end
describe '.status_name' do
using RSpec::Parameterized::TableSyntax
where(:raw_status, :status) do
0 | :triggered
1 | :acknowledged
2 | :resolved
3 | :ignored
-1 | nil
end
with_them do
it 'returns status name by its values' do
expect(described_class.status_name(raw_status)).to eq(status)
end
end
end
describe '#trigger' do
subject { escalatable.trigger }
context 'when escalatable is in triggered state' do
let(:escalatable) { triggered_escalatable }
it 'does not change the escalatable status' do
expect { subject }.not_to change { escalatable.reload.status }
end
end
context 'when escalatable is not in triggered state' do
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to triggered' do
expect { subject }.to change { escalatable.triggered? }.to(true)
end
it 'resets resolved at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
end
describe '#acknowledge' do
subject { escalatable.acknowledge }
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to acknowledged' do
expect { subject }.to change { escalatable.acknowledged? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
describe '#resolve' do
let!(:resolved_at) { Time.current }
subject do
escalatable.resolved_at = resolved_at
escalatable.resolve
end
context 'when escalatable is already resolved' do
let(:escalatable) { resolved_escalatable }
it 'does not change the escalatable status' do
expect { subject }.not_to change { resolved_escalatable.reload.status }
end
end
context 'when escalatable is not resolved' do
let(:escalatable) { triggered_escalatable }
it 'changes escalatable status to "resolved"' do
expect { subject }.to change { escalatable.resolved? }.to(true)
end
end
end
describe '#ignore' do
subject { escalatable.ignore }
let(:escalatable) { resolved_escalatable }
it 'changes the escalatable status to ignored' do
expect { subject }.to change { escalatable.ignored? }.to(true)
end
it 'resets ended at' do
expect { subject }.to change { escalatable.reload.resolved_at }.to nil
end
end
describe '#status_event_for' do
using RSpec::Parameterized::TableSyntax
where(:for_status, :event) do
:triggered | :trigger
'triggered' | :trigger
:acknowledged | :acknowledge
'acknowledged' | :acknowledge
:resolved | :resolve
'resolved' | :resolve
:ignored | :ignore
'ignored' | :ignore
:unknown | nil
nil | nil
'' | nil
1 | nil
end
with_them do
let(:escalatable) { build(escalatable_factory) }
it 'returns event by status name' do
expect(escalatable.status_event_for(for_status)).to eq(event)
end
end
end
private
def factory_from_class(klass)
klass.name.underscore.tr('/', '_')
end
end
# rubocop:enable Rails/SaveBang

View File

@ -0,0 +1,19 @@
/* eslint-disable import/no-commonjs */
const IS_EE = require('../../config/helpers/is_ee_env');
const IS_JH = require('../../config/helpers/is_jh_env');
const allPatterns = [
{
ignore: !IS_EE,
pattern: 'ee/**/*.*',
},
{
ignore: !IS_JH,
pattern: 'jh/**/*.*',
},
];
const ignorePatterns = allPatterns.filter((x) => x.ignore).map((x) => x.pattern);
module.exports = { ignorePatterns };

View File

@ -1,5 +0,0 @@
/* eslint-disable import/no-commonjs */
const IS_EE = require('../../config/helpers/is_ee_env');
module.exports = IS_EE ? {} : { ignorePatterns: ['ee/**/*.*'] };