Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2021-10-21 18:10:11 +00:00
parent 2d31f347bf
commit da5d2aa34f
47 changed files with 248 additions and 2105 deletions

View File

@ -106,6 +106,7 @@ RSpec/MultipleMemoizedHelpers:
Naming/FileName:
ExpectMatchingDefinition: true
CheckDefinitionPathHierarchy: false
Exclude:
- '**/*/*.builder'
- 'ee/bin/*'

View File

@ -17,7 +17,6 @@ import { convertToSnakeCase } from '~/lib/utils/text_utility';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import {
tdClass,
thClass,
@ -26,7 +25,6 @@ import {
} from '~/vue_shared/components/paginated_table_with_search_and_tabs/constants';
import PaginatedTableWithSearchAndTabs from '~/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { ALERTS_STATUS_TABS, SEVERITY_LEVELS, trackAlertListViewsOptions } from '../constants';
import getAlertsCountByStatus from '../graphql/queries/get_count_by_status.query.graphql';
@ -98,7 +96,6 @@ export default {
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
AlertsDeprecationWarning,
GlAlert,
GlLoadingIcon,
GlTable,
@ -115,7 +112,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'textQuery', 'assigneeUsernameQuery', 'populatingAlertsHelpUrl'],
apollo: {
alerts: {
@ -277,8 +273,6 @@ export default {
</gl-sprintf>
</gl-alert>
<alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
<paginated-table-with-search-and-tabs
:show-error-msg="showErrorMsg"
:i18n="$options.i18n"

View File

@ -23,7 +23,6 @@ export default () => {
assigneeUsernameQuery,
alertManagementEnabled,
userCanEnableAlertManagement,
hasManagedPrometheus,
} = domEl.dataset;
const apolloProvider = new VueApollo({
@ -66,7 +65,6 @@ export default () => {
alertManagementEnabled: parseBoolean(alertManagementEnabled),
trackAlertStatusUpdateOptions: PAGE_CONFIG.OPERATIONS.TRACK_ALERT_STATUS_UPDATE_OPTIONS,
userCanEnableAlertManagement: parseBoolean(userCanEnableAlertManagement),
hasManagedPrometheus: parseBoolean(hasManagedPrometheus),
},
apolloProvider,
render(createElement) {

View File

@ -1,285 +0,0 @@
<script>
import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import { values, get } from 'lodash';
import createFlash from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { OPERATORS } from '../constants';
import AlertsService from '../services/alerts_service';
import { alertsValidator, queriesValidator } from '../validators';
import AlertWidgetForm from './alert_widget_form.vue';
export default {
components: {
AlertWidgetForm,
GlBadge,
GlLoadingIcon,
GlIcon,
GlTooltip,
GlSprintf,
},
directives: {
GlModal: GlModalDirective,
},
props: {
alertsEndpoint: {
type: String,
required: true,
},
showLoadingState: {
type: Boolean,
required: false,
default: true,
},
// { [alertPath]: { alert_attributes } }. Populated from subsequent API calls.
// Includes only the metrics/alerts to be managed by this widget.
alertsToManage: {
type: Object,
required: false,
default: () => ({}),
validator: alertsValidator,
},
// [{ metric+query_attributes }]. Represents queries (and alerts) we know about
// on intial fetch. Essentially used for reference.
relevantQueries: {
type: Array,
required: true,
validator: queriesValidator,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
service: null,
errorMessage: null,
isLoading: false,
apiAction: 'create',
};
},
i18n: {
alertsCountMsg: s__('PrometheusAlerts|%{count} alerts applied'),
singleFiringMsg: s__('PrometheusAlerts|Firing: %{alert}'),
multipleFiringMsg: s__('PrometheusAlerts|%{firingCount} firing'),
firingAlertsTooltip: s__('PrometheusAlerts|Firing: %{alerts}'),
},
computed: {
singleAlertSummary() {
return {
message: this.isFiring ? this.$options.i18n.singleFiringMsg : this.thresholds[0],
alert: this.thresholds[0],
};
},
multipleAlertsSummary() {
return {
message: this.isFiring
? `${this.$options.i18n.alertsCountMsg}, ${this.$options.i18n.multipleFiringMsg}`
: this.$options.i18n.alertsCountMsg,
count: this.thresholds.length,
firingCount: this.firingAlerts.length,
};
},
shouldShowLoadingIcon() {
return this.showLoadingState && this.isLoading;
},
thresholds() {
const alertsToManage = Object.keys(this.alertsToManage);
return alertsToManage.map(this.formatAlertSummary);
},
hasAlerts() {
return Boolean(Object.keys(this.alertsToManage).length);
},
hasMultipleAlerts() {
return this.thresholds.length > 1;
},
isFiring() {
return Boolean(this.firingAlerts.length);
},
firingAlerts() {
return values(this.alertsToManage).filter((alert) =>
this.passedAlertThreshold(this.getQueryData(alert), alert),
);
},
formattedFiringAlerts() {
return this.firingAlerts.map((alert) => this.formatAlertSummary(alert.alert_path));
},
configuredAlert() {
return this.hasAlerts ? values(this.alertsToManage)[0].metricId : '';
},
},
created() {
this.service = new AlertsService({ alertsEndpoint: this.alertsEndpoint });
this.fetchAlertData();
},
methods: {
fetchAlertData() {
this.isLoading = true;
const queriesWithAlerts = this.relevantQueries.filter((query) => query.alert_path);
return Promise.all(
queriesWithAlerts.map((query) =>
this.service
.readAlert(query.alert_path)
.then((alertAttributes) => this.setAlert(alertAttributes, query.metricId)),
),
)
.then(() => {
this.isLoading = false;
})
.catch(() => {
createFlash({
message: s__('PrometheusAlerts|Error fetching alert'),
});
this.isLoading = false;
});
},
setAlert(alertAttributes, metricId) {
this.$emit('setAlerts', alertAttributes.alert_path, { ...alertAttributes, metricId });
},
removeAlert(alertPath) {
this.$emit('setAlerts', alertPath, null);
},
formatAlertSummary(alertPath) {
const alert = this.alertsToManage[alertPath];
const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
return `${alertQuery.label} ${alert.operator} ${alert.threshold}`;
},
passedAlertThreshold(data, alert) {
const { threshold, operator } = alert;
switch (operator) {
case OPERATORS.greaterThan:
return data.some((value) => value > threshold);
case OPERATORS.lessThan:
return data.some((value) => value < threshold);
case OPERATORS.equalTo:
return data.some((value) => value === threshold);
default:
return false;
}
},
getQueryData(alert) {
const alertQuery = this.relevantQueries.find((query) => query.metricId === alert.metricId);
return get(alertQuery, 'result[0].values', []).map((value) => get(value, '[1]', null));
},
showModal() {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
hideModal() {
this.errorMessage = null;
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
handleSetApiAction(apiAction) {
this.apiAction = apiAction;
},
handleCreate({ operator, threshold, prometheus_metric_id, runbookUrl }) {
const newAlert = { operator, threshold, prometheus_metric_id, runbookUrl };
this.isLoading = true;
this.service
.createAlert(newAlert)
.then((alertAttributes) => {
this.setAlert(alertAttributes, prometheus_metric_id);
this.isLoading = false;
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error creating alert');
this.isLoading = false;
});
},
handleUpdate({ alert, operator, threshold, runbookUrl }) {
const updatedAlert = { operator, threshold, runbookUrl };
this.isLoading = true;
this.service
.updateAlert(alert, updatedAlert)
.then((alertAttributes) => {
this.setAlert(alertAttributes, this.alertsToManage[alert].metricId);
this.isLoading = false;
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error saving alert');
this.isLoading = false;
});
},
handleDelete({ alert }) {
this.isLoading = true;
this.service
.deleteAlert(alert)
.then(() => {
this.removeAlert(alert);
this.isLoading = false;
this.hideModal();
})
.catch(() => {
this.errorMessage = s__('PrometheusAlerts|Error deleting alert');
this.isLoading = false;
});
},
},
};
</script>
<template>
<div class="prometheus-alert-widget dropdown flex-grow-2 overflow-hidden">
<gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" size="sm" />
<span v-else-if="errorMessage" ref="alertErrorMessage" class="alert-error-message">{{
errorMessage
}}</span>
<span
v-else-if="hasAlerts"
ref="alertCurrentSetting"
class="alert-current-setting cursor-pointer d-flex"
@click="showModal"
>
<gl-badge :variant="isFiring ? 'danger' : 'neutral'" class="d-flex-center text-truncate">
<gl-icon name="warning" :size="16" class="flex-shrink-0" />
<span class="text-truncate gl-pl-2">
<gl-sprintf
:message="
hasMultipleAlerts ? multipleAlertsSummary.message : singleAlertSummary.message
"
>
<template #alert>
{{ singleAlertSummary.alert }}
</template>
<template #count>
{{ multipleAlertsSummary.count }}
</template>
<template #firingCount>
{{ multipleAlertsSummary.firingCount }}
</template>
</gl-sprintf>
</span>
</gl-badge>
<gl-tooltip v-if="hasMultipleAlerts && isFiring" :target="() => $refs.alertCurrentSetting">
<gl-sprintf :message="$options.i18n.firingAlertsTooltip">
<template #alerts>
<div v-for="alert in formattedFiringAlerts" :key="alert.alert_path">
{{ alert }}
</div>
</template>
</gl-sprintf>
</gl-tooltip>
</span>
<alert-widget-form
ref="widgetForm"
:disabled="isLoading"
:alerts-to-manage="alertsToManage"
:relevant-queries="relevantQueries"
:error-message="errorMessage"
:configured-alert="configuredAlert"
:modal-id="modalId"
@create="handleCreate"
@update="handleUpdate"
@delete="handleDelete"
@cancel="hideModal"
@setAction="handleSetApiAction"
/>
</div>
</template>

View File

@ -1,324 +0,0 @@
<script>
import {
GlLink,
GlButton,
GlButtonGroup,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlModal,
GlTooltipDirective,
GlIcon,
} from '@gitlab/ui';
import { isEmpty, findKey } from 'lodash';
import Vue from 'vue';
import { __, s__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Translate from '~/vue_shared/translate';
import { OPERATORS } from '../constants';
import { alertsValidator, queriesValidator } from '../validators';
Vue.use(Translate);
const SUBMIT_ACTION_TEXT = {
create: __('Add'),
update: __('Save'),
delete: __('Delete'),
};
const SUBMIT_BUTTON_CLASS = {
create: 'btn-success',
update: 'btn-success',
delete: 'btn-danger',
};
export default {
components: {
GlButton,
GlButtonGroup,
GlFormGroup,
GlFormInput,
GlDropdown,
GlDropdownItem,
GlModal,
GlLink,
GlIcon,
},
directives: {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
disabled: {
type: Boolean,
required: true,
},
errorMessage: {
type: String,
required: false,
default: '',
},
configuredAlert: {
type: String,
required: false,
default: '',
},
alertsToManage: {
type: Object,
required: false,
default: () => ({}),
validator: alertsValidator,
},
relevantQueries: {
type: Array,
required: true,
validator: queriesValidator,
},
modalId: {
type: String,
required: true,
},
},
data() {
return {
operators: OPERATORS,
operator: null,
threshold: null,
prometheusMetricId: null,
runbookUrl: null,
selectedAlert: {},
alertQuery: '',
};
},
computed: {
isValidQuery() {
// TODO: Add query validation check (most likely via http request)
return this.alertQuery.length ? true : null;
},
currentQuery() {
return this.relevantQueries.find((query) => query.metricId === this.prometheusMetricId) || {};
},
formDisabled() {
// We need a prometheusMetricId to determine whether we're
// creating/updating/deleting
return this.disabled || !(this.prometheusMetricId || this.isValidQuery);
},
supportsComputedAlerts() {
return this.glFeatures.prometheusComputedAlerts;
},
queryDropdownLabel() {
return this.currentQuery.label || s__('PrometheusAlerts|Select query');
},
haveValuesChanged() {
return (
this.operator &&
this.threshold === Number(this.threshold) &&
(this.operator !== this.selectedAlert.operator ||
this.threshold !== this.selectedAlert.threshold ||
this.runbookUrl !== this.selectedAlert.runbookUrl)
);
},
submitAction() {
if (isEmpty(this.selectedAlert)) return 'create';
if (this.haveValuesChanged) return 'update';
return 'delete';
},
submitActionText() {
return SUBMIT_ACTION_TEXT[this.submitAction];
},
submitButtonClass() {
return SUBMIT_BUTTON_CLASS[this.submitAction];
},
isSubmitDisabled() {
return this.disabled || (this.submitAction === 'create' && !this.haveValuesChanged);
},
dropdownTitle() {
return this.submitAction === 'create'
? s__('PrometheusAlerts|Add alert')
: s__('PrometheusAlerts|Edit alert');
},
},
watch: {
alertsToManage() {
this.resetAlertData();
},
submitAction() {
this.$emit('setAction', this.submitAction);
},
},
methods: {
selectQuery(queryId) {
const existingAlertPath = findKey(this.alertsToManage, (alert) => alert.metricId === queryId);
const existingAlert = this.alertsToManage[existingAlertPath];
if (existingAlert) {
const { operator, threshold, runbookUrl } = existingAlert;
this.selectedAlert = existingAlert;
this.operator = operator;
this.threshold = threshold;
this.runbookUrl = runbookUrl;
} else {
this.selectedAlert = {};
this.operator = this.operators.greaterThan;
this.threshold = null;
this.runbookUrl = null;
}
this.prometheusMetricId = queryId;
},
handleHidden() {
this.resetAlertData();
this.$emit('cancel');
},
handleSubmit() {
this.$emit(this.submitAction, {
alert: this.selectedAlert.alert_path,
operator: this.operator,
threshold: this.threshold,
prometheus_metric_id: this.prometheusMetricId,
runbookUrl: this.runbookUrl,
});
},
handleShown() {
if (this.configuredAlert) {
this.selectQuery(this.configuredAlert);
} else if (this.relevantQueries.length === 1) {
this.selectQuery(this.relevantQueries[0].metricId);
}
},
resetAlertData() {
this.operator = null;
this.threshold = null;
this.prometheusMetricId = null;
this.selectedAlert = {};
this.runbookUrl = null;
},
getAlertFormActionTrackingOption() {
const label = `${this.submitAction}_alert`;
return {
category: document.body.dataset.page,
action: 'click_button',
label,
};
},
},
alertQueryText: {
label: __('Query'),
validFeedback: __('Query is valid'),
invalidFeedback: __('Invalid query'),
descriptionTooltip: __(
'Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula.',
),
},
};
</script>
<template>
<gl-modal
ref="alertModal"
:title="dropdownTitle"
:modal-id="modalId"
:ok-variant="submitAction === 'delete' ? 'danger' : 'success'"
:ok-disabled="formDisabled"
@ok.prevent="handleSubmit"
@hidden="handleHidden"
@shown="handleShown"
>
<div v-if="errorMessage" class="alert-modal-message danger_message">{{ errorMessage }}</div>
<div class="alert-form">
<gl-form-group
v-if="supportsComputedAlerts"
:label="$options.alertQueryText.label"
label-for="alert-query-input"
:valid-feedback="$options.alertQueryText.validFeedback"
:invalid-feedback="$options.alertQueryText.invalidFeedback"
:state="isValidQuery"
>
<gl-form-input id="alert-query-input" v-model.trim="alertQuery" :state="isValidQuery" />
<template #description>
<div class="d-flex align-items-center">
{{ __('Single or combined queries') }}
<gl-icon
v-gl-tooltip="$options.alertQueryText.descriptionTooltip"
name="question"
class="gl-ml-2"
/>
</div>
</template>
</gl-form-group>
<gl-form-group v-else label-for="alert-query-dropdown" :label="$options.alertQueryText.label">
<gl-dropdown
id="alert-query-dropdown"
:text="queryDropdownLabel"
toggle-class="dropdown-menu-toggle gl-border-1! qa-alert-query-dropdown"
>
<gl-dropdown-item
v-for="query in relevantQueries"
:key="query.metricId"
data-qa-selector="alert_query_option"
@click="selectQuery(query.metricId)"
>
{{ query.label }}
</gl-dropdown-item>
</gl-dropdown>
</gl-form-group>
<gl-button-group class="mb-3" :label="s__('PrometheusAlerts|Operator')">
<gl-button
:class="{ active: operator === operators.greaterThan }"
:disabled="formDisabled"
@click="operator = operators.greaterThan"
>
{{ operators.greaterThan }}
</gl-button>
<gl-button
:class="{ active: operator === operators.equalTo }"
:disabled="formDisabled"
@click="operator = operators.equalTo"
>
{{ operators.equalTo }}
</gl-button>
<gl-button
:class="{ active: operator === operators.lessThan }"
:disabled="formDisabled"
@click="operator = operators.lessThan"
>
{{ operators.lessThan }}
</gl-button>
</gl-button-group>
<gl-form-group :label="s__('PrometheusAlerts|Threshold')" label-for="alerts-threshold">
<gl-form-input
id="alerts-threshold"
v-model.number="threshold"
:disabled="formDisabled"
type="number"
data-qa-selector="alert_threshold_field"
/>
</gl-form-group>
<gl-form-group
:label="s__('PrometheusAlerts|Runbook URL (optional)')"
label-for="alert-runbook"
>
<gl-form-input
id="alert-runbook"
v-model="runbookUrl"
:disabled="formDisabled"
data-testid="alertRunbookField"
type="text"
:placeholder="s__('PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks')"
/>
</gl-form-group>
</div>
<template #modal-ok>
<gl-link
v-track-event="getAlertFormActionTrackingOption()"
class="text-reset text-decoration-none"
>
{{ submitActionText }}
</gl-link>
</template>
</gl-modal>
</template>

View File

@ -73,11 +73,6 @@ export default {
required: false,
default: chartHeight,
},
thresholds: {
type: Array,
required: false,
default: () => [],
},
legendLayout: {
type: String,
required: false,
@ -391,7 +386,6 @@ export default {
:option="chartOptions"
:format-tooltip-text="formatTooltipText"
:format-annotations-tooltip-text="formatAnnotationsTooltipText"
:thresholds="thresholds"
:width="width"
:height="height"
:legend-layout="legendLayout"

View File

@ -8,10 +8,8 @@ import invalidUrl from '~/lib/utils/invalid_url';
import { ESC_KEY } from '~/lib/utils/keys';
import { mergeUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import AlertsDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
import { defaultTimeRange } from '~/vue_shared/constants';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { metricStates, keyboardShortcutKeys } from '../constants';
import {
timeRangeFromUrl,
@ -30,7 +28,6 @@ import VariablesSection from './variables_section.vue';
export default {
components: {
AlertsDeprecationWarning,
VueDraggable,
DashboardHeader,
DashboardPanel,
@ -47,7 +44,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
hasMetrics: {
type: Boolean,
@ -399,8 +395,6 @@ export default {
<template>
<div class="prometheus-graphs" data-qa-selector="prometheus_graphs">
<alerts-deprecation-warning v-if="!glFeatures.managedAlertsDeprecation" />
<dashboard-header
v-if="showHeader"
ref="prometheusGraphsHeader"

View File

@ -13,20 +13,16 @@ import {
GlTooltip,
GlTooltipDirective,
} from '@gitlab/ui';
import { mapValues, pickBy } from 'lodash';
import { mapState } from 'vuex';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import invalidUrl from '~/lib/utils/invalid_url';
import { relativePathToAbsolute, getBaseURL, visitUrl, isSafeURL } from '~/lib/utils/url_utility';
import { __, n__ } from '~/locale';
import TrackEventDirective from '~/vue_shared/directives/track_event';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { panelTypes } from '../constants';
import { graphDataToCsv } from '../csv_export';
import { timeRangeToUrl, downloadCSVOptions, generateLinkToChartOptions } from '../utils';
import AlertWidget from './alert_widget.vue';
import MonitorAnomalyChart from './charts/anomaly.vue';
import MonitorBarChart from './charts/bar.vue';
import MonitorColumnChart from './charts/column.vue';
@ -45,7 +41,6 @@ const events = {
export default {
components: {
MonitorEmptyChart,
AlertWidget,
GlIcon,
GlLink,
GlLoadingIcon,
@ -62,7 +57,6 @@ export default {
GlTooltip: GlTooltipDirective,
TrackEvent: TrackEventDirective,
},
mixins: [glFeatureFlagMixin()],
props: {
clipboardText: {
type: String,
@ -84,16 +78,6 @@ export default {
required: false,
default: 'monitoringDashboard',
},
alertsEndpoint: {
type: String,
required: false,
default: null,
},
prometheusAlertsAvailable: {
type: Boolean,
required: false,
default: false,
},
settingsPath: {
type: String,
required: false,
@ -104,7 +88,6 @@ export default {
return {
showTitleTooltip: false,
zoomedTimeRange: null,
allAlerts: {},
expandBtnAvailable: Boolean(this.$listeners[events.expand]),
};
},
@ -211,7 +194,7 @@ export default {
/**
* In monitoring, Time Series charts typically support
* a larger feature set like "annotations", "deployment
* data", alert "thresholds" and "datazoom".
* data" and "datazoom".
*
* This is intentional as Time Series are more frequently
* used.
@ -252,34 +235,11 @@ export default {
const { metrics = [] } = this.graphData;
return metrics.some(({ metricId }) => this.metricsSavedToDb.includes(metricId));
},
alertWidgetAvailable() {
const supportsAlerts =
this.isPanelType(panelTypes.AREA_CHART) || this.isPanelType(panelTypes.LINE_CHART);
return (
supportsAlerts &&
this.prometheusAlertsAvailable &&
this.alertsEndpoint &&
this.graphData &&
this.hasMetricsInDb &&
!this.glFeatures.managedAlertsDeprecation
);
},
alertModalId() {
return `alert-modal-${this.graphData.id}`;
},
},
mounted() {
this.refreshTitleTooltip();
},
methods: {
getGraphAlerts(queries) {
if (!this.allAlerts) return {};
const metricIdsForChart = queries.map((q) => q.metricId);
return pickBy(this.allAlerts, (alert) => metricIdsForChart.includes(alert.metricId));
},
getGraphAlertValues(queries) {
return Object.values(this.getGraphAlerts(queries));
},
isPanelType(type) {
return this.graphData?.type === type;
},
@ -310,24 +270,9 @@ export default {
this.onExpand();
}
},
setAlerts(alertPath, alertAttributes) {
if (alertAttributes) {
this.$set(this.allAlerts, alertPath, alertAttributes);
} else {
this.$delete(this.allAlerts, alertPath);
}
},
safeUrl(url) {
return isSafeURL(url) ? url : '#';
},
showAlertModal() {
this.$root.$emit(BV_SHOW_MODAL, this.alertModalId);
},
showAlertModalFromKeyboardShortcut() {
if (this.isContextualMenuShown) {
this.showAlertModal();
}
},
visitLogsPage() {
if (this.logsPathWithTimeRange) {
visitUrl(relativePathToAbsolute(this.logsPathWithTimeRange, getBaseURL()));
@ -348,19 +293,6 @@ export default {
this.$refs.copyChartLink.$el.firstChild.click();
}
},
getAlertRunbooks(queries) {
const hasRunbook = (alert) => Boolean(alert.runbookUrl);
const graphAlertsWithRunbooks = pickBy(this.getGraphAlerts(queries), hasRunbook);
const alertToRunbookTransform = (alert) => {
const alertQuery = queries.find((query) => query.metricId === alert.metricId);
return {
key: alert.metricId,
href: alert.runbookUrl,
label: alertQuery.label,
};
};
return mapValues(graphAlertsWithRunbooks, alertToRunbookTransform);
},
},
panelTypes,
};
@ -378,15 +310,6 @@ export default {
<gl-tooltip :target="() => $refs.graphTitle" :disabled="!showTitleTooltip">
{{ title }}
</gl-tooltip>
<alert-widget
v-if="isContextualMenuShown && alertWidgetAvailable"
class="mx-1"
:modal-id="alertModalId"
:alerts-endpoint="alertsEndpoint"
:relevant-queries="graphData.metrics"
:alerts-to-manage="getGraphAlerts(graphData.metrics)"
@setAlerts="setAlerts"
/>
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon size="sm" />
@ -450,32 +373,6 @@ export default {
>
{{ __('Copy link to chart') }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="alertWidgetAvailable"
v-gl-modal="alertModalId"
data-qa-selector="alert_widget_menu_item"
>
{{ __('Alerts') }}
</gl-dropdown-item>
<gl-dropdown-item
v-for="runbook in getAlertRunbooks(graphData.metrics)"
:key="runbook.key"
:href="safeUrl(runbook.href)"
data-testid="runbookLink"
target="_blank"
rel="noopener noreferrer"
>
<span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
<span>
<gl-sprintf :message="s__('Metrics|View runbook - %{label}')">
<template #label>
{{ runbook.label }}
</template>
</gl-sprintf>
</span>
<gl-icon name="external-link" />
</span>
</gl-dropdown-item>
<template v-if="graphData.links && graphData.links.length">
<gl-dropdown-divider />
@ -515,7 +412,6 @@ export default {
:deployment-data="deploymentData"
:annotations="annotations"
:project-path="projectPath"
:thresholds="getGraphAlertValues(graphData.metrics)"
:group-id="groupId"
:timezone="dashboardTimezone"
:time-range="fixedCurrentTimeRange"

View File

@ -12,10 +12,7 @@ export default (props = {}) => {
if (el && el.dataset) {
const { metricsDashboardBasePath, ...dataset } = el.dataset;
const {
initState,
dataProps: { hasManagedPrometheus, ...dataProps },
} = stateAndPropsFromDataset(dataset);
const { initState, dataProps } = stateAndPropsFromDataset(dataset);
const store = createStore(initState);
const router = createRouter(metricsDashboardBasePath);
@ -24,7 +21,6 @@ export default (props = {}) => {
el,
store,
router,
provide: { hasManagedPrometheus },
data() {
return {
dashboardProps: { ...dataProps, ...props },

View File

@ -41,7 +41,6 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
dataProps.hasManagedPrometheus = parseBoolean(dataProps.hasManagedPrometheus);
return {
initState: {

View File

@ -382,7 +382,11 @@ export default {
:data-testid="`radio-${value}`"
>
<div>
<gl-icon :name="icon" />
<gl-icon
data-qa-selector="fork_privacy_button"
:name="icon"
:data-qa-privacy-level="`${value}`"
/>
<span>{{ text }}</span>
</div>
<template #help>{{ help }}</template>

View File

@ -1,46 +0,0 @@
<script>
import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { s__ } from '~/locale';
export default {
components: {
GlAlert,
GlLink,
GlSprintf,
},
inject: ['hasManagedPrometheus'],
i18n: {
alertsDeprecationText: s__(
'Metrics|GitLab-managed Prometheus is deprecated and %{linkStart}scheduled for removal%{linkEnd}. Following this removal, your existing alerts will continue to function as part of the new cluster integration. However, you will no longer be able to add new alerts or edit existing alerts from the metrics dashboard.',
),
},
methods: {
helpPagePath,
},
};
</script>
<template>
<gl-alert
v-if="hasManagedPrometheus"
variant="warning"
class="my-2"
data-testid="alerts-deprecation-warning"
>
<gl-sprintf :message="$options.i18n.alertsDeprecationText">
<template #link="{ content }">
<gl-link
:href="
helpPagePath('operations/metrics/alerts.html', {
anchor: 'managed-prometheus-instances',
})
"
target="_blank"
>
<span>{{ content }}</span>
</gl-link>
</template>
</gl-sprintf>
</gl-alert>
</template>

View File

@ -3,10 +3,6 @@
class Projects::AlertManagementController < Projects::ApplicationController
before_action :authorize_read_alert_management_alert!
before_action(only: [:index]) do
push_frontend_feature_flag(:managed_alerts_deprecation, @project, default_enabled: :yaml)
end
feature_category :incident_management
def index

View File

@ -12,7 +12,6 @@ module Projects
before_action do
push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
push_frontend_feature_flag(:managed_alerts_deprecation, @project, default_enabled: :yaml)
end
feature_category :metrics

View File

@ -69,9 +69,7 @@ module EnvironmentsHelper
'custom_metrics_path' => project_prometheus_metrics_path(project),
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
'custom_metrics_available' => "#{custom_metrics_available?(project)}",
'prometheus_alerts_available' => "#{can?(current_user, :read_prometheus_alerts, project)}",
'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase,
'has_managed_prometheus' => has_managed_prometheus?(project).to_s
'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
}
end
@ -86,10 +84,6 @@ module EnvironmentsHelper
}
end
def has_managed_prometheus?(project)
project.prometheus_integration&.prometheus_available? == true
end
def metrics_dashboard_base_path(environment, project)
# This is needed to support our transition from environment scoped metric paths to project scoped.
if project

View File

@ -10,7 +10,6 @@ module Projects::AlertManagementHelper
'empty-alert-svg-path' => image_path('illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => can?(current_user, :admin_operations, project).to_s,
'alert-management-enabled' => alert_management_enabled?(project).to_s,
'has-managed-prometheus' => has_managed_prometheus?(project).to_s,
'text-query': params[:search],
'assignee-username-query': params[:assignee_username]
}
@ -28,10 +27,6 @@ module Projects::AlertManagementHelper
private
def has_managed_prometheus?(project)
project.prometheus_integration&.prometheus_available? == true
end
def alert_management_enabled?(project)
!!(
project.alert_management_alerts.any? ||

View File

@ -1,8 +0,0 @@
---
name: managed_alerts_deprecation
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62528
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331863
milestone: '14.0'
type: development
group: group::monitor
default_enabled: true

View File

@ -114,3 +114,5 @@ Sidekiq.configure_client do |config|
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
end
Sidekiq::Client.prepend Gitlab::Patch::SidekiqClient

View File

@ -1598,7 +1598,7 @@ Input type: `DastProfileRunInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdastprofilerunclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastprofilerunfullpath"></a>`fullPath` | [`ID!`](#id) | Full path for the project the scanner profile belongs to. |
| <a id="mutationdastprofilerunfullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastprofilerunid"></a>`id` | [`DastProfileID!`](#dastprofileid) | ID of the profile to be used for the scan. |
#### Fields
@ -1623,7 +1623,7 @@ Input type: `DastProfileUpdateInput`
| <a id="mutationdastprofileupdatedastscannerprofileid"></a>`dastScannerProfileId` | [`DastScannerProfileID`](#dastscannerprofileid) | ID of the scanner profile to be associated. |
| <a id="mutationdastprofileupdatedastsiteprofileid"></a>`dastSiteProfileId` | [`DastSiteProfileID`](#dastsiteprofileid) | ID of the site profile to be associated. |
| <a id="mutationdastprofileupdatedescription"></a>`description` | [`String`](#string) | Description of the profile. Defaults to an empty string. |
| <a id="mutationdastprofileupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Project the profile belongs to. |
| <a id="mutationdastprofileupdatefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastprofileupdateid"></a>`id` | [`DastProfileID!`](#dastprofileid) | ID of the profile to be deleted. |
| <a id="mutationdastprofileupdatename"></a>`name` | [`String`](#string) | Name of the profile. |
| <a id="mutationdastprofileupdaterunafterupdate"></a>`runAfterUpdate` | [`Boolean`](#boolean) | Run scan using profile after update. Defaults to false. |
@ -1671,7 +1671,7 @@ Input type: `DastScannerProfileDeleteInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdastscannerprofiledeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastscannerprofiledeletefullpath"></a>`fullPath` | [`ID!`](#id) | Full path for the project the scanner profile belongs to. |
| <a id="mutationdastscannerprofiledeletefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastscannerprofiledeleteid"></a>`id` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be deleted. |
#### Fields
@ -1690,7 +1690,7 @@ Input type: `DastScannerProfileUpdateInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdastscannerprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastscannerprofileupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Project the scanner profile belongs to. |
| <a id="mutationdastscannerprofileupdatefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastscannerprofileupdateid"></a>`id` | [`DastScannerProfileID!`](#dastscannerprofileid) | ID of the scanner profile to be updated. |
| <a id="mutationdastscannerprofileupdateprofilename"></a>`profileName` | [`String!`](#string) | Name of the scanner profile. |
| <a id="mutationdastscannerprofileupdatescantype"></a>`scanType` | [`DastScanTypeEnum`](#dastscantypeenum) | Indicates the type of DAST scan that will run. Either a Passive Scan or an Active Scan. |
@ -1741,7 +1741,7 @@ Input type: `DastSiteProfileDeleteInput`
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationdastsiteprofiledeleteclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastsiteprofiledeletefullpath"></a>`fullPath` | [`ID!`](#id) | Project the site profile belongs to. |
| <a id="mutationdastsiteprofiledeletefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastsiteprofiledeleteid"></a>`id` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be deleted. |
#### Fields
@ -1762,7 +1762,7 @@ Input type: `DastSiteProfileUpdateInput`
| <a id="mutationdastsiteprofileupdateauth"></a>`auth` | [`DastSiteProfileAuthInput`](#dastsiteprofileauthinput) | Parameters for authentication. |
| <a id="mutationdastsiteprofileupdateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationdastsiteprofileupdateexcludedurls"></a>`excludedUrls` | [`[String!]`](#string) | URLs to skip during an authenticated scan. |
| <a id="mutationdastsiteprofileupdatefullpath"></a>`fullPath` | [`ID!`](#id) | Project the site profile belongs to. |
| <a id="mutationdastsiteprofileupdatefullpath"></a>`fullPath` **{warning-solid}** | [`ID`](#id) | **Deprecated:** Full path not required to qualify Global ID. Deprecated in 14.5. |
| <a id="mutationdastsiteprofileupdateid"></a>`id` | [`DastSiteProfileID!`](#dastsiteprofileid) | ID of the site profile to be updated. |
| <a id="mutationdastsiteprofileupdateprofilename"></a>`profileName` | [`String!`](#string) | Name of the site profile. |
| <a id="mutationdastsiteprofileupdaterequestheaders"></a>`requestHeaders` | [`String`](#string) | Comma-separated list of request header names and values to be added to every request made by DAST. |

View File

@ -16,7 +16,7 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/workflow.html'
1. Create branch with your feature:
```shell
git checkout -b $feature_name
git checkout -b feature_name
```
1. Write code. Commit changes:
@ -28,7 +28,7 @@ disqus_identifier: 'https://docs.gitlab.com/ee/workflow/workflow.html'
1. Push your branch to GitLab:
```shell
git push origin $feature_name
git push origin feature_name
```
1. Review your code on commits page.

View File

@ -169,7 +169,7 @@ The following table lists project permissions available for each role:
| [Repository](project/repository/index.md):<br>Enable or disable branch protection | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Enable or disable tag protection | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Manage [push rules](../push_rules/push_rules.md) | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Push to protected branches | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Push to protected branches (*5*) | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Turn on or off protected branch push for developers | | | | ✓ | ✓ |
| [Repository](project/repository/index.md):<br>Remove fork relationship | | | | | ✓ |
| [Repository](project/repository/index.md):<br>Force push to protected branches (*4*) | | | | | |

View File

@ -9,31 +9,45 @@ info: To determine the technical writer assigned to the Stage/Group associated w
You can configure a [webhook](webhooks.md) in your project that triggers when
an event occurs. The following events are supported.
Event type | Trigger
---------------------------------------------|-----------------------------------------------------------------------------
[Push event](#push-events) | A push is made to the repository.
[Tag event](#tag-events) | Tags are created or deleted in the repository.
[Issue event](#issue-events) | A new issue is created or an existing issue is updated, closed, or reopened.
[Comment event](#comment-events) | A new comment is made on commits, merge requests, issues, and code snippets.
[Merge request event](#merge-request-events) | A merge request is created, updated, merged, or closed, or a commit is added in the source branch.
[Wiki page event](#wiki-page-events) | A wiki page is created, updated, or deleted.
[Pipeline event](#pipeline-events) | A pipeline status changes.
[Job event](#job-events) | A job status changes.
[Deployment event](#deployment-events) | A deployment starts, succeeds, fails, or is canceled.
[Group member event](#group-member-events) | A user is added or removed from a group, or a user's access level or access expiration date changes.
[Subgroup event](#subgroup-events) | A subgroup is created or removed from a group.
[Feature flag event](#feature-flag-events) | A feature flag is turned on or off.
[Release event](#release-events) | A release is created or updated.
## Push events
Triggered when you push to the repository except when pushing tags.
Push events are triggered when you push to the repository, except when:
NOTE:
When more than 20 commits are pushed at once, the `commits` webhook
attribute only contains the newest 20 for performance reasons. Loading
detailed commit data is expensive. Note that despite only 20 commits being
present in the `commits` attribute, the `total_commits_count` attribute contains the actual total.
- You push tags.
- A single push includes changes for more than three branches by default
(depending on the [`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)).
NOTE:
If a branch creation push event is generated without new commits being introduced, the
If you push more than 20 commits at once, the `commits`
attribute in the payload contains information about the newest 20 commits only.
Loading detailed commit data is expensive, so this restriction exists for performance reasons.
The `total_commits_count` attribute contains the actual number of commits.
If you create and push a branch without any new commits, the
`commits` attribute in the payload is empty.
Also, if a single push includes changes for more than three (by default, depending on
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls))
branches, this hook isn't executed.
**Request header**:
Request header:
```plaintext
X-Gitlab-Event: Push Hook
```
**Payload example:**
Payload example:
```json
{
@ -111,20 +125,19 @@ X-Gitlab-Event: Push Hook
## Tag events
Triggered when you create (or delete) tags to the repository.
Tag events are triggered when you create or delete tags in the repository.
NOTE:
If a single push includes changes for more than three (by default, depending on
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls))
tags, this hook is not executed.
This hook is not executed if a single push includes changes for more than three
tags by default (depending on the
[`push_event_hooks_limit` setting](../../../api/settings.md#list-of-settings-that-can-be-accessed-via-api-calls)).
**Request header**:
Request header:
```plaintext
X-Gitlab-Event: Tag Push Hook
```
**Payload example:**
Payload example:
```json
{
@ -171,22 +184,26 @@ X-Gitlab-Event: Tag Push Hook
## Issue events
Triggered when a new issue is created or an existing issue was updated/closed/reopened.
Issue events are triggered when a new issue is created or
an existing issue is updated, closed, or reopened.
**Request header**:
```plaintext
X-Gitlab-Event: Issue Hook
```
**Available `object_attributes.action`:**
The available values for `object_attributes.action` in the payload are:
- `open`
- `close`
- `reopen`
- `update`
**Payload example:**
The `assignee` and `assignee_id` keys are deprecated
and contain the first assignee only.
Request header:
```plaintext
X-Gitlab-Event: Issue Hook
```
Payload example:
```json
{
@ -329,31 +346,31 @@ X-Gitlab-Event: Issue Hook
}
```
NOTE:
`assignee` and `assignee_id` keys are deprecated and now show the first assignee only.
## Comment events
Triggered when a new comment is made on commits, merge requests, issues, and code snippets.
The note data is stored in `object_attributes` (for example, `note` or `noteable_type`). The
payload also includes information about the target of the comment. For example,
a comment on an issue includes the specific issue information under the `issue` key.
Valid target types:
Comment events are triggered when a new comment is made on commits,
merge requests, issues, and code snippets.
The note data is stored in `object_attributes` (for example, `note` or `noteable_type`).
The payload includes information about the target of the comment. For example,
a comment on an issue includes specific issue information under the `issue` key.
The valid target types are:
- `commit`
- `merge_request`
- `issue`
- `snippet`
### Comment on commit
### Comment on a commit
**Request header**:
Request header:
```plaintext
X-Gitlab-Event: Note Hook
```
**Payload example:**
Payload example:
```json
{
@ -428,15 +445,15 @@ X-Gitlab-Event: Note Hook
}
```
### Comment on merge request
### Comment on a merge request
**Request header**:
Request header:
```plaintext
X-Gitlab-Event: Note Hook
```
**Payload example:**
Payload example:
```json
{
@ -558,15 +575,18 @@ X-Gitlab-Event: Note Hook
}
```
### Comment on issue
### Comment on an issue
**Request header**:
- The `assignee_id` field is deprecated and shows the first assignee only.
- The `event_type` is set to `confidential_note` for confidential issues.
Request header:
```plaintext
X-Gitlab-Event: Note Hook
```
**Payload example:**
Payload example:
```json
{
@ -664,21 +684,15 @@ X-Gitlab-Event: Note Hook
}
```
NOTE:
`assignee_id` field is deprecated and now shows the first assignee only.
### Comment on a code snippet
NOTE:
`event_type` is set to `confidential_note` for confidential issues.
### Comment on code snippet
**Request header**:
Request header:
```plaintext
X-Gitlab-Event: Note Hook
```
**Payload example:**
Payload example:
```json
{
@ -749,15 +763,13 @@ X-Gitlab-Event: Note Hook
## Merge request events
Triggered when a new merge request is created, an existing merge request was updated/merged/closed or a commit is added in the source branch.
Merge request events are triggered when:
**Request header**:
- A new merge request is created.
- An existing merge request is updated, merged, or closed.
- A commit is added in the source branch.
```plaintext
X-Gitlab-Event: Merge Request Hook
```
**Available `object_attributes.action`:**
The available values for `object_attributes.action` in the payload are:
- `open`
- `close`
@ -767,7 +779,13 @@ X-Gitlab-Event: Merge Request Hook
- `unapproved`
- `merge`
**Payload example:**
Request header:
```plaintext
X-Gitlab-Event: Merge Request Hook
```
Payload example:
```json
{
@ -921,17 +939,17 @@ X-Gitlab-Event: Merge Request Hook
}
```
## Wiki Page events
## Wiki page events
Triggered when a wiki page is created, updated or deleted.
Wiki page events are triggered when a wiki page is created, updated, or deleted.
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Wiki Page Hook
```
**Payload example**:
Payload example:
```json
{
@ -981,18 +999,18 @@ X-Gitlab-Event: Wiki Page Hook
## Pipeline events
Pipeline events are triggered when the status of a pipeline changes.
In [GitLab 13.9](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/53159)
and later, the pipeline webhook returns only the latest jobs.
Triggered on status change of Pipeline.
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Pipeline Hook
```
**Payload example**:
Payload example:
```json
{
@ -1233,15 +1251,17 @@ X-Gitlab-Event: Pipeline Hook
## Job events
Triggered on status change of a job.
Job events are triggered when the status of a job changes.
**Request Header**:
The `commit.id` in the payload is the ID of the pipeline, not the ID of the commit.
Request header:
```plaintext
X-Gitlab-Event: Job Hook
```
**Payload example**:
Payload example:
```json
{
@ -1303,24 +1323,24 @@ X-Gitlab-Event: Job Hook
}
```
Note that `commit.id` is the ID of the pipeline, not the ID of the commit.
## Deployment events
Triggered when a deployment:
Deployment events are triggered when a deployment:
- Starts ([Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214) in GitLab 13.5.)
- Starts ([introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41214) in GitLab 13.5)
- Succeeds
- Fails
- Is cancelled
**Request Header**:
The `deployable_id` in the payload is the ID of the CI/CD job.
Request header:
```plaintext
X-Gitlab-Event: Deployment Hook
```
**Payload example**:
Payload example:
```json
{
@ -1363,28 +1383,26 @@ X-Gitlab-Event: Deployment Hook
}
```
Note that `deployable_id` is the ID of the CI job.
## Group member events **(PREMIUM)**
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/260347) in GitLab 13.7.
Member events are triggered when:
- A user is added as a group member
- The access level of a user has changed
- The expiration date for user access has been updated
- A user has been removed from the group
- A user is added as a group member.
- The access level of a user changes.
- The expiration date for user access is updated.
- A user is removed from the group.
### Add member to group
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Member Hook
```
**Payload example**:
Payload example:
```json
{
@ -1406,13 +1424,13 @@ X-Gitlab-Event: Member Hook
### Update member access level or expiration date
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Member Hook
```
**Payload example**:
Payload example:
```json
{
@ -1434,13 +1452,13 @@ X-Gitlab-Event: Member Hook
### Remove member from group
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Member Hook
```
**Payload example**:
Payload example:
```json
{
@ -1466,18 +1484,18 @@ X-Gitlab-Event: Member Hook
Subgroup events are triggered when:
- A [subgroup is created in a group](#subgroup-created-in-a-group)
- A [subgroup is removed from a group](#subgroup-removed-from-a-group)
- A [subgroup is created in a group](#create-a-subgroup-in-a-group).
- A [subgroup is removed from a group](#remove-a-subgroup-from-a-group).
### Subgroup created in a group
### Create a subgroup in a group
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Subgroup Hook
```
**Payload example**:
Payload example:
```json
{
@ -1497,15 +1515,17 @@ X-Gitlab-Event: Subgroup Hook
}
```
### Subgroup removed from a group
### Remove a subgroup from a group
**Request Header**:
This webhook is not triggered when a [subgroup is transferred to a new parent group](../../group/index.md#transfer-a-group).
Request header:
```plaintext
X-Gitlab-Event: Subgroup Hook
```
**Payload example**:
Payload example:
```json
{
@ -1525,20 +1545,17 @@ X-Gitlab-Event: Subgroup Hook
}
```
NOTE:
Webhooks for when a [subgroup is removed from a group](#subgroup-removed-from-a-group) are not triggered when a [subgroup is transferred to a new parent group](../../group/index.md#transfer-a-group)
## Feature flag events
## Feature Flag events
Feature flag events are triggered when a feature flag is turned on or off.
Triggered when a feature flag is turned on or off.
**Request Header**:
Request header:
```plaintext
X-Gitlab-Event: Feature Flag Hook
```
**Payload example**:
Payload example:
```json
{
@ -1580,20 +1597,20 @@ X-Gitlab-Event: Feature Flag Hook
## Release events
Triggered when a release is created or updated.
Release events are triggered when a release is created or updated.
**Request Header**:
The available values for `object_attributes.action` in the payload are:
- `create`
- `update`
Request header:
```plaintext
X-Gitlab-Event: Release Hook
```
**Available `object_attributes.action`:**
- `create`
- `update`
**Payload example**:
Payload example:
```json
{

View File

@ -0,0 +1,22 @@
# frozen_string_literal: true
module Gitlab
module Patch
module SidekiqClient
private
# This is a copy of https://github.com/mperham/sidekiq/blob/v6.2.2/lib/sidekiq/client.rb#L187-L194
# but using `conn.pipelined` instead of `conn.multi`. The multi call isn't needed here because in
# the case of scheduled jobs, only one Redis call is made. For other jobs, we don't really need
# the commands to be atomic.
def raw_push(payloads)
@redis_pool.with do |conn| # rubocop:disable Gitlab/ModuleWithInstanceVariables
conn.pipelined do
atomic_push(conn, payloads)
end
end
true
end
end
end
end

View File

@ -13767,9 +13767,6 @@ msgstr ""
msgid "Example: @sub\\.company\\.com$"
msgstr ""
msgid "Example: Usage = single query. (Requested) / (Capacity) = multiple queries combined into a formula."
msgstr ""
msgid "Except policy:"
msgstr ""
@ -18579,9 +18576,6 @@ msgstr ""
msgid "Invalid policy type"
msgstr ""
msgid "Invalid query"
msgstr ""
msgid "Invalid repository bundle for snippet with id %{snippet_id}"
msgstr ""
@ -21831,9 +21825,6 @@ msgstr ""
msgid "Metrics|For grouping similar metrics"
msgstr ""
msgid "Metrics|GitLab-managed Prometheus is deprecated and %{linkStart}scheduled for removal%{linkEnd}. Following this removal, your existing alerts will continue to function as part of the new cluster integration. However, you will no longer be able to add new alerts or edit existing alerts from the metrics dashboard."
msgstr ""
msgid "Metrics|Invalid time range, please verify."
msgstr ""
@ -21969,9 +21960,6 @@ msgstr ""
msgid "Metrics|View logs"
msgstr ""
msgid "Metrics|View runbook - %{label}"
msgstr ""
msgid "Metrics|Y-axis label"
msgstr ""
@ -27299,54 +27287,9 @@ msgstr ""
msgid "Prometheus"
msgstr ""
msgid "PrometheusAlerts|%{count} alerts applied"
msgstr ""
msgid "PrometheusAlerts|%{firingCount} firing"
msgstr ""
msgid "PrometheusAlerts|Add alert"
msgstr ""
msgid "PrometheusAlerts|Edit alert"
msgstr ""
msgid "PrometheusAlerts|Error creating alert"
msgstr ""
msgid "PrometheusAlerts|Error deleting alert"
msgstr ""
msgid "PrometheusAlerts|Error fetching alert"
msgstr ""
msgid "PrometheusAlerts|Error saving alert"
msgstr ""
msgid "PrometheusAlerts|Firing: %{alerts}"
msgstr ""
msgid "PrometheusAlerts|Firing: %{alert}"
msgstr ""
msgid "PrometheusAlerts|Operator"
msgstr ""
msgid "PrometheusAlerts|Runbook URL (optional)"
msgstr ""
msgid "PrometheusAlerts|Select query"
msgstr ""
msgid "PrometheusAlerts|Threshold"
msgstr ""
msgid "PrometheusAlerts|exceeded"
msgstr ""
msgid "PrometheusAlerts|https://gitlab.com/gitlab-com/runbooks"
msgstr ""
msgid "PrometheusAlerts|is equal to"
msgstr ""
@ -28007,9 +27950,6 @@ msgstr ""
msgid "Query cannot be processed"
msgstr ""
msgid "Query is valid"
msgstr ""
msgid "Queued"
msgstr ""
@ -31703,9 +31643,6 @@ msgstr ""
msgid "Simulate a pipeline created for the default branch"
msgstr ""
msgid "Single or combined queries"
msgstr ""
msgid "Site profile failed to delete"
msgstr ""

View File

@ -12,6 +12,7 @@ module QA
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do
element :fork_namespace_dropdown
element :fork_project_button
element :fork_privacy_button
end
def fork_project(namespace = Runtime::Namespace.path)
@ -19,6 +20,7 @@ module QA
click_element(:fork_namespace_button, name: namespace)
else
select_element(:fork_namespace_dropdown, namespace)
click_element(:fork_privacy_button, privacy_level: 'public')
click_element(:fork_project_button)
end
end

View File

@ -31,7 +31,6 @@ module QA
view 'app/assets/javascripts/monitoring/components/dashboard_panel.vue' do
element :prometheus_graph_widgets
element :prometheus_widgets_dropdown
element :alert_widget_menu_item
element :generate_chart_link_menu_item
end

View File

@ -37,11 +37,7 @@ module QA
namespace_path ||= user.name
# Sign out as admin and sign is as the fork user
Page::Main::Menu.perform(&:sign_out)
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform do |login|
login.sign_in_using_credentials(user: user)
end
Flow::Login.sign_in(as: user)
upstream.visit!
@ -61,9 +57,6 @@ module QA
def fabricate_via_api!
populate(:upstream, :user)
# Remove after Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/343396
Flow::Login.sign_in(as: user) if Specs::Helpers::ContextSelector.dot_com?
@api_client = Runtime::API::Client.new(:gitlab, is_new_session: false, user: user)
Runtime::Logger.debug("Forking project #{upstream.name} to namespace #{user.username}...")
@ -71,9 +64,6 @@ module QA
wait_until_forked
populate(:project)
# Remove after Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/343396
Flow::Login.sign_in if Specs::Helpers::ContextSelector.dot_com?
end
def remove_via_api!

View File

@ -6,7 +6,7 @@ module QA
attr_accessor :fork_branch
attribute :fork do
Fork.fabricate_via_api!
Fork.fabricate_via_browser_ui!
end
attribute :push do

View File

@ -76,6 +76,15 @@ module QA
RSpec.configure do |config|
config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter)
config.append_after do |example|
Allure.add_attachment(
name: 'browser.log',
source: Capybara.current_session.driver.browser.logs.get(:browser).map(&:to_s).join("\n\n"),
type: Allure::ContentType::TXT,
test_case: true
)
end
end
end

View File

@ -3,7 +3,7 @@
describe QA::Runtime::AllureReport do
include QA::Support::Helpers::StubEnv
let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) }
let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, append_after: nil) }
let(:png_path) { 'png_path' }
let(:html_path) { 'html_path' }
@ -46,6 +46,8 @@ describe QA::Runtime::AllureReport do
let(:html_file) { 'html-file' }
let(:ci_job) { 'ee:relative 5' }
let(:versions) { { version: '14', revision: '6ced31db947' } }
let(:session) { double('session') }
let(:browser_log) { ['log message 1', 'log message 2'] }
before do
stub_env('CI', 'true')
@ -58,6 +60,9 @@ describe QA::Runtime::AllureReport do
allow(RestClient::Request).to receive(:execute) { double('response', code: 200, body: versions.to_json) }
allow(QA::Runtime::Scenario).to receive(:method_missing).with(:gitlab_address).and_return('gitlab.com')
allow(Capybara).to receive(:current_session).and_return(session)
allow(session).to receive_message_chain('driver.browser.logs.get').and_return(browser_log)
described_class.configure!
end
@ -76,7 +81,11 @@ describe QA::Runtime::AllureReport do
.with(QA::Support::Formatters::AllureMetadataFormatter).ordered
end
it 'configures screenshot saving' do
it 'configures attachments saving' do
expect(rspec_config).to have_received(:append_after) do |&arg|
arg.call
end
aggregate_failures do
expect(Allure).to have_received(:add_attachment).with(
name: 'screenshot',
@ -90,6 +99,12 @@ describe QA::Runtime::AllureReport do
type: 'text/html',
test_case: true
)
expect(Allure).to have_received(:add_attachment).with(
name: 'browser.log',
source: browser_log.join("\n\n"),
type: Allure::ContentType::TXT,
test_case: true
)
end
end
end

View File

@ -55,28 +55,4 @@ RSpec.describe 'Alert Management index', :js do
it_behaves_like 'alert page with title, filtered search, and table'
end
end
describe 'managed_alerts_deprecation feature flag' do
subject { page }
before do
stub_feature_flags(managed_alerts_deprecation: feature_flag_value)
sign_in(developer)
visit project_alert_management_index_path(project)
wait_for_requests
end
context 'feature flag on' do
let(:feature_flag_value) { true }
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: true) }
end
context 'feature flag off' do
let(:feature_flag_value) { false }
it { is_expected.to have_pushed_frontend_feature_flags(managedAlertsDeprecation: false) }
end
end
end

View File

@ -40,7 +40,6 @@ describe('AlertManagementTable', () => {
resolved: 11,
all: 26,
};
const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
function mountComponent({ provide = {}, data = {}, loading = false, stubs = {} } = {}) {
wrapper = extendedWrapper(
@ -49,7 +48,6 @@ describe('AlertManagementTable', () => {
...defaultProvideValues,
alertManagementEnabled: true,
userCanEnableAlertManagement: true,
hasManagedPrometheus: false,
...provide,
},
data() {
@ -237,22 +235,6 @@ describe('AlertManagementTable', () => {
expect(visitUrl).toHaveBeenCalledWith('/1527542/details', true);
});
it.each`
managedAlertsDeprecation | hasManagedPrometheus | isVisible
${false} | ${false} | ${false}
${false} | ${true} | ${true}
${true} | ${false} | ${false}
${true} | ${true} | ${false}
`(
'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
mountComponent({
provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } },
});
expect(findDeprecationNotice().exists()).toBe(isVisible);
},
);
describe('alert issue links', () => {
beforeEach(() => {
mountComponent({

View File

@ -1,43 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
<gl-badge-stub
class="d-flex-center text-truncate"
size="md"
variant="danger"
>
<gl-icon-stub
class="flex-shrink-0"
name="warning"
size="16"
/>
<span
class="text-truncate gl-pl-2"
>
Firing:
alert-label &gt; 42
</span>
</gl-badge-stub>
`;
exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
<gl-badge-stub
class="d-flex-center text-truncate"
size="md"
variant="neutral"
>
<gl-icon-stub
class="flex-shrink-0"
name="warning"
size="16"
/>
<span
class="text-truncate gl-pl-2"
>
alert-label &gt; 42
</span>
</gl-badge-stub>
`;

View File

@ -1,423 +0,0 @@
import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
const mockReadAlert = jest.fn();
const mockCreateAlert = jest.fn();
const mockUpdateAlert = jest.fn();
const mockDeleteAlert = jest.fn();
jest.mock('~/flash');
jest.mock(
'~/monitoring/services/alerts_service',
() =>
function AlertsServiceMock() {
return {
readAlert: mockReadAlert,
createAlert: mockCreateAlert,
updateAlert: mockUpdateAlert,
deleteAlert: mockDeleteAlert,
};
},
);
describe('AlertWidget', () => {
let wrapper;
const nonFiringAlertResult = [
{
values: [
[0, 1],
[1, 42],
[2, 41],
],
},
];
const firingAlertResult = [
{
values: [
[0, 42],
[1, 43],
[2, 44],
],
},
];
const metricId = '5';
const alertPath = 'my/alert.json';
const relevantQueries = [
{
metricId,
label: 'alert-label',
alert_path: alertPath,
result: nonFiringAlertResult,
},
];
const firingRelevantQueries = [
{
metricId,
label: 'alert-label',
alert_path: alertPath,
result: firingAlertResult,
},
];
const defaultProps = {
alertsEndpoint: '',
relevantQueries,
alertsToManage: {},
modalId: 'alert-modal-1',
};
const propsWithAlert = {
relevantQueries,
};
const propsWithAlertData = {
relevantQueries,
alertsToManage: {
[alertPath]: { operator: '>', threshold: 42, alert_path: alertPath, metricId },
},
};
const createComponent = (propsData) => {
wrapper = shallowMount(AlertWidget, {
stubs: { GlTooltip, GlSprintf },
propsData: {
...defaultProps,
...propsData,
},
});
};
const hasLoadingIcon = () => wrapper.find(GlLoadingIcon).exists();
const findWidgetForm = () => wrapper.find({ ref: 'widgetForm' });
const findAlertErrorMessage = () => wrapper.find({ ref: 'alertErrorMessage' });
const findCurrentSettingsText = () =>
wrapper.find({ ref: 'alertCurrentSetting' }).text().replace(/\s\s+/g, ' ');
const findBadge = () => wrapper.find(GlBadge);
const findTooltip = () => wrapper.find(GlTooltip);
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
it('displays a loading spinner and disables form when fetching alerts', () => {
let resolveReadAlert;
mockReadAlert.mockReturnValue(
new Promise((resolve) => {
resolveReadAlert = resolve;
}),
);
createComponent(defaultProps);
return wrapper.vm
.$nextTick()
.then(() => {
expect(hasLoadingIcon()).toBe(true);
expect(findWidgetForm().props('disabled')).toBe(true);
resolveReadAlert({ operator: '==', threshold: 42 });
})
.then(() => waitForPromises())
.then(() => {
expect(hasLoadingIcon()).toBe(false);
expect(findWidgetForm().props('disabled')).toBe(false);
});
});
it('does not render loading spinner if showLoadingState is false', () => {
let resolveReadAlert;
mockReadAlert.mockReturnValue(
new Promise((resolve) => {
resolveReadAlert = resolve;
}),
);
createComponent({
...defaultProps,
showLoadingState: false,
});
return wrapper.vm
.$nextTick()
.then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
resolveReadAlert({ operator: '==', threshold: 42 });
})
.then(() => waitForPromises())
.then(() => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
});
it('displays an error message when fetch fails', () => {
mockReadAlert.mockRejectedValue();
createComponent(propsWithAlert);
expect(hasLoadingIcon()).toBe(true);
return waitForPromises().then(() => {
expect(createFlash).toHaveBeenCalled();
expect(hasLoadingIcon()).toBe(false);
});
});
describe('Alert not firing', () => {
it('displays a warning icon and matches snapshot', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
createComponent(propsWithAlertData);
return waitForPromises().then(() => {
expect(findBadge().element).toMatchSnapshot();
});
});
it('displays an alert summary when there is a single alert', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
createComponent(propsWithAlertData);
return waitForPromises().then(() => {
expect(findCurrentSettingsText()).toEqual('alert-label > 42');
});
});
it('displays a combined alert summary when there are multiple alerts', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
const propsWithManyAlerts = {
relevantQueries: [
...relevantQueries,
...[
{
metricId: '6',
alert_path: 'my/alert2.json',
label: 'alert-label2',
result: [{ values: [] }],
},
],
],
alertsToManage: {
'my/alert.json': {
operator: '>',
threshold: 42,
alert_path: alertPath,
metricId,
},
'my/alert2.json': {
operator: '==',
threshold: 900,
alert_path: 'my/alert2.json',
metricId: '6',
},
},
};
createComponent(propsWithManyAlerts);
return waitForPromises().then(() => {
expect(findCurrentSettingsText()).toContain('2 alerts applied');
});
});
});
describe('Alert firing', () => {
it('displays a warning icon and matches snapshot', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
propsWithAlertData.relevantQueries = firingRelevantQueries;
createComponent(propsWithAlertData);
return waitForPromises().then(() => {
expect(findBadge().element).toMatchSnapshot();
});
});
it('displays an alert summary when there is a single alert', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
propsWithAlertData.relevantQueries = firingRelevantQueries;
createComponent(propsWithAlertData);
return waitForPromises().then(() => {
expect(findCurrentSettingsText()).toEqual('Firing: alert-label > 42');
});
});
it('displays a combined alert summary when there are multiple alerts', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
const propsWithManyAlerts = {
relevantQueries: [
...firingRelevantQueries,
...[
{
metricId: '6',
alert_path: 'my/alert2.json',
label: 'alert-label2',
result: [{ values: [] }],
},
],
],
alertsToManage: {
'my/alert.json': {
operator: '>',
threshold: 42,
alert_path: alertPath,
metricId,
},
'my/alert2.json': {
operator: '==',
threshold: 900,
alert_path: 'my/alert2.json',
metricId: '6',
},
},
};
createComponent(propsWithManyAlerts);
return waitForPromises().then(() => {
expect(findCurrentSettingsText()).toContain('2 alerts applied, 1 firing');
});
});
it('should display tooltip with thresholds summary', () => {
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
const propsWithManyAlerts = {
relevantQueries: [
...firingRelevantQueries,
...[
{
metricId: '6',
alert_path: 'my/alert2.json',
label: 'alert-label2',
result: [{ values: [] }],
},
],
],
alertsToManage: {
'my/alert.json': {
operator: '>',
threshold: 42,
alert_path: alertPath,
metricId,
},
'my/alert2.json': {
operator: '==',
threshold: 900,
alert_path: 'my/alert2.json',
metricId: '6',
},
},
};
createComponent(propsWithManyAlerts);
return waitForPromises().then(() => {
expect(findTooltip().text().replace(/\s\s+/g, ' ')).toEqual('Firing: alert-label > 42');
});
});
});
it('creates an alert with an appropriate handler', () => {
const alertParams = {
operator: '<',
threshold: 4,
prometheus_metric_id: '5',
};
mockReadAlert.mockResolvedValue({ operator: '>', threshold: 42 });
const fakeAlertPath = 'foo/bar';
mockCreateAlert.mockResolvedValue({ alert_path: fakeAlertPath, ...alertParams });
createComponent({
alertsToManage: {
[fakeAlertPath]: {
alert_path: fakeAlertPath,
operator: '<',
threshold: 4,
prometheus_metric_id: '5',
metricId: '5',
},
},
});
findWidgetForm().vm.$emit('create', alertParams);
expect(mockCreateAlert).toHaveBeenCalledWith(alertParams);
});
it('updates an alert with an appropriate handler', () => {
const alertParams = { operator: '<', threshold: 4, alert_path: alertPath };
const newAlertParams = { operator: '==', threshold: 12 };
mockReadAlert.mockResolvedValue(alertParams);
mockUpdateAlert.mockResolvedValue({ ...alertParams, ...newAlertParams });
createComponent({
...propsWithAlertData,
alertsToManage: {
[alertPath]: {
alert_path: alertPath,
operator: '==',
threshold: 12,
metricId: '5',
},
},
});
findWidgetForm().vm.$emit('update', {
alert: alertPath,
...newAlertParams,
prometheus_metric_id: '5',
});
expect(mockUpdateAlert).toHaveBeenCalledWith(alertPath, newAlertParams);
});
it('deletes an alert with an appropriate handler', () => {
const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
mockReadAlert.mockResolvedValue(alertParams);
mockDeleteAlert.mockResolvedValue({});
createComponent({
...propsWithAlert,
alertsToManage: {
[alertPath]: {
alert_path: alertPath,
operator: '>',
threshold: 42,
metricId: '5',
},
},
});
findWidgetForm().vm.$emit('delete', { alert: alertPath });
return wrapper.vm.$nextTick().then(() => {
expect(mockDeleteAlert).toHaveBeenCalledWith(alertPath);
expect(findAlertErrorMessage().exists()).toBe(false);
});
});
describe('when delete fails', () => {
beforeEach(() => {
const alertParams = { alert_path: alertPath, operator: '>', threshold: 42 };
mockReadAlert.mockResolvedValue(alertParams);
mockDeleteAlert.mockRejectedValue();
createComponent({
...propsWithAlert,
alertsToManage: {
[alertPath]: {
alert_path: alertPath,
operator: '>',
threshold: 42,
metricId: '5',
},
},
});
findWidgetForm().vm.$emit('delete', { alert: alertPath });
return wrapper.vm.$nextTick();
});
it('shows error message', () => {
expect(findAlertErrorMessage().text()).toEqual('Error deleting alert');
});
it('dismisses error message on cancel', () => {
findWidgetForm().vm.$emit('cancel');
return wrapper.vm.$nextTick().then(() => {
expect(findAlertErrorMessage().exists()).toBe(false);
});
});
});
});

View File

@ -8,8 +8,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
metricsdashboardbasepath="/monitoring/monitor-project/-/environments/1/metrics"
metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
>
<alerts-deprecation-warning-stub />
<div
class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
>

View File

@ -1,242 +0,0 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import INVALID_URL from '~/lib/utils/invalid_url';
import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
import ModalStub from '../stubs/modal_stub';
describe('AlertWidgetForm', () => {
let wrapper;
const metricId = '8';
const alertPath = 'alert';
const relevantQueries = [{ metricId, alert_path: alertPath, label: 'alert-label' }];
const dataTrackingOptions = {
create: { action: 'click_button', label: 'create_alert' },
delete: { action: 'click_button', label: 'delete_alert' },
update: { action: 'click_button', label: 'update_alert' },
};
const defaultProps = {
disabled: false,
relevantQueries,
modalId: 'alert-modal-1',
};
const propsWithAlertData = {
...defaultProps,
alertsToManage: {
alert: {
alert_path: alertPath,
operator: '<',
threshold: 5,
metricId,
runbookUrl: INVALID_URL,
},
},
configuredAlert: metricId,
};
function createComponent(props = {}) {
const propsData = {
...defaultProps,
...props,
};
wrapper = shallowMount(AlertWidgetForm, {
propsData,
stubs: {
GlModal: ModalStub,
},
});
}
const modal = () => wrapper.find(ModalStub);
const modalTitle = () => modal().attributes('title');
const submitButton = () => modal().find(GlLink);
const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]');
const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]');
const submitButtonTrackingOpts = () =>
JSON.parse(submitButton().attributes('data-tracking-options'));
const stubEvent = { preventDefault: jest.fn() };
afterEach(() => {
if (wrapper) wrapper.destroy();
});
it('disables the form when disabled prop is set', () => {
createComponent({ disabled: true });
expect(modal().attributes('ok-disabled')).toBe('true');
});
it('disables the form if no query is selected', () => {
createComponent();
expect(modal().attributes('ok-disabled')).toBe('true');
});
it('shows correct title and button text', () => {
createComponent();
expect(modalTitle()).toBe('Add alert');
expect(submitButton().text()).toBe('Add');
});
it('sets tracking options for create alert', () => {
createComponent();
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
});
it('emits a "create" event when form submitted without existing alert', async () => {
createComponent(defaultProps);
modal().vm.$emit('shown');
findThresholdField().vm.$emit('input', 900);
findRunbookField().vm.$emit('input', INVALID_URL);
modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().create[0]).toEqual([
{
alert: undefined,
operator: '>',
threshold: 900,
prometheus_metric_id: '8',
runbookUrl: INVALID_URL,
},
]);
});
it('resets form when modal is dismissed (hidden)', () => {
createComponent(defaultProps);
modal().vm.$emit('shown');
findThresholdField().vm.$emit('input', 800);
findRunbookField().vm.$emit('input', INVALID_URL);
modal().vm.$emit('hidden');
expect(wrapper.vm.selectedAlert).toEqual({});
expect(wrapper.vm.operator).toBe(null);
expect(wrapper.vm.threshold).toBe(null);
expect(wrapper.vm.prometheusMetricId).toBe(null);
expect(wrapper.vm.runbookUrl).toBe(null);
});
it('sets selectedAlert to the provided configuredAlert on modal show', () => {
createComponent(propsWithAlertData);
modal().vm.$emit('shown');
expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
});
it('sets selectedAlert to the first relevantQueries if there is only one option on modal show', () => {
createComponent({
...propsWithAlertData,
configuredAlert: '',
});
modal().vm.$emit('shown');
expect(wrapper.vm.selectedAlert).toEqual(propsWithAlertData.alertsToManage[alertPath]);
});
it('does not set selectedAlert to the first relevantQueries if there is more than one option on modal show', () => {
createComponent({
relevantQueries: [
{
metricId: '8',
alertPath: 'alert',
label: 'alert-label',
},
{
metricId: '9',
alertPath: 'alert',
label: 'alert-label',
},
],
});
modal().vm.$emit('shown');
expect(wrapper.vm.selectedAlert).toEqual({});
});
describe('with existing alert', () => {
beforeEach(() => {
createComponent(propsWithAlertData);
modal().vm.$emit('shown');
});
it('sets tracking options for delete alert', () => {
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.delete);
});
it('updates button text', () => {
expect(modalTitle()).toBe('Edit alert');
expect(submitButton().text()).toBe('Delete');
});
it('emits "delete" event when form values unchanged', () => {
modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().delete[0]).toEqual([
{
alert: 'alert',
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
runbookUrl: INVALID_URL,
},
]);
});
});
it('emits "update" event when form changed', () => {
const updatedRunbookUrl = `${INVALID_URL}/test`;
createComponent(propsWithAlertData);
modal().vm.$emit('shown');
findRunbookField().vm.$emit('input', updatedRunbookUrl);
findThresholdField().vm.$emit('input', 11);
modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().update[0]).toEqual([
{
alert: 'alert',
operator: '<',
threshold: 11,
prometheus_metric_id: '8',
runbookUrl: updatedRunbookUrl,
},
]);
});
it('sets tracking options for update alert', async () => {
createComponent(propsWithAlertData);
modal().vm.$emit('shown');
findThresholdField().vm.$emit('input', 11);
await wrapper.vm.$nextTick();
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
});
describe('alert runbooks', () => {
it('shows the runbook field', () => {
createComponent();
expect(findRunbookField().exists()).toBe(true);
});
});
});

View File

@ -159,10 +159,6 @@ describe('Anomaly chart component', () => {
const { deploymentData } = getTimeSeriesProps();
expect(deploymentData).toEqual(anomalyDeploymentData);
});
it('"thresholds" keeps the same value', () => {
const { thresholds } = getTimeSeriesProps();
expect(thresholds).toEqual(inputThresholds);
});
it('"projectPath" keeps the same value', () => {
const { projectPath } = getTimeSeriesProps();
expect(projectPath).toEqual(mockProjectPath);

View File

@ -643,7 +643,6 @@ describe('Time series component', () => {
expect(props.data).toBe(wrapper.vm.chartData);
expect(props.option).toBe(wrapper.vm.chartOptions);
expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
expect(props.thresholds).toBe(wrapper.vm.thresholds);
});
it('receives a tooltip title', () => {

View File

@ -28,7 +28,6 @@ describe('dashboard invalid url parameters', () => {
},
},
options,
provide: { hasManagedPrometheus: false },
});
};

View File

@ -5,7 +5,6 @@ import Vuex from 'vuex';
import { setTestTimeout } from 'helpers/timeout';
import axios from '~/lib/utils/axios_utils';
import invalidUrl from '~/lib/utils/invalid_url';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
@ -28,7 +27,6 @@ import {
barGraphData,
} from '../graph_data';
import {
mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
@ -56,7 +54,6 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
const findAlertsWidget = () => wrapper.find(AlertWidget);
const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(DashboardPanel, {
@ -80,9 +77,6 @@ describe('Dashboard Panel', () => {
});
};
const setMetricsSavedToDb = (val) =>
monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
beforeEach(() => {
setTestTimeout(1000);
@ -601,42 +595,6 @@ describe('Dashboard Panel', () => {
});
});
describe('panel alerts', () => {
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
createWrapper();
});
describe.each`
desc | metricsSavedToDb | props | isShown
${'with permission and no metrics in db'} | ${[]} | ${{}} | ${false}
${'with permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{}} | ${true}
${'without permission and related metrics in db'} | ${[graphData.metrics[0].metricId]} | ${{ prometheusAlertsAvailable: false }} | ${false}
${'with permission and unrelated metrics in db'} | ${['another_metric_id']} | ${{}} | ${false}
`('$desc', ({ metricsSavedToDb, isShown, props }) => {
const showsDesc = isShown ? 'shows' : 'does not show';
beforeEach(() => {
setMetricsSavedToDb(metricsSavedToDb);
createWrapper({
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true,
...props,
});
return wrapper.vm.$nextTick();
});
it(`${showsDesc} alert widget`, () => {
expect(findAlertsWidget().exists()).toBe(isShown);
});
it(`${showsDesc} alert configuration`, () => {
expect(findMenuItemByText('Alerts').exists()).toBe(isShown);
});
});
});
describe('When graphData contains links', () => {
const findManageLinksItem = () => wrapper.find({ ref: 'manageLinksItem' });
const mockLinks = [
@ -730,13 +688,6 @@ describe('Dashboard Panel', () => {
describe('Runbook url', () => {
const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
const { metricId } = graphData.metrics[0];
const { alert_path: alertPath } = mockAlert;
const mockRunbookAlert = {
...mockAlert,
metricId,
};
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
@ -747,62 +698,5 @@ describe('Dashboard Panel', () => {
expect(findRunbookLinks().length).toBe(0);
});
describe('when alerts are present', () => {
beforeEach(() => {
setMetricsSavedToDb([metricId]);
createWrapper({
alertsEndpoint: '/endpoint',
prometheusAlertsAvailable: true,
});
});
it('does not show a runbook link when a runbook is not set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, {
...mockRunbookAlert,
runbookUrl: '',
});
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(0);
});
it('shows a runbook link when a runbook is set', async () => {
findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
await wrapper.vm.$nextTick();
expect(findRunbookLinks().length).toBe(1);
expect(findRunbookLinks().at(0).attributes('href')).toBe(invalidUrl);
});
});
describe('managed alert deprecation feature flag', () => {
beforeEach(() => {
setMetricsSavedToDb([metricId]);
});
it('shows alerts when alerts are not deprecated', () => {
createWrapper(
{ alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
{ provide: { glFeatures: { managedAlertsDeprecation: false } } },
);
expect(findAlertsWidget().exists()).toBe(true);
expect(findMenuItemByText('Alerts').exists()).toBe(true);
});
it('hides alerts when alerts are deprecated', () => {
createWrapper(
{ alertsEndpoint: '/endpoint', prometheusAlertsAvailable: true },
{ provide: { glFeatures: { managedAlertsDeprecation: true } } },
);
expect(findAlertsWidget().exists()).toBe(false);
expect(findMenuItemByText('Alerts').exists()).toBe(false);
});
});
});
});

View File

@ -46,7 +46,6 @@ describe('Dashboard', () => {
stubs: {
DashboardHeader,
},
provide: { hasManagedPrometheus: false },
...options,
});
};
@ -60,9 +59,6 @@ describe('Dashboard', () => {
'dashboard-panel': true,
'dashboard-header': DashboardHeader,
},
provide: {
hasManagedPrometheus: false,
},
...options,
});
};
@ -807,29 +803,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true);
});
});
describe('alerts deprecation', () => {
beforeEach(() => {
setupStoreWithData(store);
});
const findDeprecationNotice = () => wrapper.findByTestId('alerts-deprecation-warning');
it.each`
managedAlertsDeprecation | hasManagedPrometheus | isVisible
${false} | ${false} | ${false}
${false} | ${true} | ${true}
${true} | ${false} | ${false}
${true} | ${true} | ${false}
`(
'when the deprecation feature flag is $managedAlertsDeprecation and has managed prometheus is $hasManagedPrometheus',
({ hasManagedPrometheus, managedAlertsDeprecation, isVisible }) => {
createMountedWrapper(
{},
{ provide: { hasManagedPrometheus, glFeatures: { managedAlertsDeprecation } } },
);
expect(findDeprecationNotice().exists()).toBe(isVisible);
},
);
});
});

View File

@ -31,7 +31,6 @@ describe('dashboard invalid url parameters', () => {
store,
stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
...options,
provide: { hasManagedPrometheus: false },
});
};

View File

@ -20,8 +20,6 @@ const MockApp = {
template: `<router-view :dashboard-props="dashboardProps"/>`,
};
const provide = { hasManagedPrometheus: false };
describe('Monitoring router', () => {
let router;
let store;
@ -39,7 +37,6 @@ describe('Monitoring router', () => {
localVue,
store,
router,
provide,
});
};

View File

@ -1,48 +0,0 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import AlertDeprecationWarning from '~/vue_shared/components/alerts_deprecation_warning.vue';
describe('AlertDetails', () => {
let wrapper;
function mountComponent(hasManagedPrometheus = false) {
wrapper = mount(AlertDeprecationWarning, {
provide: {
hasManagedPrometheus,
},
});
}
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
describe('Alert details', () => {
describe('with no manual prometheus', () => {
beforeEach(() => {
mountComponent();
});
it('renders nothing', () => {
expect(findAlert().exists()).toBe(false);
});
});
describe('with manual prometheus', () => {
beforeEach(() => {
mountComponent(true);
});
it('renders a deprecation notice', () => {
expect(findAlert().text()).toContain('GitLab-managed Prometheus is deprecated');
expect(findLink().attributes('href')).toContain(
'operations/metrics/alerts.html#managed-prometheus-instances',
);
});
});
});
});

View File

@ -40,12 +40,10 @@ RSpec.describe EnvironmentsHelper do
'validate_query_path' => validate_query_project_prometheus_metrics_path(project),
'custom_metrics_available' => 'true',
'alerts_endpoint' => project_prometheus_alerts_path(project, environment_id: environment.id, format: :json),
'prometheus_alerts_available' => 'true',
'custom_dashboard_base_path' => Gitlab::Metrics::Dashboard::RepoDashboardFinder::DASHBOARD_ROOT,
'operations_settings_path' => project_settings_operations_path(project),
'can_access_operations_settings' => 'true',
'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json),
'has_managed_prometheus' => 'false'
'panel_preview_endpoint' => project_metrics_dashboards_builder_path(project, format: :json)
)
end
@ -63,20 +61,6 @@ RSpec.describe EnvironmentsHelper do
end
end
context 'without read_prometheus_alerts permission' do
before do
allow(helper).to receive(:can?)
.with(user, :read_prometheus_alerts, project)
.and_return(false)
end
it 'returns false' do
expect(metrics_data).to include(
'prometheus_alerts_available' => 'false'
)
end
end
context 'with metrics_setting' do
before do
create(:project_metrics_setting, project: project, external_dashboard_url: 'http://gitlab.com')
@ -120,52 +104,6 @@ RSpec.describe EnvironmentsHelper do
end
end
end
context 'has_managed_prometheus' do
context 'without prometheus integration' do
it "doesn't have managed prometheus" do
expect(metrics_data).to include(
'has_managed_prometheus' => 'false'
)
end
end
context 'with prometheus integration' do
let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) }
context 'when manual prometheus integration is active' do
it "doesn't have managed prometheus" do
prometheus_integration.update!(manual_configuration: true)
expect(metrics_data).to include(
'has_managed_prometheus' => 'false'
)
end
end
context 'when prometheus integration is inactive' do
it "doesn't have managed prometheus" do
prometheus_integration.update!(manual_configuration: false)
expect(metrics_data).to include(
'has_managed_prometheus' => 'false'
)
end
end
context 'when a cluster prometheus is available' do
let(:cluster) { create(:cluster, projects: [project]) }
it 'has managed prometheus' do
create(:clusters_integrations_prometheus, cluster: cluster)
expect(metrics_data).to include(
'has_managed_prometheus' => 'true'
)
end
end
end
end
end
describe '#custom_metrics_available?' do

View File

@ -34,7 +34,6 @@ RSpec.describe Projects::AlertManagementHelper do
'empty-alert-svg-path' => match_asset_path('/assets/illustrations/alert-management-empty-state.svg'),
'user-can-enable-alert-management' => 'true',
'alert-management-enabled' => 'false',
'has-managed-prometheus' => 'false',
'text-query': nil,
'assignee-username-query': nil
)
@ -45,52 +44,26 @@ RSpec.describe Projects::AlertManagementHelper do
let_it_be(:prometheus_integration) { create(:prometheus_integration, project: project) }
context 'when manual prometheus integration is active' do
it "enables alert management and doesn't show managed prometheus" do
it "enables alert management" do
prometheus_integration.update!(manual_configuration: true)
expect(data).to include(
'alert-management-enabled' => 'true'
)
expect(data).to include(
'has-managed-prometheus' => 'false'
)
end
end
context 'when a cluster prometheus is available' do
let(:cluster) { create(:cluster, projects: [project]) }
it 'has managed prometheus' do
create(:clusters_integrations_prometheus, cluster: cluster)
expect(data).to include(
'has-managed-prometheus' => 'true'
)
end
end
context 'when prometheus integration is inactive' do
it 'disables alert management and hides managed prometheus' do
context 'when prometheus service is inactive' do
it 'disables alert management' do
prometheus_integration.update!(manual_configuration: false)
expect(data).to include(
'alert-management-enabled' => 'false'
)
expect(data).to include(
'has-managed-prometheus' => 'false'
)
end
end
end
context 'without prometheus integration' do
it "doesn't have managed prometheus" do
expect(data).to include(
'has-managed-prometheus' => 'false'
)
end
end
context 'with http integration' do
let_it_be(:integration) { create(:alert_management_http_integration, project: project) }

View File

@ -140,76 +140,63 @@ RSpec.describe Issues::BuildService do
end
describe '#execute' do
context 'as developer' do
it 'builds a new issues with given params' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
describe 'setting milestone' do
context 'when developer' do
it 'builds a new issues with given params' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
expect(issue.milestone).to eq(milestone)
expect(issue.issue_type).to eq('issue')
expect(issue.work_item_type.base_type).to eq('issue')
expect(issue.milestone).to eq(milestone)
end
it 'sets milestone to nil if it is not available for the project' do
milestone = create(:milestone, project: create(:project))
issue = build_issue(milestone_id: milestone.id)
expect(issue.milestone).to be_nil
end
end
it 'sets milestone to nil if it is not available for the project' do
milestone = create(:milestone, project: create(:project))
issue = build_issue(milestone_id: milestone.id)
context 'when guest' do
let(:user) { guest }
expect(issue.milestone).to be_nil
end
it 'cannot set milestone' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
context 'when issue_type is incident' do
it 'sets the correct issue type' do
issue = build_issue(issue_type: 'incident')
expect(issue.issue_type).to eq('incident')
expect(issue.work_item_type.base_type).to eq('incident')
expect(issue.milestone).to be_nil
end
end
end
context 'as guest' do
let(:user) { guest }
describe 'setting issue type' do
context 'with a corresponding WorkItem::Type' do
let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id }
let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id }
it 'cannot set milestone' do
milestone = create(:milestone, project: project)
issue = build_issue(milestone_id: milestone.id)
where(:issue_type, :current_user, :work_item_type_id, :resulting_issue_type) do
nil | ref(:guest) | ref(:type_issue_id) | 'issue'
'issue' | ref(:guest) | ref(:type_issue_id) | 'issue'
'incident' | ref(:guest) | ref(:type_incident_id) | 'incident'
# update once support for test_case is enabled
'test_case' | ref(:guest) | ref(:type_issue_id) | 'issue'
# update once support for requirement is enabled
'requirement' | ref(:guest) | ref(:type_issue_id) | 'issue'
'invalid' | ref(:guest) | ref(:type_issue_id) | 'issue'
# ensure that we don't set a value which has a permission check but is an invalid issue type
'project' | ref(:guest) | ref(:type_issue_id) | 'issue'
end
expect(issue.milestone).to be_nil
end
with_them do
let(:user) { current_user }
context 'setting issue type' do
shared_examples 'builds an issue' do
specify do
it 'builds an issue' do
issue = build_issue(issue_type: issue_type)
expect(issue.issue_type).to eq(resulting_issue_type)
expect(issue.work_item_type_id).to eq(work_item_type_id)
end
end
it 'cannot set invalid issue type' do
issue = build_issue(issue_type: 'project')
expect(issue).to be_issue
end
context 'with a corresponding WorkItem::Type' do
let_it_be(:type_issue_id) { WorkItem::Type.default_issue_type.id }
let_it_be(:type_incident_id) { WorkItem::Type.default_by_type(:incident).id }
where(:issue_type, :work_item_type_id, :resulting_issue_type) do
nil | ref(:type_issue_id) | 'issue'
'issue' | ref(:type_issue_id) | 'issue'
'incident' | ref(:type_incident_id) | 'incident'
'test_case' | ref(:type_issue_id) | 'issue' # update once support for test_case is enabled
'requirement' | ref(:type_issue_id) | 'issue' # update once support for requirement is enabled
'invalid' | ref(:type_issue_id) | 'issue'
end
with_them do
it_behaves_like 'builds an issue'
end
end
end
end
end