Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.eslintrc.yml2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue43
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/finders/alert_management/alerts_finder.rb2
-rw-r--r--app/models/alert_management/alert.rb92
-rw-r--r--app/models/concerns/incident_management/escalatable.rb104
-rw-r--r--app/models/incident_management/issuable_escalation_status.rb15
-rw-r--r--app/models/issue.rb1
-rw-r--r--app/services/draft_notes/publish_service.rb30
-rw-r--r--app/services/notes/create_service.rb8
-rw-r--r--app/views/shared/boards/_show.html.haml6
-rw-r--r--db/migrate/20210729202143_create_incident_management_issuable_escalation_statuses.rb20
-rw-r--r--db/schema_migrations/202107292021431
-rw-r--r--db/structure.sql35
-rw-r--r--doc/api/graphql/reference/index.md4
-rw-r--r--doc/development/testing_guide/end_to_end/environment_selection.md9
-rw-r--r--doc/integration/oauth_provider.md18
-rw-r--r--lib/gitlab/analytics/cycle_analytics/records_fetcher.rb59
-rw-r--r--lib/gitlab/database/load_balancing/host_list.rb6
-rw-r--r--spec/factories/incident_management/issuable_escalation_statuses.rb25
-rw-r--r--spec/features/cycle_analytics_spec.rb10
-rw-r--r--spec/frontend/cycle_analytics/mock_data.js2
-rw-r--r--spec/frontend/cycle_analytics/stage_table_spec.js108
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js11
-rw-r--r--spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb50
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/alert_management/alert_spec.rb285
-rw-r--r--spec/models/incident_management/issuable_escalation_status_spec.rb22
-rw-r--r--spec/models/issue_spec.rb1
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb18
-rw-r--r--spec/services/draft_notes/publish_service_spec.rb28
-rw-r--r--spec/services/notes/create_service_spec.rb8
-rw-r--r--spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb246
-rw-r--r--tooling/eslint-config/conditionally_ignore.js19
-rw-r--r--tooling/eslint-config/conditionally_ignore_ee.js5
40 files changed, 674 insertions, 662 deletions
diff --git a/.eslintrc.yml b/.eslintrc.yml
index b6abb574e19..6b9a1ce62c0 100644
--- a/.eslintrc.yml
+++ b/.eslintrc.yml
@@ -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
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
index 7b31e8d902d..0c47838c773 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -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>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index ea8d9b76b2a..c1be2ce7096 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -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.',
);
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index ebda2dce61c..2e9634819a0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -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) },
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 172b5c402f6..d1326e96794 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -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"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
index fda43f00a10..9f68308808e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
@@ -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)">
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index ae514c47068..c31f3a25fb1 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -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: {
diff --git a/app/finders/alert_management/alerts_finder.rb b/app/finders/alert_management/alerts_finder.rb
index b4f66a38faa..1fbc1a4a258 100644
--- a/app/finders/alert_management/alerts_finder.rb
+++ b/app/finders/alert_management/alerts_finder.rb
@@ -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)
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index d0e4163dcdb..0555c6e78bd 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -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
diff --git a/app/models/concerns/incident_management/escalatable.rb b/app/models/concerns/incident_management/escalatable.rb
new file mode 100644
index 00000000000..78dce63f59e
--- /dev/null
+++ b/app/models/concerns/incident_management/escalatable.rb
@@ -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
diff --git a/app/models/incident_management/issuable_escalation_status.rb b/app/models/incident_management/issuable_escalation_status.rb
new file mode 100644
index 00000000000..88aef104d88
--- /dev/null
+++ b/app/models/incident_management/issuable_escalation_status.rb
@@ -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')
diff --git a/app/models/issue.rb b/app/models/issue.rb
index bf3e9c06d68..48e3fdd51e9 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -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
diff --git a/app/services/draft_notes/publish_service.rb b/app/services/draft_notes/publish_service.rb
index d73c3417a8b..3a1db16aaf4 100644
--- a/app/services/draft_notes/publish_service.rb
+++ b/app/services/draft_notes/publish_service.rb
@@ -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
- draft_notes.delete_all
+ 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
diff --git a/app/services/notes/create_service.rb b/app/services/notes/create_service.rb
index 542fafb901b..194c3d7bf7b 100644
--- a/app/services/notes/create_service.rb
+++ b/app/services/notes/create_service.rb
@@ -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
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index 9ccd5655fb0..a49c17e9265 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -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
diff --git a/db/migrate/20210729202143_create_incident_management_issuable_escalation_statuses.rb b/db/migrate/20210729202143_create_incident_management_issuable_escalation_statuses.rb
new file mode 100644
index 00000000000..b16904a3b47
--- /dev/null
+++ b/db/migrate/20210729202143_create_incident_management_issuable_escalation_statuses.rb
@@ -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
diff --git a/db/schema_migrations/20210729202143 b/db/schema_migrations/20210729202143
new file mode 100644
index 00000000000..c817508eb5f
--- /dev/null
+++ b/db/schema_migrations/20210729202143
@@ -0,0 +1 @@
+ce20c699d6e6d6baf812c926dde08485764faa2fdeb8af14808670bf692aab00 \ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 413c90d060c..91c02925a4e 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -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;
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index eab921a1de6..63781f9f353 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -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`
diff --git a/doc/development/testing_guide/end_to_end/environment_selection.md b/doc/development/testing_guide/end_to_end/environment_selection.md
deleted file mode 100644
index e03966d754b..00000000000
--- a/doc/development/testing_guide/end_to_end/environment_selection.md
+++ /dev/null
@@ -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 -->
diff --git a/doc/integration/oauth_provider.md b/doc/integration/oauth_provider.md
index 7540a02e520..596fff47716 100644
--- a/doc/integration/oauth_provider.md
+++ b/doc/integration/oauth_provider.md
@@ -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
diff --git a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
index 9a37a41ff81..f94696e3186 100644
--- a/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
+++ b/lib/gitlab/analytics/cycle_analytics/records_fetcher.rb
@@ -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
- .joins(ci_build_join)
- .select(build_table[:id], *time_columns)
-
- 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 = 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
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
diff --git a/lib/gitlab/database/load_balancing/host_list.rb b/lib/gitlab/database/load_balancing/host_list.rb
index fb3175c7d5d..aa731521732 100644
--- a/lib/gitlab/database/load_balancing/host_list.rb
+++ b/lib/gitlab/database/load_balancing/host_list.rb
@@ -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
diff --git a/spec/factories/incident_management/issuable_escalation_statuses.rb b/spec/factories/incident_management/issuable_escalation_statuses.rb
new file mode 100644
index 00000000000..54d0887f386
--- /dev/null
+++ b/spec/factories/incident_management/issuable_escalation_statuses.rb
@@ -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
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index a13aed35768..de6cb53fdfa 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -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)
diff --git a/spec/frontend/cycle_analytics/mock_data.js b/spec/frontend/cycle_analytics/mock_data.js
index c29e81edf46..d9659d5d4c3 100644
--- a/spec/frontend/cycle_analytics/mock_data.js
+++ b/spec/frontend/cycle_analytics/mock_data.js
@@ -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;
diff --git a/spec/frontend/cycle_analytics/stage_table_spec.js b/spec/frontend/cycle_analytics/stage_table_spec.js
index 21130810cce..47a2ce4444b 100644
--- a/spec/frontend/cycle_analytics/stage_table_spec.js
+++ b/spec/frontend/cycle_analytics/stage_table_spec.js
@@ -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);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index c746cb7749a..eb1dbed52cc 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -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,
};
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
index e5ffbd41afa..a348344b9dd 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -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);
});
});
diff --git a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
index ebc5ae2a632..4fe55ba0c0c 100644
--- a/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
+++ b/spec/lib/gitlab/analytics/cycle_analytics/records_fetcher_spec.rb
@@ -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
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 36eb1c04c81..2b7138a7a10 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -57,6 +57,7 @@ issues:
- issue_email_participants
- test_reports
- requirement
+- incident_management_issuable_escalation_status
work_item_type:
- issues
events:
diff --git a/spec/models/alert_management/alert_spec.rb b/spec/models/alert_management/alert_spec.rb
index 18d486740b8..35398e29062 100644
--- a/spec/models/alert_management/alert_spec.rb
+++ b/spec/models/alert_management/alert_spec.rb
@@ -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,85 +342,17 @@ RSpec.describe AlertManagement::Alert do
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
+ 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 '#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
+ describe '#to_reference' do
+ it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
end
describe '#register_new_event!' do
@@ -545,53 +365,20 @@ RSpec.describe AlertManagement::Alert do
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
+ describe '#resolved_at' do
+ subject { resolved_alert.resolved_at }
- 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
diff --git a/spec/models/incident_management/issuable_escalation_status_spec.rb b/spec/models/incident_management/issuable_escalation_status_spec.rb
new file mode 100644
index 00000000000..f3e7b90cf3c
--- /dev/null
+++ b/spec/models/incident_management/issuable_escalation_status_spec.rb
@@ -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
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 8e28b7dd250..116bda7a18b 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -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
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 7921fdcb0de..89d46b64311 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -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
diff --git a/spec/services/draft_notes/publish_service_spec.rb b/spec/services/draft_notes/publish_service_spec.rb
index 2e1de367da3..4f761454516 100644
--- a/spec/services/draft_notes/publish_service_spec.rb
+++ b/spec/services/draft_notes/publish_service_spec.rb
@@ -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 }
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 6621ad1f294..793e9ed9848 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -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
diff --git a/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
new file mode 100644
index 00000000000..7b33a95bfa1
--- /dev/null
+++ b/spec/support/shared_examples/models/concerns/incident_management/escalatable_shared_examples.rb
@@ -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
diff --git a/tooling/eslint-config/conditionally_ignore.js b/tooling/eslint-config/conditionally_ignore.js
new file mode 100644
index 00000000000..6132c1f52f4
--- /dev/null
+++ b/tooling/eslint-config/conditionally_ignore.js
@@ -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 };
diff --git a/tooling/eslint-config/conditionally_ignore_ee.js b/tooling/eslint-config/conditionally_ignore_ee.js
deleted file mode 100644
index e5e3c8013f4..00000000000
--- a/tooling/eslint-config/conditionally_ignore_ee.js
+++ /dev/null
@@ -1,5 +0,0 @@
-/* eslint-disable import/no-commonjs */
-
-const IS_EE = require('../../config/helpers/is_ee_env');
-
-module.exports = IS_EE ? {} : { ignorePatterns: ['ee/**/*.*'] };