Files
grafana/public/app/features/alerting/AlertTabCtrl.ts
Dominik Prokop a55a272276 Routing NG: Replace Angular routing with react-router (#31463)
* Add router packages

* Get react app root work instead of Angular one

* Logger util

* Patch Angular routing ($routeProvider, $routeParamsProvider)

* Use react-router-dom history instead of separate dependency

* Add test routes

* Sidemenu - use Link instead of anchors

* Patch Angular $location service (stub)

* WIP: geting rid of $location provider from TimeSrv

* Intercept anchor clicks to use history under the hood

* Sync Redux location slice with history state

* Make login/logout work

* Debug routes for testing

* Make force login work

* Make sure query param change does not recreate page components

* Hide side menu in specified locations

* Make the dashboar route query parameters work, make panel edit menu work

* Enable more routes

* Fix side menu

* Handle view modes

* Disable playlist routes

* Make SafeDynamicImport work again

* Bring back router-debug

* Separate redux location sync from route rendering

* Refactor updateLocation to thunk and move force refresh(login) to it

* Fixing init dashboard issue

* Support switching between dashboards without an unmount of DashboardPage

* More fixes for init dashboard and panel edit

* More type fixes

* Moving angular location wrapper out of main LocationService, and fixing typescript issues

* Fixed last typescript errors

* LocationService: Move to runtime and  remove getLocationService and export singleston const instead (#31523)

* Moving location service implementation to runtime and removing get function and making it a package const singleton

* Added test that used locationService directly

* removed unused import

* AngularApp: Moving angular dependencies and the app boot out of the main app into it's own file  (#31525)

* Fixes angular panels by calling the monkey patch

* Moving angular stuff to to it's own files

* udpated

* Fixing clicking on divs and spans inside anchor

* Moving app notifications out of angular app and removing angular directive wrapper

* Moving search from angular to react and removing angular search wrapper

* Clean up, tried to remove the redux location wrapper but requires a big update for DashboardPage, so adding it back

* Moving AppWrapper to root to limit circular dependencies (app/core -> app/routing and back)

* Open and close search now works

* Hide sidemenu when in kiosk mode

* Restoring some keybindings like ESC key

* Removed kiosk events and simplified it, just handled through updating URL

* Fixing typescript errors

* Simplified GrafanaRouteComponentProps and renamed to ContainerProps

* renamed back

* Changed AlertRuleList to use GrafanaRouteComponentProps and location.search passed to it

* Removing the reloadOnSearch property, this is not needed now for react as react by default does not unmount components when only url match or query parmas change

* SafeDynamicImport causing unmount un every search update, not sure how to fix yet

* Fix signature for SafeDynamicImport so we do not create new route components on every route render

* Removing the redux location wrapper as it was causing errors, and making dashboard page work with RouteProps (location, match) etc

* Updating DashboardPage and SoloPanelPage to use match params and history location

* Fixed DashboardPage tests

* Fixing solo route tests

* LocationService: Rename getCurrentLocation to just getLocation

* do not intercept link clicks with target blank or self

* Experimental useUrlParams hook

* Update DataSourceSettingsPage to use router match params

* fix links with urls that have no starting / to work like before

* Fix forceLogin

* Add queryParams to GrafanaRouteComponentProps

* PanelEditor get rid of updateLocation and location state

* Improve grafana route query params typing

* Add getSearchObject to LocationService

* Use DashboardPAge queryParams instead of location.search parsing

* Fix DashboardPage typing

* Fix some tests weirdness

* Bring back KeyboardSrv

* Fixes typescript issues

* Team pages now use router match params

* Get rid of  from GrafanaRouteComponent props

* Removed unnessary calls to getSearchObject when calling locationService.partial

* Updated DashboardPage tests after queryParams was added

* Fixing dashboard settings back

* GrafanaRoute: Adding tests and remove use of global locationService

* Fixing tests and typescript errors

* Bring back kiosk modes and add tests

* Fix TimeSrv tests

* Fix typecheck errors

* Fixing tests

* Updated SideMenu test to react-testing and wrapped component in Router, and fixed issue importing createMemoryHistory

* Get rid of routeChange event from TimeSrv from

* Fixed TopSectionItem test

* Trying to make basename work but failing

* Update TopSectionItem snapshot

* Fix TopSectionItem snapshot test

* Fix API keys creation

* Remove Angular dependencies from KeybindingSrv (#31617)

* Remove Angular dependency from KeybindingsSrv

* Fix tests and typecheck issues

* basename is starting to work

* Make dashboard save work

* KeybindingSrv: Remove as angular service and no usage angular scope

* So long bridge_srv, we won't miss you

* Update snapshots

* Dashboard: Refactoring ChangeTracker to use History api and no angular (#31653)

* Dashboard: Refactoring ChangeTracker to use History api and no angular

* Updated

* Removed logging

* fixed unit tests

* updated snapshots

* Mechanism for force reloading routes (#31683)

* e2e: Fixes various things in e2e scenarios after router migration (#31685)

* Explore: Update reading query params from router props and updating location via locationService (ReactRouter)  (#31688)

* RoutingNG: Initial explore redux location to router location migration

* Updated explore Wrapper tests

* Fixing more tests

* remove loggin

* rename back to make naming consistent

* Fixing return to dashboard button

* fixing navigation to explore from dashboard

* updated routeProps

* Updated tests

* Make DashboardListPage work

* Fixing navigation after add new data source, and fixes explore e2e

* Fixing solo panel page

* PluginsPage now works

* RoutingNG: When parsing and rendering url search/query params preseve old logic of handling booleans and arrays (#31725)

* RoutingNG: When parsing and rendering url search/query params preserve old logic of handling booleans and arrays

* Fixed test

* Make snapshots list work

* fixed alert notification channel edit page

* Simplify LocationService, did not need special handling for login or forceLogin as target _self on link already handles that

* fixed UserAdminPage

* fixed edit orgs page

* Fixing LdapPage

* fixing dashboard import

* Fixed new folder page

* Fixed data source dashboards page

* fixing Folder permissions and folder settings page

* fixing snapshot list page nav model

* remove unused file

* Added placeholder page for playlist

* Moved browser compatability to index-template

* Restored 404/default page

* Fixed reset password page

* Fixed SignUpInvited page

* Fixing CreateTeam, Create user page, add panel widget

* Restore browwser file to make tests happy

* Fixed unit tests

* Removed unused import

* Replacing usage of updateLocation

* Fixed test

* Updating search filters to use history / location service for filters

* remove unused file

* AppRootPage fixed

* Fixing test and search issue

* Changes to support enterprise extensions

* remove console.log

* Removing more use of redux location

* Fixed signup page

* removed unused old angular controllers

* Fixing bugs

* one final bugfix

* Removed location from redux state

* Fixing ts issues and tests

* Fixing test issue

* fixing tests

* Fixing tests

* removed unused stuff

* Fixed search test

* Adding some doc comments

* Routing NG: Angular location provider patch (#31773)

* Patch Angulars $location provider

* Update public/app/angular/bridgeReactAngularRouting.ts

* Remove only test

* Update tests, disable loggers in test env

* Routing NG: remove $location provider usage (#31816)

* Remove dashboard_loaders

* Remove $location from Analytics service, track page views form GrafanaRoute

* Remove NotificationsEditCtrl

* Remove Angular dependencies from uploadDashboardDirective

* Update public/app/features/dashboard/containers/DashboardPage.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update public/app/features/dashboard/containers/DashboardPage.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Remove unused test helpers (#31831)

* Playlist react (#31829)

* playlist list in react

* Playlist start

* Things started to work

* Updated

* Handle empty list

* Fix ts

* Fixes and kiosk mode stuff

* Removed unused events

* fixing ts issue

* Another ts issue

* Fixing tests

Co-authored-by: Dominik Prokop <dominik.prokop@grafana.com>

* fixed test

* Update public/app/AppWrapper.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Update public/app/AppWrapper.tsx

Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>

* Remove Angular dependency from DashboardLoaderSrv (#31863)

Co-authored-by: Torkel Ödegaard <torkel@grafana.com>
Co-authored-by: Torkel Ödegaard <torkel@grafana.org>
Co-authored-by: Hugo Häggmark <hugo.haggmark@grafana.com>
Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>
2021-03-10 18:03:36 +01:00

518 lines
15 KiB
TypeScript

import _ from 'lodash';
import coreModule from 'app/core/core_module';
import { ThresholdMapper } from './state/ThresholdMapper';
import { QueryPart } from 'app/core/components/query_part/query_part';
import alertDef from './state/alertDef';
import config from 'app/core/config';
import appEvents from 'app/core/app_events';
import { getBackendSrv } from '@grafana/runtime';
import { DashboardSrv } from '../dashboard/services/DashboardSrv';
import DatasourceSrv from '../plugins/datasource_srv';
import { DataQuery, DataSourceApi, rangeUtil } from '@grafana/data';
import { PanelModel } from 'app/features/dashboard/state';
import { getDefaultCondition } from './getAlertingValidationMessage';
import { CoreEvents } from 'app/types';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
import { ShowConfirmModalEvent } from '../../types/events';
export class AlertTabCtrl {
panel: PanelModel;
panelCtrl: any;
subTabIndex: number;
conditionTypes: any;
alert: any;
conditionModels: any;
evalFunctions: any;
evalOperators: any;
noDataModes: any;
executionErrorModes: any;
addNotificationSegment: any;
notifications: any;
alertNotifications: any;
error: string;
appSubUrl: string;
alertHistory: any;
newAlertRuleTag: any;
alertingMinIntervalSecs: number;
alertingMinInterval: string;
frequencyWarning: any;
/** @ngInject */
constructor(
private $scope: any,
private dashboardSrv: DashboardSrv,
private uiSegmentSrv: any,
private datasourceSrv: DatasourceSrv
) {
this.panelCtrl = $scope.ctrl;
this.panel = this.panelCtrl.panel;
this.$scope.ctrl = this;
this.subTabIndex = 0;
this.evalFunctions = alertDef.evalFunctions;
this.evalOperators = alertDef.evalOperators;
this.conditionTypes = alertDef.conditionTypes;
this.noDataModes = alertDef.noDataModes;
this.executionErrorModes = alertDef.executionErrorModes;
this.appSubUrl = config.appSubUrl;
this.panelCtrl._enableAlert = this.enable;
this.alertingMinIntervalSecs = config.alertingMinInterval;
this.alertingMinInterval = rangeUtil.secondsToHms(config.alertingMinInterval);
}
$onInit() {
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
// subscribe to graph threshold handle changes
const thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
// set panel alert edit mode
this.$scope.$on('$destroy', () => {
this.panelCtrl.events.off(CoreEvents.thresholdChanged, thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
});
// build notification model
this.notifications = [];
this.alertNotifications = [];
this.alertHistory = [];
return promiseToDigest(this.$scope)(
getBackendSrv()
.get('/api/alert-notifications/lookup')
.then((res: any) => {
this.notifications = res;
this.initModel();
this.validateModel();
})
);
}
getAlertHistory() {
promiseToDigest(this.$scope)(
getBackendSrv()
.get(`/api/annotations?dashboardId=${this.panelCtrl.dashboard.id}&panelId=${this.panel.id}&limit=50&type=alert`)
.then((res: any) => {
this.alertHistory = _.map(res, (ah) => {
ah.time = this.dashboardSrv.getCurrent().formatDate(ah.time, 'MMM D, YYYY HH:mm:ss');
ah.stateModel = alertDef.getStateDisplayModel(ah.newState);
ah.info = alertDef.getAlertAnnotationInfo(ah);
return ah;
});
})
);
}
getNotificationIcon(type: string): string {
switch (type) {
case 'email':
return 'envelope';
case 'slack':
return 'slack';
case 'victorops':
return 'fa fa-pagelines';
case 'webhook':
return 'cube';
case 'pagerduty':
return 'fa fa-bullhorn';
case 'opsgenie':
return 'bell';
case 'hipchat':
return 'fa fa-mail-forward';
case 'pushover':
return 'mobile-android';
case 'kafka':
return 'arrow-random';
case 'teams':
return 'fa fa-windows';
}
return 'bell';
}
getNotifications() {
return Promise.resolve(
this.notifications.map((item: any) => {
return this.uiSegmentSrv.newSegment(item.name);
})
);
}
notificationAdded() {
const model: any = _.find(this.notifications, {
name: this.addNotificationSegment.value,
});
if (!model) {
return;
}
this.alertNotifications.push({
name: model.name,
iconClass: this.getNotificationIcon(model.type),
isDefault: false,
uid: model.uid,
});
// avoid duplicates using both id and uid to be backwards compatible.
if (!_.find(this.alert.notifications, (n) => n.id === model.id || n.uid === model.uid)) {
this.alert.notifications.push({ uid: model.uid });
}
// reset plus button
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
this.addNotificationSegment.fake = true;
}
removeNotification(an: any) {
// remove notifiers referred to by id and uid to support notifiers added
// before and after we added support for uid
_.remove(this.alert.notifications, (n: any) => n.uid === an.uid || n.id === an.id);
_.remove(this.alertNotifications, (n: any) => n.uid === an.uid || n.id === an.id);
}
addAlertRuleTag() {
if (this.newAlertRuleTag.name) {
this.alert.alertRuleTags[this.newAlertRuleTag.name] = this.newAlertRuleTag.value;
}
this.newAlertRuleTag.name = '';
this.newAlertRuleTag.value = '';
}
removeAlertRuleTag(tagName: string) {
delete this.alert.alertRuleTags[tagName];
}
initModel() {
const alert = (this.alert = this.panel.alert);
if (!alert) {
return;
}
this.checkFrequency();
alert.conditions = alert.conditions || [];
if (alert.conditions.length === 0) {
alert.conditions.push(getDefaultCondition());
}
alert.noDataState = alert.noDataState || config.alertingNoDataOrNullValues;
alert.executionErrorState = alert.executionErrorState || config.alertingErrorOrTimeout;
alert.frequency = alert.frequency || '1m';
alert.handler = alert.handler || 1;
alert.notifications = alert.notifications || [];
alert.for = alert.for || '0m';
alert.alertRuleTags = alert.alertRuleTags || {};
const defaultName = this.panel.title + ' alert';
alert.name = alert.name || defaultName;
this.conditionModels = _.reduce(
alert.conditions,
(memo, value) => {
memo.push(this.buildConditionModel(value));
return memo;
},
[] as string[]
);
ThresholdMapper.alertToGraphThresholds(this.panel);
for (const addedNotification of alert.notifications) {
let identifier = addedNotification.uid;
// lookup notifier type by uid
let model: any = _.find(this.notifications, { uid: identifier });
// fallback using id if uid is missing
if (!model && addedNotification.id) {
identifier = addedNotification.id;
model = _.find(this.notifications, { id: identifier });
}
if (!model) {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Notifier with invalid identifier is detected',
text: `Do you want to delete notifier with invalid identifier: ${identifier} from the dashboard JSON?`,
text2: 'After successful deletion, make sure to save the dashboard for storing the update JSON.',
icon: 'trash-alt',
confirmText: 'Delete',
yesText: 'Delete',
onConfirm: async () => {
this.removeNotification(addedNotification);
},
})
);
}
if (model && model.isDefault === false) {
model.iconClass = this.getNotificationIcon(model.type);
this.alertNotifications.push(model);
}
}
for (const notification of this.notifications) {
if (notification.isDefault) {
notification.iconClass = this.getNotificationIcon(notification.type);
this.alertNotifications.push(notification);
}
}
this.panelCtrl.editingThresholds = true;
this.panelCtrl.render();
}
checkFrequency() {
if (!this.alert.frequency) {
return;
}
this.frequencyWarning = '';
try {
const frequencySecs = rangeUtil.intervalToSeconds(this.alert.frequency);
if (frequencySecs < this.alertingMinIntervalSecs) {
this.frequencyWarning =
'A minimum evaluation interval of ' +
this.alertingMinInterval +
' have been configured in Grafana and will be used for this alert rule. ' +
'Please contact the administrator to configure a lower interval.';
}
} catch (err) {
this.frequencyWarning = err;
}
}
graphThresholdChanged(evt: any) {
for (const condition of this.alert.conditions) {
if (condition.type === 'query') {
condition.evaluator.params[evt.handleIndex] = evt.threshold.value;
this.evaluatorParamsChanged();
break;
}
}
}
validateModel() {
if (!this.alert) {
return;
}
let firstTarget;
let foundTarget: DataQuery | null = null;
const promises: Array<Promise<any>> = [];
for (const condition of this.alert.conditions) {
if (condition.type !== 'query') {
continue;
}
for (const target of this.panel.targets) {
if (!firstTarget) {
firstTarget = target;
}
if (condition.query.params[0] === target.refId) {
foundTarget = target;
break;
}
}
if (!foundTarget) {
if (firstTarget) {
condition.query.params[0] = firstTarget.refId;
foundTarget = firstTarget;
} else {
this.error = 'Could not find any metric queries';
return;
}
}
const datasourceName = foundTarget.datasource || this.panel.datasource;
promises.push(
this.datasourceSrv.get(datasourceName).then(
((foundTarget) => (ds: DataSourceApi) => {
if (!ds.meta.alerting) {
return Promise.reject('The datasource does not support alerting queries');
} else if (ds.targetContainsTemplate && ds.targetContainsTemplate(foundTarget)) {
return Promise.reject('Template variables are not supported in alert queries');
}
return Promise.resolve();
})(foundTarget)
)
);
}
Promise.all(promises).then(
() => {
this.error = '';
this.$scope.$apply();
},
(e) => {
this.error = e;
this.$scope.$apply();
}
);
}
buildConditionModel(source: any) {
const cm: any = { source: source, type: source.type };
cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef);
cm.reducerPart = alertDef.createReducerPart(source.reducer);
cm.evaluator = source.evaluator;
cm.operator = source.operator;
return cm;
}
handleQueryPartEvent(conditionModel: any, evt: any) {
switch (evt.name) {
case 'action-remove-part': {
break;
}
case 'get-part-actions': {
return Promise.resolve([]);
}
case 'part-param-changed': {
this.validateModel();
}
case 'get-param-options': {
const result = this.panel.targets.map((target) => {
return this.uiSegmentSrv.newSegment({ value: target.refId });
});
return Promise.resolve(result);
}
default: {
return Promise.resolve();
}
}
return Promise.resolve();
}
handleReducerPartEvent(conditionModel: any, evt: any) {
switch (evt.name) {
case 'action': {
conditionModel.source.reducer.type = evt.action.value;
conditionModel.reducerPart = alertDef.createReducerPart(conditionModel.source.reducer);
this.evaluatorParamsChanged();
break;
}
case 'get-part-actions': {
const result = [];
for (const type of alertDef.reducerTypes) {
if (type.value !== conditionModel.source.reducer.type) {
result.push(type);
}
}
return Promise.resolve(result);
}
}
return Promise.resolve();
}
addCondition(type: string) {
const condition = getDefaultCondition();
// add to persited model
this.alert.conditions.push(condition);
// add to view model
this.conditionModels.push(this.buildConditionModel(condition));
}
removeCondition(index: number) {
this.alert.conditions.splice(index, 1);
this.conditionModels.splice(index, 1);
}
delete() {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Delete Alert',
text: 'Are you sure you want to delete this alert rule?',
text2: 'You need to save dashboard for the delete to take effect',
icon: 'trash-alt',
yesText: 'Delete',
onConfirm: () => {
delete this.panel.alert;
this.alert = null;
this.panel.thresholds = [];
this.conditionModels = [];
this.panelCtrl.alertState = null;
this.panelCtrl.render();
},
})
);
}
enable = () => {
this.panel.alert = {};
this.initModel();
this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes
};
evaluatorParamsChanged() {
ThresholdMapper.alertToGraphThresholds(this.panel);
this.panelCtrl.render();
}
evaluatorTypeChanged(evaluator: any) {
// ensure params array is correct length
switch (evaluator.type) {
case 'lt':
case 'gt': {
evaluator.params = [evaluator.params[0]];
break;
}
case 'within_range':
case 'outside_range': {
evaluator.params = [evaluator.params[0], evaluator.params[1]];
break;
}
case 'no_value': {
evaluator.params = [];
}
}
this.evaluatorParamsChanged();
}
clearHistory() {
appEvents.publish(
new ShowConfirmModalEvent({
title: 'Delete Alert History',
text: 'Are you sure you want to remove all history & annotations for this alert?',
icon: 'trash-alt',
yesText: 'Yes',
onConfirm: () => {
promiseToDigest(this.$scope)(
getBackendSrv()
.post('/api/annotations/mass-delete', {
dashboardId: this.panelCtrl.dashboard.id,
panelId: this.panel.id,
})
.then(() => {
this.alertHistory = [];
this.panelCtrl.refresh();
})
);
},
})
);
}
}
/** @ngInject */
export function alertTab() {
'use strict';
return {
restrict: 'E',
scope: true,
templateUrl: 'public/app/features/alerting/partials/alert_tab.html',
controller: AlertTabCtrl,
};
}
coreModule.directive('alertTab', alertTab);