Add latest changes from gitlab-org/gitlab@master
This commit is contained in:
parent
2d31f347bf
commit
da5d2aa34f
|
@ -106,6 +106,7 @@ RSpec/MultipleMemoizedHelpers:
|
|||
|
||||
Naming/FileName:
|
||||
ExpectMatchingDefinition: true
|
||||
CheckDefinitionPathHierarchy: false
|
||||
Exclude:
|
||||
- '**/*/*.builder'
|
||||
- 'ee/bin/*'
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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? ||
|
||||
|
|
|
@ -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
|
|
@ -114,3 +114,5 @@ Sidekiq.configure_client do |config|
|
|||
|
||||
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
|
||||
end
|
||||
|
||||
Sidekiq::Client.prepend Gitlab::Patch::SidekiqClient
|
||||
|
|
|
@ -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. |
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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*) | | | | | |
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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
|
|
@ -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 ""
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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 > 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 > 42
|
||||
</span>
|
||||
</gl-badge-stub>
|
||||
`;
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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"
|
||||
>
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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);
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -28,7 +28,6 @@ describe('dashboard invalid url parameters', () => {
|
|||
},
|
||||
},
|
||||
options,
|
||||
provide: { hasManagedPrometheus: false },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,7 +31,6 @@ describe('dashboard invalid url parameters', () => {
|
|||
store,
|
||||
stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
|
||||
...options,
|
||||
provide: { hasManagedPrometheus: false },
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -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',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -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
|
||||
|
|
|
@ -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) }
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue