diff options
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> - · - </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/**/*.*'] }; |