1 import { Component } from '@angular/core';
2 import { Validators } from '@angular/forms';
3 import { ActivatedRoute, Router } from '@angular/router';
5 import _ from 'lodash';
6 import moment from 'moment';
8 import { DashboardNotFoundError } from '~/app/core/error/error';
9 import { PrometheusService } from '~/app/shared/api/prometheus.service';
10 import { ActionLabelsI18n, SucceededActionLabelsI18n } from '~/app/shared/constants/app.constants';
11 import { Icons } from '~/app/shared/enum/icons.enum';
12 import { NotificationType } from '~/app/shared/enum/notification-type.enum';
13 import { CdFormBuilder } from '~/app/shared/forms/cd-form-builder';
14 import { CdFormGroup } from '~/app/shared/forms/cd-form-group';
15 import { CdValidators } from '~/app/shared/forms/cd-validators';
18 AlertmanagerSilenceMatcher,
19 AlertmanagerSilenceMatcherMatch
20 } from '~/app/shared/models/alertmanager-silence';
21 import { Permission } from '~/app/shared/models/permissions';
22 import { AlertmanagerAlert, PrometheusRule } from '~/app/shared/models/prometheus-alerts';
23 import { AuthStorageService } from '~/app/shared/services/auth-storage.service';
24 import { ModalService } from '~/app/shared/services/modal.service';
25 import { NotificationService } from '~/app/shared/services/notification.service';
26 import { PrometheusSilenceMatcherService } from '~/app/shared/services/prometheus-silence-matcher.service';
27 import { TimeDiffService } from '~/app/shared/services/time-diff.service';
28 import { SilenceMatcherModalComponent } from '../silence-matcher-modal/silence-matcher-modal.component';
31 selector: 'cd-prometheus-form',
32 templateUrl: './silence-form.component.html',
33 styleUrls: ['./silence-form.component.scss']
35 export class SilenceFormComponent {
37 permission: Permission;
39 rules: PrometheusRule[];
48 resource = $localize`silence`;
50 matchers: AlertmanagerSilenceMatcher[] = [];
51 matcherMatch: AlertmanagerSilenceMatcherMatch = undefined;
54 tooltip: $localize`Attribute name`,
55 icon: this.icons.paragraph,
59 tooltip: $localize`Value`,
60 icon: this.icons.terminal,
64 tooltip: $localize`Regular expression`,
65 icon: this.icons.magic,
70 datetimeFormat = 'YYYY-MM-DD HH:mm';
74 private router: Router,
75 private authStorageService: AuthStorageService,
76 private formBuilder: CdFormBuilder,
77 private prometheusService: PrometheusService,
78 private notificationService: NotificationService,
79 private route: ActivatedRoute,
80 private timeDiff: TimeDiffService,
81 private modalService: ModalService,
82 private silenceMatcher: PrometheusSilenceMatcherService,
83 private actionLabels: ActionLabelsI18n,
84 private succeededLabels: SucceededActionLabelsI18n
97 private chooseMode() {
98 this.edit = this.router.url.startsWith('/monitoring/silences/edit');
99 this.recreate = this.router.url.startsWith('/monitoring/silences/recreate');
101 this.action = this.actionLabels.EDIT;
102 } else if (this.recreate) {
103 this.action = this.actionLabels.RECREATE;
105 this.action = this.actionLabels.CREATE;
109 private authenticate() {
110 this.permission = this.authStorageService.getPermissions().prometheus;
112 this.permission.read && (this.edit ? this.permission.update : this.permission.create);
114 throw new DashboardNotFoundError();
118 private createForm() {
119 const formatValidator = CdValidators.custom('format', (expiresAt: string) => {
120 const result = expiresAt === '' || moment(expiresAt, this.datetimeFormat).isValid();
123 this.form = this.formBuilder.group(
125 startsAt: ['', [Validators.required, formatValidator]],
126 duration: ['2h', [Validators.min(1)]],
127 endsAt: ['', [Validators.required, formatValidator]],
128 createdBy: [this.authStorageService.getUsername(), [Validators.required]],
129 comment: [null, [Validators.required]]
132 validators: CdValidators.custom('matcherRequired', () => this.matchers.length === 0)
137 private setupDates() {
138 const now = moment().format(this.datetimeFormat);
139 this.form.silentSet('startsAt', now);
141 this.subscribeDateChanges();
144 private updateDate(updateStartDate?: boolean) {
146 this.form.getValue(updateStartDate ? 'endsAt' : 'startsAt'),
149 const next = this.timeDiff.calculateDate(date, this.form.getValue('duration'), updateStartDate);
151 const nextDate = moment(next).format(this.datetimeFormat);
152 this.form.silentSet(updateStartDate ? 'startsAt' : 'endsAt', nextDate);
156 private subscribeDateChanges() {
157 this.form.get('startsAt').valueChanges.subscribe(() => {
160 this.form.get('duration').valueChanges.subscribe(() => {
163 this.form.get('endsAt').valueChanges.subscribe(() => {
164 this.onDateChange(true);
168 private onDateChange(updateStartDate?: boolean) {
169 const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat);
170 const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat);
171 if (startsAt.isBefore(endsAt)) {
172 this.updateDuration();
174 this.updateDate(updateStartDate);
178 private updateDuration() {
179 const startsAt = moment(this.form.getValue('startsAt'), this.datetimeFormat).toDate();
180 const endsAt = moment(this.form.getValue('endsAt'), this.datetimeFormat).toDate();
181 this.form.silentSet('duration', this.timeDiff.calculateDuration(startsAt, endsAt));
186 this.getModeSpecificData();
190 this.prometheusService.ifPrometheusConfigured(
192 this.prometheusService.getRules().subscribe(
194 this.rules = groups['groups'].reduce(
195 (acc, group) => _.concat<PrometheusRule>(acc, group.rules),
200 this.prometheusService.disablePrometheusConfig();
206 this.notificationService.show(
207 NotificationType.info,
208 $localize`Please add your Prometheus host to the dashboard configuration and refresh the page`,
218 private getModeSpecificData() {
219 this.route.params.subscribe((params: { id: string }) => {
223 if (this.edit || this.recreate) {
224 this.prometheusService.getSilences().subscribe((silences) => {
225 const silence = _.find(silences, ['id', params.id]);
226 if (!_.isUndefined(silence)) {
227 this.fillFormWithSilence(silence);
231 this.prometheusService.getAlerts().subscribe((alerts) => {
232 const alert = _.find(alerts, ['fingerprint', params.id]);
233 if (!_.isUndefined(alert)) {
234 this.fillFormByAlert(alert);
241 private fillFormWithSilence(silence: AlertmanagerSilence) {
242 this.id = silence.id;
244 ['startsAt', 'endsAt'].forEach((attr) =>
245 this.form.silentSet(attr, moment(silence[attr]).format(this.datetimeFormat))
247 this.updateDuration();
249 ['createdBy', 'comment'].forEach((attr) => this.form.silentSet(attr, silence[attr]));
250 this.matchers = silence.matchers;
251 this.validateMatchers();
254 private validateMatchers() {
256 window.setTimeout(() => this.validateMatchers(), 100);
259 this.matcherMatch = this.silenceMatcher.multiMatch(this.matchers, this.rules);
260 this.form.markAsDirty();
261 this.form.updateValueAndValidity();
264 private fillFormByAlert(alert: AlertmanagerAlert) {
265 const labels = alert.labels;
268 value: labels.alertname,
273 private setMatcher(matcher: AlertmanagerSilenceMatcher, index?: number) {
274 if (_.isNumber(index)) {
275 this.matchers[index] = matcher;
277 this.matchers.push(matcher);
279 this.validateMatchers();
282 showMatcherModal(index?: number) {
283 const modalRef = this.modalService.show(SilenceMatcherModalComponent);
284 const modalComponent = modalRef.componentInstance as SilenceMatcherModalComponent;
285 modalComponent.rules = this.rules;
286 if (_.isNumber(index)) {
287 modalComponent.editMode = true;
288 modalComponent.preFillControls(this.matchers[index]);
290 modalComponent.submitAction.subscribe((matcher: AlertmanagerSilenceMatcher) => {
291 this.setMatcher(matcher, index);
295 deleteMatcher(index: number) {
296 this.matchers.splice(index, 1);
297 this.validateMatchers();
301 if (this.form.invalid) {
304 this.prometheusService.setSilence(this.getSubmitData()).subscribe(
307 data.silenceId = resp.body['silenceId'];
309 if (this.isNavigate) {
310 this.router.navigate(['/monitoring/silences']);
312 this.notificationService.show(
313 NotificationType.success,
314 this.getNotificationTile(this.matchers),
321 () => this.form.setErrors({ cdSubmitButton: true })
325 private getSubmitData(): AlertmanagerSilence {
326 const payload = this.form.value;
327 delete payload.duration;
328 payload.startsAt = moment(payload.startsAt, this.datetimeFormat).toISOString();
329 payload.endsAt = moment(payload.endsAt, this.datetimeFormat).toISOString();
330 payload.matchers = this.matchers;
332 payload.id = this.id;
337 private getNotificationTile(matchers: AlertmanagerSilenceMatcher[]) {
340 action = this.succeededLabels.EDITED;
341 } else if (this.recreate) {
342 action = this.succeededLabels.RECREATED;
344 action = this.succeededLabels.CREATED;
347 for (const matcher of matchers) {
348 msg = msg.concat(` ${matcher.name} - ${matcher.value},`);
350 return `${action} ${this.resource} for ${msg.slice(0, -1)}`;
353 createSilenceFromNotification(data: any) {
354 this.isNavigate = false;
357 value: data['title'].split(' ')[0],
361 this.form.get('comment').setValue('Silence created from the alert notification');