Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
25d4a24f83
commit
514ace3632
|
@ -3,7 +3,7 @@ extends:
|
||||||
- plugin:@gitlab/i18n
|
- plugin:@gitlab/i18n
|
||||||
- plugin:no-jquery/slim
|
- plugin:no-jquery/slim
|
||||||
- plugin:no-jquery/deprecated-3.4
|
- plugin:no-jquery/deprecated-3.4
|
||||||
- ./tooling/eslint-config/conditionally_ignore_ee.js
|
- ./tooling/eslint-config/conditionally_ignore.js
|
||||||
globals:
|
globals:
|
||||||
__webpack_public_path__: true
|
__webpack_public_path__: true
|
||||||
gl: false
|
gl: false
|
||||||
|
|
|
@ -17,8 +17,6 @@ import {
|
||||||
PAGINATION_SORT_FIELD_DURATION,
|
PAGINATION_SORT_FIELD_DURATION,
|
||||||
PAGINATION_SORT_DIRECTION_ASC,
|
PAGINATION_SORT_DIRECTION_ASC,
|
||||||
PAGINATION_SORT_DIRECTION_DESC,
|
PAGINATION_SORT_DIRECTION_DESC,
|
||||||
STAGE_TITLE_STAGING,
|
|
||||||
STAGE_TITLE_TEST,
|
|
||||||
} from '../constants';
|
} from '../constants';
|
||||||
import TotalTime from './total_time_component.vue';
|
import TotalTime from './total_time_component.vue';
|
||||||
|
|
||||||
|
@ -107,28 +105,12 @@ export default {
|
||||||
emptyStateTitleText() {
|
emptyStateTitleText() {
|
||||||
return this.emptyStateTitle || NOT_ENOUGH_DATA_ERROR;
|
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() {
|
isMergeRequestStage() {
|
||||||
const [firstEvent] = this.stageEvents;
|
const [firstEvent] = this.stageEvents;
|
||||||
return this.isMrLink(firstEvent.url);
|
return this.isMrLink(firstEvent.url);
|
||||||
},
|
},
|
||||||
workflowTitle() {
|
workflowTitle() {
|
||||||
if (this.isDefaultTestStage) {
|
if (this.isMergeRequestStage) {
|
||||||
return WORKFLOW_COLUMN_TITLES.jobs;
|
|
||||||
} else if (this.isDefaultStagingStage) {
|
|
||||||
return WORKFLOW_COLUMN_TITLES.deployments;
|
|
||||||
} else if (this.isMergeRequestStage) {
|
|
||||||
return WORKFLOW_COLUMN_TITLES.mergeRequests;
|
return WORKFLOW_COLUMN_TITLES.mergeRequests;
|
||||||
}
|
}
|
||||||
return WORKFLOW_COLUMN_TITLES.issues;
|
return WORKFLOW_COLUMN_TITLES.issues;
|
||||||
|
@ -209,22 +191,6 @@ export default {
|
||||||
<div data-testid="vsa-stage-event">
|
<div data-testid="vsa-stage-event">
|
||||||
<div v-if="item.id" data-testid="vsa-stage-content">
|
<div v-if="item.id" data-testid="vsa-stage-content">
|
||||||
<p class="gl-m-0">
|
<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"
|
<gl-link class="gl-text-black-normal pipeline-id" :href="item.url"
|
||||||
>#{{ item.id }}</gl-link
|
>#{{ item.id }}</gl-link
|
||||||
>
|
>
|
||||||
|
@ -246,12 +212,7 @@ export default {
|
||||||
>
|
>
|
||||||
</p>
|
</p>
|
||||||
<p class="gl-m-0">
|
<p class="gl-m-0">
|
||||||
<span v-if="isDefaultTestStage" data-testid="vsa-stage-event-build-status-date">
|
<span data-testid="vsa-stage-event-build-author-and-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">
|
|
||||||
<gl-link class="gl-text-black-normal build-date" :href="item.url">{{
|
<gl-link class="gl-text-black-normal build-date" :href="item.url">{{
|
||||||
item.date
|
item.date
|
||||||
}}</gl-link>
|
}}</gl-link>
|
||||||
|
|
|
@ -25,9 +25,6 @@ export const PAGINATION_SORT_FIELD_DURATION = 'duration';
|
||||||
export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
|
export const PAGINATION_SORT_DIRECTION_DESC = 'desc';
|
||||||
export const PAGINATION_SORT_DIRECTION_ASC = 'asc';
|
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 = __(
|
export const I18N_VSA_ERROR_STAGES = __(
|
||||||
'There was an error fetching value stream analytics stages.',
|
'There was an error fetching value stream analytics stages.',
|
||||||
);
|
);
|
||||||
|
|
|
@ -9,6 +9,7 @@ export const FILTER_ANY = 'Any';
|
||||||
export const FILTER_CURRENT = 'Current';
|
export const FILTER_CURRENT = 'Current';
|
||||||
export const FILTER_UPCOMING = 'Upcoming';
|
export const FILTER_UPCOMING = 'Upcoming';
|
||||||
export const FILTER_STARTED = 'Started';
|
export const FILTER_STARTED = 'Started';
|
||||||
|
export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
|
||||||
|
|
||||||
export const OPERATOR_IS = '=';
|
export const OPERATOR_IS = '=';
|
||||||
export const OPERATOR_IS_TEXT = __('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) },
|
{ value: FILTER_CURRENT, text: __(FILTER_CURRENT) },
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const DEFAULT_LABELS = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
|
|
||||||
|
|
||||||
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
|
export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
|
||||||
{ value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
|
{ value: FILTER_UPCOMING, text: __(FILTER_UPCOMING) },
|
||||||
{ value: FILTER_STARTED, text: __(FILTER_STARTED) },
|
{ value: FILTER_STARTED, text: __(FILTER_STARTED) },
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
} from '@gitlab/ui';
|
} from '@gitlab/ui';
|
||||||
import { debounce } from 'lodash';
|
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';
|
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -89,6 +89,14 @@ export default {
|
||||||
activeTokenValue() {
|
activeTokenValue() {
|
||||||
return this.getActiveTokenValue(this.suggestions, this.value.data);
|
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
|
* Return all the suggestions when searchKey is present
|
||||||
* otherwise return only the suggestions which aren't
|
* otherwise return only the suggestions which aren't
|
||||||
|
@ -104,7 +112,7 @@ export default {
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
showDefaultSuggestions() {
|
showDefaultSuggestions() {
|
||||||
return this.defaultSuggestions.length;
|
return this.availableDefaultSuggestions.length;
|
||||||
},
|
},
|
||||||
showRecentSuggestions() {
|
showRecentSuggestions() {
|
||||||
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
|
return this.isRecentSuggestionsEnabled && this.recentSuggestions.length && !this.searchKey;
|
||||||
|
@ -180,7 +188,7 @@ export default {
|
||||||
<template v-if="showSuggestions" #suggestions>
|
<template v-if="showSuggestions" #suggestions>
|
||||||
<template v-if="showDefaultSuggestions">
|
<template v-if="showDefaultSuggestions">
|
||||||
<gl-filtered-search-suggestion
|
<gl-filtered-search-suggestion
|
||||||
v-for="token in defaultSuggestions"
|
v-for="token in availableDefaultSuggestions"
|
||||||
:key="token.value"
|
:key="token.value"
|
||||||
:value="token.value"
|
:value="token.value"
|
||||||
>
|
>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import {
|
||||||
import { debounce } from 'lodash';
|
import { debounce } from 'lodash';
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import { __ } from '~/locale';
|
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 {
|
export default {
|
||||||
separator: '::&',
|
separator: '::&',
|
||||||
|
@ -48,6 +48,14 @@ export default {
|
||||||
defaultEpics() {
|
defaultEpics() {
|
||||||
return this.config.defaultEpics || DEFAULT_NONE_ANY;
|
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() {
|
activeEpic() {
|
||||||
if (this.currentValue && this.epics.length) {
|
if (this.currentValue && this.epics.length) {
|
||||||
// Check if current value is an epic ID.
|
// Check if current value is an epic ID.
|
||||||
|
@ -127,13 +135,13 @@ export default {
|
||||||
</template>
|
</template>
|
||||||
<template #suggestions>
|
<template #suggestions>
|
||||||
<gl-filtered-search-suggestion
|
<gl-filtered-search-suggestion
|
||||||
v-for="epic in defaultEpics"
|
v-for="epic in availableDefaultEpics"
|
||||||
:key="epic.value"
|
:key="epic.value"
|
||||||
:value="epic.value"
|
:value="epic.value"
|
||||||
>
|
>
|
||||||
{{ epic.text }}
|
{{ epic.text }}
|
||||||
</gl-filtered-search-suggestion>
|
</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" />
|
<gl-loading-icon v-if="loading" size="sm" />
|
||||||
<template v-else>
|
<template v-else>
|
||||||
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
|
<gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
|
||||||
|
|
|
@ -5,7 +5,7 @@ import createFlash from '~/flash';
|
||||||
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
|
||||||
import { __ } from '~/locale';
|
import { __ } from '~/locale';
|
||||||
|
|
||||||
import { DEFAULT_LABELS } from '../constants';
|
import { DEFAULT_NONE_ANY } from '../constants';
|
||||||
import { stripQuotes } from '../filtered_search_utils';
|
import { stripQuotes } from '../filtered_search_utils';
|
||||||
|
|
||||||
import BaseToken from './base_token.vue';
|
import BaseToken from './base_token.vue';
|
||||||
|
@ -38,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
defaultLabels() {
|
defaultLabels() {
|
||||||
return this.config.defaultLabels || DEFAULT_LABELS;
|
return this.config.defaultLabels || DEFAULT_NONE_ANY;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -46,7 +46,7 @@ module AlertManagement
|
||||||
def by_status(collection)
|
def by_status(collection)
|
||||||
values = AlertManagement::Alert.status_names & Array(params[:status])
|
values = AlertManagement::Alert.status_names & Array(params[:status])
|
||||||
|
|
||||||
values.present? ? collection.for_status(values) : collection
|
values.present? ? collection.with_status(values) : collection
|
||||||
end
|
end
|
||||||
|
|
||||||
def by_search(collection)
|
def by_search(collection)
|
||||||
|
|
|
@ -13,20 +13,7 @@ module AlertManagement
|
||||||
include Presentable
|
include Presentable
|
||||||
include Gitlab::Utils::StrongMemoize
|
include Gitlab::Utils::StrongMemoize
|
||||||
include Referable
|
include Referable
|
||||||
|
include ::IncidentManagement::Escalatable
|
||||||
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
|
|
||||||
|
|
||||||
belongs_to :project
|
belongs_to :project
|
||||||
belongs_to :issue, optional: true
|
belongs_to :issue, optional: true
|
||||||
|
@ -44,6 +31,9 @@ module AlertManagement
|
||||||
|
|
||||||
sha_attribute :fingerprint
|
sha_attribute :fingerprint
|
||||||
|
|
||||||
|
# Allow :ended_at to be managed by Escalatable
|
||||||
|
alias_attribute :resolved_at, :ended_at
|
||||||
|
|
||||||
TITLE_MAX_LENGTH = 200
|
TITLE_MAX_LENGTH = 200
|
||||||
DESCRIPTION_MAX_LENGTH = 1_000
|
DESCRIPTION_MAX_LENGTH = 1_000
|
||||||
SERVICE_MAX_LENGTH = 100
|
SERVICE_MAX_LENGTH = 100
|
||||||
|
@ -57,7 +47,6 @@ module AlertManagement
|
||||||
validates :project, presence: true
|
validates :project, presence: true
|
||||||
validates :events, presence: true
|
validates :events, presence: true
|
||||||
validates :severity, presence: true
|
validates :severity, presence: true
|
||||||
validates :status, presence: true
|
|
||||||
validates :started_at, presence: true
|
validates :started_at, presence: true
|
||||||
validates :fingerprint, allow_blank: true, uniqueness: {
|
validates :fingerprint, allow_blank: true, uniqueness: {
|
||||||
scope: :project,
|
scope: :project,
|
||||||
|
@ -80,52 +69,10 @@ module AlertManagement
|
||||||
threat_monitoring: 1
|
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 :iid, to: :issue, prefix: true, allow_nil: true
|
||||||
delegate :details_url, to: :present
|
delegate :details_url, to: :present
|
||||||
|
|
||||||
scope :for_iid, -> (iid) { where(iid: iid) }
|
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_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
|
||||||
scope :for_environment, -> (environment) { where(environment: environment) }
|
scope :for_environment, -> (environment) { where(environment: environment) }
|
||||||
scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
|
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, -> (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) }
|
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 }
|
scope :counts_by_project_id, -> { group(:project_id).count }
|
||||||
|
|
||||||
alias_method :state, :status_name
|
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
|
def self.counts_by_status
|
||||||
group(:status).count.transform_keys { |k| status_name(k) }
|
group(:status).count.transform_keys { |k| status_name(k) }
|
||||||
end
|
end
|
||||||
|
|
||||||
def self.status_names
|
|
||||||
@status_names ||= state_machine_statuses.keys
|
|
||||||
end
|
|
||||||
|
|
||||||
def self.sort_by_attribute(method)
|
def self.sort_by_attribute(method)
|
||||||
case method.to_s
|
case method.to_s
|
||||||
when 'started_at_asc' then order_start_time(:asc)
|
when 'started_at_asc' then order_start_time(:asc)
|
||||||
|
@ -229,15 +154,6 @@ module AlertManagement
|
||||||
self.class.open_status?(status_name)
|
self.class.open_status?(status_name)
|
||||||
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
|
|
||||||
|
|
||||||
def change_status_to(new_status)
|
|
||||||
event = status_event_for(new_status)
|
|
||||||
event && fire_status_event(event)
|
|
||||||
end
|
|
||||||
|
|
||||||
def prometheus?
|
def prometheus?
|
||||||
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
|
monitoring_tool == Gitlab::AlertManagement::Payload::MONITORING_TOOLS[:prometheus]
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -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')
|
|
@ -77,6 +77,7 @@ class Issue < ApplicationRecord
|
||||||
has_one :issuable_severity
|
has_one :issuable_severity
|
||||||
has_one :sentry_issue
|
has_one :sentry_issue
|
||||||
has_one :alert_management_alert, class_name: 'AlertManagement::Alert'
|
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 :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_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
|
has_many :prometheus_alerts, through: :prometheus_alert_events
|
||||||
|
|
|
@ -32,26 +32,28 @@ module DraftNotes
|
||||||
|
|
||||||
review = Review.create!(author: current_user, merge_request: merge_request, project: project)
|
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
|
draft_note.review = review
|
||||||
create_note_from_draft(draft_note)
|
create_note_from_draft(draft_note, skip_capture_diff_note_position: true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
capture_diff_note_positions(created_notes)
|
||||||
draft_notes.delete_all
|
draft_notes.delete_all
|
||||||
|
|
||||||
set_reviewed
|
set_reviewed
|
||||||
|
|
||||||
notification_service.async.new_review(review)
|
notification_service.async.new_review(review)
|
||||||
MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
|
MergeRequests::ResolvedDiscussionNotificationService.new(project: project, current_user: current_user).execute(merge_request)
|
||||||
end
|
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
|
# Make sure the diff file is unfolded in order to find the correct line
|
||||||
# codes.
|
# codes.
|
||||||
draft.diff_file&.unfold_diff_lines(draft.original_position)
|
draft.diff_file&.unfold_diff_lines(draft.original_position)
|
||||||
|
|
||||||
note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute
|
note = Notes::CreateService.new(draft.project, draft.author, draft.publish_params).execute(
|
||||||
set_discussion_resolve_status(note, draft)
|
skip_capture_diff_note_position: skip_capture_diff_note_position
|
||||||
|
)
|
||||||
|
|
||||||
|
set_discussion_resolve_status(note, draft)
|
||||||
note
|
note
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,5 +72,19 @@ module DraftNotes
|
||||||
def set_reviewed
|
def set_reviewed
|
||||||
::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
|
::MergeRequests::MarkReviewerReviewedService.new(project: project, current_user: current_user).execute(merge_request)
|
||||||
end
|
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
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,7 @@ module Notes
|
||||||
class CreateService < ::Notes::BaseService
|
class CreateService < ::Notes::BaseService
|
||||||
include IncidentManagement::UsageData
|
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
|
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
|
# n+1: https://gitlab.com/gitlab-org/gitlab-foss/issues/37440
|
||||||
|
@ -34,7 +34,7 @@ module Notes
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
note
|
note
|
||||||
|
@ -68,14 +68,14 @@ module Notes
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def when_saved(note)
|
def when_saved(note, skip_capture_diff_note_position: false)
|
||||||
todo_service.new_note(note, current_user)
|
todo_service.new_note(note, current_user)
|
||||||
clear_noteable_diffs_cache(note)
|
clear_noteable_diffs_cache(note)
|
||||||
Suggestions::CreateService.new(note).execute
|
Suggestions::CreateService.new(note).execute
|
||||||
increment_usage_counter(note)
|
increment_usage_counter(note)
|
||||||
track_event(note, current_user)
|
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)
|
Discussions::CaptureDiffNotePositionService.new(note.noteable, note.diff_file&.paths).execute(note.discussion)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -4,7 +4,8 @@
|
||||||
- @no_container = true
|
- @no_container = true
|
||||||
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
|
- @content_wrapper_class = "#{@content_wrapper_class} gl-relative"
|
||||||
- @content_class = "issue-boards-content js-focus-mode-board"
|
- @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")
|
- breadcrumb_title _("Epic Boards")
|
||||||
- else
|
- else
|
||||||
- breadcrumb_title _("Issue Boards")
|
- breadcrumb_title _("Issue Boards")
|
||||||
|
@ -19,5 +20,6 @@
|
||||||
= render 'shared/issuable/search_bar', type: :boards, board: board
|
= 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-app.boards-app.position-relative{ "v-cloak" => "true", data: board_data, ":class" => "{ 'is-compact': detailIssueVisible }" }
|
||||||
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
|
%board-content{ ":lists" => "state.lists", ":disabled" => "disabled" }
|
||||||
|
- if !is_epic_board && !Feature.enabled?(:graphql_board_lists, default_enabled: :yaml)
|
||||||
= render "shared/boards/components/sidebar", group: group
|
= render "shared/boards/components/sidebar", group: group
|
||||||
%board-settings-sidebar
|
%board-settings-sidebar
|
||||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
||||||
|
ce20c699d6e6d6baf812c926dde08485764faa2fdeb8af14808670bf692aab00
|
|
@ -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;
|
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 (
|
CREATE TABLE incident_management_oncall_participants (
|
||||||
id bigint NOT NULL,
|
id bigint NOT NULL,
|
||||||
oncall_rotation_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_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_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);
|
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
|
ALTER TABLE ONLY incident_management_escalation_rules
|
||||||
ADD CONSTRAINT incident_management_escalation_rules_pkey PRIMARY KEY (id);
|
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
|
ALTER TABLE ONLY incident_management_oncall_participants
|
||||||
ADD CONSTRAINT incident_management_oncall_participants_pkey PRIMARY KEY (id);
|
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_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_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);
|
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 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 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);
|
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
|
ALTER TABLE ONLY vulnerability_findings_remediations
|
||||||
ADD CONSTRAINT fk_rails_28a8d0cf93 FOREIGN KEY (vulnerability_occurrence_id) REFERENCES vulnerability_occurrences(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY resource_state_events
|
||||||
ADD CONSTRAINT fk_rails_29af06892a FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY board_group_recent_visits
|
||||||
ADD CONSTRAINT fk_rails_f410736518 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
|
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
|
ALTER TABLE ONLY resource_state_events
|
||||||
ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
|
ADD CONSTRAINT fk_rails_f5827a7ccd FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
|
|
@ -14601,8 +14601,8 @@ Alert status values.
|
||||||
| Value | Description |
|
| Value | Description |
|
||||||
| ----- | ----------- |
|
| ----- | ----------- |
|
||||||
| <a id="alertmanagementstatusacknowledged"></a>`ACKNOWLEDGED` | Someone is actively investigating the problem. |
|
| <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="alertmanagementstatusignored"></a>`IGNORED` | No action will be taken. |
|
||||||
| <a id="alertmanagementstatusresolved"></a>`RESOLVED` | No further work is required. |
|
| <a id="alertmanagementstatusresolved"></a>`RESOLVED` | The problem has been addressed. |
|
||||||
| <a id="alertmanagementstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
|
| <a id="alertmanagementstatustriggered"></a>`TRIGGERED` | Investigation has not started. |
|
||||||
|
|
||||||
### `ApiFuzzingScanMode`
|
### `ApiFuzzingScanMode`
|
||||||
|
|
|
@ -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 -->
|
|
|
@ -51,10 +51,13 @@ To add a new application for your user:
|
||||||
1. In the left sidebar, select **Applications**.
|
1. In the left sidebar, select **Applications**.
|
||||||
1. Enter a **Name**, **Redirect URI** and OAuth 2 scopes as defined in [Authorized Applications](#authorized-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.
|
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.
|
- The OAuth 2 Client ID in the **Application ID** field.
|
||||||
- Secret: OAuth 2 Client Secret.
|
- 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
|
## Group owned applications
|
||||||
|
|
||||||
|
@ -66,10 +69,13 @@ To add a new application for a group:
|
||||||
1. On the left sidebar, select **Settings > Applications**.
|
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).
|
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.
|
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.
|
- The OAuth 2 Client ID in the **Application ID** field.
|
||||||
- Secret: OAuth 2 Client Secret.
|
- 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
|
## Instance-wide applications
|
||||||
|
|
||||||
|
|
|
@ -38,22 +38,6 @@ module Gitlab
|
||||||
# rubocop: disable CodeReuse/ActiveRecord
|
# rubocop: disable CodeReuse/ActiveRecord
|
||||||
def serialized_records
|
def serialized_records
|
||||||
strong_memoize(:serialized_records) do
|
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)
|
records = ordered_and_limited_query.select(*columns, *time_columns)
|
||||||
|
|
||||||
yield records if block_given?
|
yield records if block_given?
|
||||||
|
@ -70,7 +54,6 @@ module Gitlab
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
# rubocop: enable CodeReuse/ActiveRecord
|
# rubocop: enable CodeReuse/ActiveRecord
|
||||||
|
|
||||||
private
|
private
|
||||||
|
@ -83,26 +66,10 @@ module Gitlab
|
||||||
end
|
end
|
||||||
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
|
def serializer
|
||||||
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
|
MAPPINGS.fetch(subject_class).fetch(:serializer_class).new
|
||||||
end
|
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
|
def ordered_and_limited_query
|
||||||
strong_memoize(:ordered_and_limited_query) do
|
strong_memoize(:ordered_and_limited_query) do
|
||||||
order_by(query, sort, direction, columns).page(page).per(per_page).without_count
|
order_by(query, sort, direction, columns).page(page).per(per_page).without_count
|
||||||
|
|
|
@ -35,6 +35,12 @@ module Gitlab
|
||||||
|
|
||||||
def hosts=(hosts)
|
def hosts=(hosts)
|
||||||
@mutex.synchronize do
|
@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
|
@hosts = hosts
|
||||||
unsafe_shuffle
|
unsafe_shuffle
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -84,13 +84,13 @@ RSpec.describe 'Value Stream Analytics', :js do
|
||||||
expect_merge_request_to_be_present
|
expect_merge_request_to_be_present
|
||||||
|
|
||||||
click_stage('Test')
|
click_stage('Test')
|
||||||
expect_build_to_be_present
|
expect_merge_request_to_be_present
|
||||||
|
|
||||||
click_stage('Review')
|
click_stage('Review')
|
||||||
expect_merge_request_to_be_present
|
expect_merge_request_to_be_present
|
||||||
|
|
||||||
click_stage('Staging')
|
click_stage('Staging')
|
||||||
expect_build_to_be_present
|
expect_merge_request_to_be_present
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when I change the time period observed" do
|
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}")
|
expect(find(stage_table_selector)).to have_content("##{issue.iid}")
|
||||||
end
|
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
|
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.title)
|
||||||
expect(find(stage_table_selector)).to have_content(mr.author.name)
|
expect(find(stage_table_selector)).to have_content(mr.author.name)
|
||||||
|
|
|
@ -135,8 +135,6 @@ export const convertedData = {
|
||||||
export const rawIssueEvents = stageFixtures.issue;
|
export const rawIssueEvents = stageFixtures.issue;
|
||||||
export const issueEvents = deepCamelCase(rawIssueEvents);
|
export const issueEvents = deepCamelCase(rawIssueEvents);
|
||||||
export const reviewEvents = deepCamelCase(stageFixtures.review);
|
export const reviewEvents = deepCamelCase(stageFixtures.review);
|
||||||
export const testEvents = deepCamelCase(stageFixtures.test);
|
|
||||||
export const stagingEvents = deepCamelCase(stageFixtures.staging);
|
|
||||||
|
|
||||||
export const pathNavIssueMetric = 172800;
|
export const pathNavIssueMetric = 172800;
|
||||||
|
|
||||||
|
|
|
@ -4,16 +4,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
|
||||||
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
|
||||||
import StageTable from '~/cycle_analytics/components/stage_table.vue';
|
import StageTable from '~/cycle_analytics/components/stage_table.vue';
|
||||||
import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
|
import { PAGINATION_SORT_FIELD_DURATION } from '~/cycle_analytics/constants';
|
||||||
import {
|
import { issueEvents, issueStage, reviewStage, reviewEvents } from './mock_data';
|
||||||
stagingEvents,
|
|
||||||
stagingStage,
|
|
||||||
issueEvents,
|
|
||||||
issueStage,
|
|
||||||
testEvents,
|
|
||||||
testStage,
|
|
||||||
reviewStage,
|
|
||||||
reviewEvents,
|
|
||||||
} from './mock_data';
|
|
||||||
|
|
||||||
let wrapper = null;
|
let wrapper = null;
|
||||||
let trackingSpy = null;
|
let trackingSpy = null;
|
||||||
|
@ -22,12 +13,8 @@ const noDataSvgPath = 'path/to/no/data';
|
||||||
const emptyStateTitle = 'Too much data';
|
const emptyStateTitle = 'Too much data';
|
||||||
const notEnoughDataError = "We don't have enough data to show this stage.";
|
const notEnoughDataError = "We don't have enough data to show this stage.";
|
||||||
const issueEventItems = issueEvents.events;
|
const issueEventItems = issueEvents.events;
|
||||||
const stagingEventItems = stagingEvents.events;
|
|
||||||
const testEventItems = testEvents.events;
|
|
||||||
const reviewEventItems = reviewEvents.events;
|
const reviewEventItems = reviewEvents.events;
|
||||||
const [firstIssueEvent] = issueEventItems;
|
const [firstIssueEvent] = issueEventItems;
|
||||||
const [firstStagingEvent] = stagingEventItems;
|
|
||||||
const [firstTestEvent] = testEventItems;
|
|
||||||
const [firstReviewEvent] = reviewEventItems;
|
const [firstReviewEvent] = reviewEventItems;
|
||||||
const pagination = { page: 1, hasNextPage: true };
|
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', () => {
|
describe('isLoading = true', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
wrapper = createComponent({ isLoading: true }, true);
|
wrapper = createComponent({ isLoading: true }, true);
|
||||||
|
|
|
@ -5,7 +5,7 @@ import {
|
||||||
mockLabels,
|
mockLabels,
|
||||||
} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
|
} 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 {
|
import {
|
||||||
getRecentlyUsedSuggestions,
|
getRecentlyUsedSuggestions,
|
||||||
setTokenValueToRecentlyUsed,
|
setTokenValueToRecentlyUsed,
|
||||||
|
@ -51,7 +51,7 @@ const mockProps = {
|
||||||
active: false,
|
active: false,
|
||||||
suggestions: [],
|
suggestions: [],
|
||||||
suggestionsLoading: false,
|
suggestionsLoading: false,
|
||||||
defaultSuggestions: DEFAULT_LABELS,
|
defaultSuggestions: DEFAULT_NONE_ANY,
|
||||||
recentSuggestionsStorageKey: mockStorageKey,
|
recentSuggestionsStorageKey: mockStorageKey,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -13,10 +13,7 @@ import {
|
||||||
import createFlash from '~/flash';
|
import createFlash from '~/flash';
|
||||||
import axios from '~/lib/utils/axios_utils';
|
import axios from '~/lib/utils/axios_utils';
|
||||||
|
|
||||||
import {
|
import { DEFAULT_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
|
||||||
DEFAULT_LABELS,
|
|
||||||
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 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';
|
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);
|
expect(wrapper.find(GlDropdownDivider).exists()).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders `DEFAULT_LABELS` as default suggestions', () => {
|
it('renders `DEFAULT_NONE_ANY` as default suggestions', () => {
|
||||||
wrapper = createComponent({
|
wrapper = createComponent({
|
||||||
active: true,
|
active: true,
|
||||||
config: { ...mockLabelToken },
|
config: { ...mockLabelToken },
|
||||||
|
@ -220,8 +217,8 @@ describe('LabelToken', () => {
|
||||||
|
|
||||||
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
|
const suggestions = wrapper.findAll(GlFilteredSearchSuggestion);
|
||||||
|
|
||||||
expect(suggestions).toHaveLength(DEFAULT_LABELS.length);
|
expect(suggestions).toHaveLength(DEFAULT_NONE_ANY.length);
|
||||||
DEFAULT_LABELS.forEach((label, index) => {
|
DEFAULT_NONE_ANY.forEach((label, index) => {
|
||||||
expect(suggestions.at(index).text()).toBe(label.text);
|
expect(suggestions.at(index).text()).toBe(label.text);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -79,56 +79,6 @@ RSpec.describe Gitlab::Analytics::CycleAnalytics::RecordsFetcher do
|
||||||
|
|
||||||
include_context 'when records are loaded by maintainer'
|
include_context 'when records are loaded by maintainer'
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe 'pagination' do
|
describe 'pagination' do
|
||||||
|
|
|
@ -57,6 +57,7 @@ issues:
|
||||||
- issue_email_participants
|
- issue_email_participants
|
||||||
- test_reports
|
- test_reports
|
||||||
- requirement
|
- requirement
|
||||||
|
- incident_management_issuable_escalation_status
|
||||||
work_item_type:
|
work_item_type:
|
||||||
- issues
|
- issues
|
||||||
events:
|
events:
|
||||||
|
|
|
@ -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(:service).is_at_most(100) }
|
||||||
it { is_expected.to validate_length_of(:monitoring_tool).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
|
describe 'fingerprint' do
|
||||||
let_it_be(:fingerprint) { 'fingerprint' }
|
let_it_be(:fingerprint) { 'fingerprint' }
|
||||||
let_it_be(:project3, refind: true) { create(:project) }
|
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) }
|
let_it_be(:existing_alert, refind: true) { create(:alert_management_alert, fingerprint: fingerprint, project: project3) }
|
||||||
|
|
||||||
# We are only validating uniqueness for non-resolved alerts
|
# We are only validating uniqueness for non-resolved alerts
|
||||||
where(:existing_status, :new_status, :valid) do
|
where(:existing_status_event, :new_status, :valid) do
|
||||||
:resolved | :triggered | true
|
:resolve | :triggered | true
|
||||||
:resolved | :acknowledged | true
|
:resolve | :acknowledged | true
|
||||||
:resolved | :ignored | true
|
:resolve | :ignored | true
|
||||||
:resolved | :resolved | true
|
:resolve | :resolved | true
|
||||||
:triggered | :triggered | false
|
:trigger | :triggered | false
|
||||||
:triggered | :acknowledged | false
|
:trigger | :acknowledged | false
|
||||||
:triggered | :ignored | false
|
:trigger | :ignored | false
|
||||||
:triggered | :resolved | true
|
:trigger | :resolved | true
|
||||||
:acknowledged | :triggered | false
|
:acknowledge | :triggered | false
|
||||||
:acknowledged | :acknowledged | false
|
:acknowledge | :acknowledged | false
|
||||||
:acknowledged | :ignored | false
|
:acknowledge | :ignored | false
|
||||||
:acknowledged | :resolved | true
|
:acknowledge | :resolved | true
|
||||||
:ignored | :triggered | false
|
:ignore | :triggered | false
|
||||||
:ignored | :acknowledged | false
|
:ignore | :acknowledged | false
|
||||||
:ignored | :ignored | false
|
:ignore | :ignored | false
|
||||||
:ignored | :resolved | true
|
:ignore | :resolved | true
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
with_them do
|
||||||
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project3) }
|
let(:new_alert) { build(:alert_management_alert, new_status, fingerprint: fingerprint, project: project3) }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
existing_alert.change_status_to(existing_status)
|
existing_alert.update!(status_event: existing_status_event)
|
||||||
end
|
end
|
||||||
|
|
||||||
if params[:valid]
|
if params[:valid]
|
||||||
|
@ -196,20 +132,6 @@ RSpec.describe AlertManagement::Alert do
|
||||||
it { is_expected.to match_array(triggered_alert) }
|
it { is_expected.to match_array(triggered_alert) }
|
||||||
end
|
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
|
describe '.for_fingerprint' do
|
||||||
let(:fingerprint) { SecureRandom.hex }
|
let(:fingerprint) { SecureRandom.hex }
|
||||||
let(:alert_with_fingerprint) { triggered_alert }
|
let(:alert_with_fingerprint) { triggered_alert }
|
||||||
|
@ -302,41 +224,7 @@ RSpec.describe AlertManagement::Alert do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.status_value' do
|
it_behaves_like 'a model including Escalatable'
|
||||||
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 '.counts_by_status' do
|
describe '.counts_by_status' do
|
||||||
subject { described_class.counts_by_status }
|
subject { described_class.counts_by_status }
|
||||||
|
@ -454,87 +342,19 @@ RSpec.describe AlertManagement::Alert do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#open?' do
|
||||||
|
it 'returns true when the status is open status' do
|
||||||
|
expect(triggered_alert.open?).to be true
|
||||||
|
expect(acknowledged_alert.open?).to be true
|
||||||
|
expect(resolved_alert.open?).to be false
|
||||||
|
expect(ignored_alert.open?).to be false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe '#to_reference' do
|
describe '#to_reference' do
|
||||||
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
|
it { expect(triggered_alert.to_reference).to eq("^alert##{triggered_alert.iid}") }
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#trigger' do
|
|
||||||
subject { alert.trigger }
|
|
||||||
|
|
||||||
context 'when alert is in triggered state' do
|
|
||||||
let(:alert) { triggered_alert }
|
|
||||||
|
|
||||||
it 'does not change the alert status' do
|
|
||||||
expect { subject }.not_to change { alert.reload.status }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when alert not in triggered state' do
|
|
||||||
let(:alert) { resolved_alert }
|
|
||||||
|
|
||||||
it 'changes the alert status to triggered' do
|
|
||||||
expect { subject }.to change { alert.triggered? }.to(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets ended at' do
|
|
||||||
expect { subject }.to change { alert.reload.ended_at }.to nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#acknowledge' do
|
|
||||||
subject { alert.acknowledge }
|
|
||||||
|
|
||||||
let(:alert) { resolved_alert }
|
|
||||||
|
|
||||||
it 'changes the alert status to acknowledged' do
|
|
||||||
expect { subject }.to change { alert.acknowledged? }.to(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets ended at' do
|
|
||||||
expect { subject }.to change { alert.reload.ended_at }.to nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#resolve' do
|
|
||||||
let!(:ended_at) { Time.current }
|
|
||||||
|
|
||||||
subject do
|
|
||||||
alert.ended_at = ended_at
|
|
||||||
alert.resolve
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when alert already resolved' do
|
|
||||||
let(:alert) { resolved_alert }
|
|
||||||
|
|
||||||
it 'does not change the alert status' do
|
|
||||||
expect { subject }.not_to change { resolved_alert.reload.status }
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
context 'when alert is not resolved' do
|
|
||||||
let(:alert) { triggered_alert }
|
|
||||||
|
|
||||||
it 'changes alert status to "resolved"' do
|
|
||||||
expect { subject }.to change { alert.resolved? }.to(true)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#ignore' do
|
|
||||||
subject { alert.ignore }
|
|
||||||
|
|
||||||
let(:alert) { resolved_alert }
|
|
||||||
|
|
||||||
it 'changes the alert status to ignored' do
|
|
||||||
expect { subject }.to change { alert.ignored? }.to(true)
|
|
||||||
end
|
|
||||||
|
|
||||||
it 'resets ended at' do
|
|
||||||
expect { subject }.to change { alert.reload.ended_at }.to nil
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#register_new_event!' do
|
describe '#register_new_event!' do
|
||||||
subject { alert.register_new_event! }
|
subject { alert.register_new_event! }
|
||||||
|
|
||||||
|
@ -545,53 +365,20 @@ RSpec.describe AlertManagement::Alert do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '#status_event_for' do
|
describe '#resolved_at' do
|
||||||
using RSpec::Parameterized::TableSyntax
|
subject { resolved_alert.resolved_at }
|
||||||
|
|
||||||
where(:for_status, :event) do
|
it { is_expected.to eq(resolved_alert.ended_at) }
|
||||||
:triggered | :trigger
|
|
||||||
'triggered' | :trigger
|
|
||||||
:acknowledged | :acknowledge
|
|
||||||
'acknowledged' | :acknowledge
|
|
||||||
:resolved | :resolve
|
|
||||||
'resolved' | :resolve
|
|
||||||
:ignored | :ignore
|
|
||||||
'ignored' | :ignore
|
|
||||||
:unknown | nil
|
|
||||||
nil | nil
|
|
||||||
'' | nil
|
|
||||||
1 | nil
|
|
||||||
end
|
end
|
||||||
|
|
||||||
with_them do
|
describe '#resolved_at=' do
|
||||||
let(:alert) { build(:alert_management_alert, project: project) }
|
let(:resolve_time) { Time.current }
|
||||||
|
|
||||||
it 'returns event by status name' do
|
it 'sets ended_at' do
|
||||||
expect(alert.status_event_for(for_status)).to eq(event)
|
triggered_alert.resolved_at = resolve_time
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
describe '#change_status_to' do
|
expect(triggered_alert.ended_at).to eq(resolve_time)
|
||||||
let_it_be_with_reload(:alert) { create(:alert_management_alert, project: project) }
|
expect(triggered_alert.resolved_at).to eq(resolve_time)
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
%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
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
@ -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
|
|
@ -33,6 +33,7 @@ RSpec.describe Issue do
|
||||||
it { is_expected.to have_many(:prometheus_alerts) }
|
it { is_expected.to have_many(:prometheus_alerts) }
|
||||||
it { is_expected.to have_many(:issue_email_participants) }
|
it { is_expected.to have_many(:issue_email_participants) }
|
||||||
it { is_expected.to have_many(:timelogs).autosave(true) }
|
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
|
describe 'versions.most_recent' do
|
||||||
it 'returns the most recent version' do
|
it 'returns the most recent version' do
|
||||||
|
|
|
@ -8,6 +8,9 @@ RSpec.describe 'value stream analytics events' do
|
||||||
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
|
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
|
||||||
|
|
||||||
describe 'GET /:namespace/:project/value_stream_analytics/events/issues' do
|
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
|
before do
|
||||||
project.add_developer(user)
|
project.add_developer(user)
|
||||||
|
|
||||||
|
@ -25,8 +28,6 @@ RSpec.describe 'value stream analytics events' do
|
||||||
it 'lists the issue events' do
|
it 'lists the issue events' do
|
||||||
get project_cycle_analytics_issue_path(project, format: :json)
|
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']).not_to be_empty
|
||||||
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
|
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
|
||||||
end
|
end
|
||||||
|
@ -34,8 +35,6 @@ RSpec.describe 'value stream analytics events' do
|
||||||
it 'lists the plan events' do
|
it 'lists the plan events' do
|
||||||
get project_cycle_analytics_plan_path(project, format: :json)
|
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']).not_to be_empty
|
||||||
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
|
expect(json_response['events'].first['iid']).to eq(first_issue_iid)
|
||||||
end
|
end
|
||||||
|
@ -45,8 +44,6 @@ RSpec.describe 'value stream analytics events' do
|
||||||
|
|
||||||
expect(json_response['events']).not_to be_empty
|
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)
|
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -54,15 +51,15 @@ RSpec.describe 'value stream analytics events' do
|
||||||
get project_cycle_analytics_test_path(project, format: :json)
|
get project_cycle_analytics_test_path(project, format: :json)
|
||||||
|
|
||||||
expect(json_response['events']).not_to be_empty
|
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
|
end
|
||||||
|
|
||||||
it 'lists the review events' do
|
it 'lists the review events' do
|
||||||
get project_cycle_analytics_review_path(project, format: :json)
|
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']).not_to be_empty
|
||||||
|
|
||||||
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
|
expect(json_response['events'].first['iid']).to eq(first_mr_iid)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -70,7 +67,8 @@ RSpec.describe 'value stream analytics events' do
|
||||||
get project_cycle_analytics_staging_path(project, format: :json)
|
get project_cycle_analytics_staging_path(project, format: :json)
|
||||||
|
|
||||||
expect(json_response['events']).not_to be_empty
|
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
|
end
|
||||||
|
|
||||||
context 'with private project and builds' do
|
context 'with private project and builds' do
|
||||||
|
|
|
@ -66,8 +66,8 @@ RSpec.describe DraftNotes::PublishService do
|
||||||
let(:commit_id) { nil }
|
let(:commit_id) { nil }
|
||||||
|
|
||||||
before do
|
before do
|
||||||
create(:draft_note, 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: '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: 'second note', commit_id: commit_id, position: position)
|
||||||
end
|
end
|
||||||
|
|
||||||
context 'when review fails to create' do
|
context 'when review fails to create' do
|
||||||
|
@ -127,6 +127,30 @@ RSpec.describe DraftNotes::PublishService do
|
||||||
publish
|
publish
|
||||||
end
|
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
|
context 'commit_id is set' do
|
||||||
let(:commit_id) { commit.id }
|
let(:commit_id) { commit.id }
|
||||||
|
|
||||||
|
|
|
@ -185,6 +185,14 @@ RSpec.describe Notes::CreateService do
|
||||||
expect(note.note_diff_file).to be_present
|
expect(note.note_diff_file).to be_present
|
||||||
expect(note.diff_note_positions).to be_present
|
expect(note.diff_note_positions).to be_present
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
context 'when DiffNote is a reply' do
|
context 'when DiffNote is a reply' do
|
||||||
|
|
|
@ -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
|
|
@ -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 };
|
|
@ -1,5 +0,0 @@
|
||||||
/* eslint-disable import/no-commonjs */
|
|
||||||
|
|
||||||
const IS_EE = require('../../config/helpers/is_ee_env');
|
|
||||||
|
|
||||||
module.exports = IS_EE ? {} : { ignorePatterns: ['ee/**/*.*'] };
|
|
Loading…
Reference in New Issue