Commit e76d5813 authored by Tristan Read's avatar Tristan Read Committed by Enrique Alcantara

Remove managed cluster alerts feature flag

Removes the feature flag and associated unused code
of the metrics alert feature for GitLab managed clusters

Changelog: other
parent 52715b71
......@@ -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) {
......
<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>
<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: {
......
<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? ||
......
---
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
......@@ -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 ""
......
# frozen_string_literal: true
module QA
module EE
module Page
module Project
module Monitor
module Metrics
module Show
extend QA::Page::PageConcern
EXPECTED_LABEL = 'Total (GB)'
def self.prepended(base)
super
base.class_eval do
view 'app/assets/javascripts/monitoring/components/alert_widget_form.vue' do
element :alert_query_dropdown
element :alert_query_option
element :alert_threshold_field
end
end
end
def wait_for_alert(operator = '>', threshold = 0)
wait_until(reload: false) { has_alert?(operator, threshold) }
end
def has_alert?(operator = '>', threshold = 0)
within_element :prometheus_graphs do
has_text?([EXPECTED_LABEL, operator, threshold].join(' '))
end
end
def write_first_alert(operator = '>', threshold = 0)
open_first_alert_modal
click_on operator
fill_element :alert_threshold_field, threshold
within('.modal-content') { click_button(class: 'btn-success') }
end
def delete_first_alert
open_first_alert_modal
within('.modal-content') { click_button(class: 'btn-danger') }
wait_for_requests
end
def open_first_alert_modal
all_elements(:prometheus_widgets_dropdown, minimum: 1).first.click
click_element :alert_widget_menu_item
click_element :alert_query_dropdown unless has_element?(:alert_query_option, wait: 3)
all_elements(:alert_query_option, minimum: 1).first.click
end
end
end
end
end
end
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
......
......@@ -11,18 +11,6 @@ module QA
@project.visit!
end
it 'allows configuration of alerts', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1337' do
Page::Project::Menu.perform(&:go_to_monitor_metrics)
Page::Project::Monitor::Metrics::Show.perform do |on_dashboard|
verify_metrics(on_dashboard)
verify_add_alert(on_dashboard)
verify_edit_alert(on_dashboard)
verify_persist_alert(on_dashboard)
verify_delete_alert(on_dashboard)
end
end
it 'creates an incident template and opens an incident with template applied', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1262' do
create_incident_template
......@@ -49,33 +37,6 @@ module QA
on_dashboard.wait_for_metrics
expect(on_dashboard).to have_metrics
expect(on_dashboard).not_to have_alert
end
def verify_add_alert(on_dashboard)
on_dashboard.write_first_alert('>', 0)
expect(on_dashboard).to have_alert
end
def verify_edit_alert(on_dashboard)
on_dashboard.write_first_alert('<', 0)
expect(on_dashboard).to have_alert('<')
end
def verify_persist_alert(on_dashboard)
on_dashboard.refresh
on_dashboard.wait_for_metrics
on_dashboard.wait_for_alert('<')
expect(on_dashboard).to have_alert('<')
end
def verify_delete_alert(on_dashboard)
on_dashboard.delete_first_alert
expect(on_dashboard).not_to have_alert('<')
end
def create_incident_template
......
......@@ -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({
......
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AlertWidget Alert firing displays a warning icon and matches snapshot 1`] = `
<gl-badge-stub
class="d-flex-center text-truncate"
size="md"
variant="danger"
>
<gl-icon-stub
class="flex-shrink-0"
name="warning"
size="16"
/>
<span
class="text-truncate gl-pl-2"
>
Firing:
alert-label &gt; 42
</span>
</gl-badge-stub>
`;
exports[`AlertWidget Alert not firing displays a warning icon and matches snapshot 1`] = `
<gl-badge-stub
class="d-flex-center text-truncate"
size="md"
variant="neutral"
>
<gl-icon-stub
class="flex-shrink-0"
name="warning"
size="16"
/>
<span
class="text-truncate gl-pl-2"
>
alert-label &gt; 42
</span>
</gl-badge-stub>
`;
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"
>
......
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,
});
};
......
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,50 +44,24 @@ 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
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment