diff options
Diffstat (limited to 'app/assets/javascripts/monitoring/components/alert_widget.vue')
-rw-r--r-- | app/assets/javascripts/monitoring/components/alert_widget.vue | 286 |
1 files changed, 286 insertions, 0 deletions
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue new file mode 100644 index 00000000000..86a793c854e --- /dev/null +++ b/app/assets/javascripts/monitoring/components/alert_widget.vue @@ -0,0 +1,286 @@ +<script> +import { GlBadge, GlLoadingIcon, GlModalDirective, GlIcon, GlTooltip, GlSprintf } from '@gitlab/ui'; +import { s__ } from '~/locale'; +import createFlash from '~/flash'; +import AlertWidgetForm from './alert_widget_form.vue'; +import AlertsService from '../services/alerts_service'; +import { alertsValidator, queriesValidator } from '../validators'; +import { OPERATORS } from '../constants'; +import { values, get } from 'lodash'; + +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(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 }) { + const newAlert = { operator, threshold, prometheus_metric_id }; + 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 }) { + const updatedAlert = { operator, threshold }; + 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" /> + <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' : 'secondary'" + pill + class="d-flex-center text-truncate" + > + <gl-icon name="warning" :size="16" class="flex-shrink-0" /> + <span class="text-truncate gl-pl-1-deprecated-no-really-do-not-use-me"> + <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> |