mirror of https://github.com/grafana/grafana.git
Merge pull request #14707 from grafana/14388/alert-tab-ux-update
Alert tab ux update
This commit is contained in:
commit
2de57f095c
|
|
@ -24,12 +24,14 @@ class EmptyListCTA extends Component<Props, any> {
|
|||
<i className={buttonIcon} />
|
||||
{buttonTitle}
|
||||
</a>
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> ProTip: {proTip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
|
||||
{proTipLinkTitle}
|
||||
</a>
|
||||
</div>
|
||||
{proTip && (
|
||||
<div className="empty-list-cta__pro-tip">
|
||||
<i className="fa fa-rocket" /> ProTip: {proTip}
|
||||
<a className="text-link empty-list-cta__pro-tip-link" href={proTipLink} target={proTipTarget}>
|
||||
{proTipLinkTitle}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ export class AlertTabCtrl {
|
|||
this.noDataModes = alertDef.noDataModes;
|
||||
this.executionErrorModes = alertDef.executionErrorModes;
|
||||
this.appSubUrl = config.appSubUrl;
|
||||
this.panelCtrl._enableAlert = this.enable;
|
||||
}
|
||||
|
||||
$onInit() {
|
||||
|
|
@ -114,7 +115,7 @@ export class AlertTabCtrl {
|
|||
}
|
||||
|
||||
getNotifications() {
|
||||
return Promise.resolve(
|
||||
return this.$q.when(
|
||||
this.notifications.map(item => {
|
||||
return this.uiSegmentSrv.newSegment(item.name);
|
||||
})
|
||||
|
|
@ -147,6 +148,7 @@ export class AlertTabCtrl {
|
|||
// reset plus button
|
||||
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
|
||||
this.addNotificationSegment.html = this.uiSegmentSrv.newPlusButton().html;
|
||||
this.addNotificationSegment.fake = true;
|
||||
}
|
||||
|
||||
removeNotification(index) {
|
||||
|
|
@ -353,11 +355,11 @@ export class AlertTabCtrl {
|
|||
});
|
||||
}
|
||||
|
||||
enable() {
|
||||
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);
|
||||
|
|
|
|||
|
|
@ -1,191 +1,168 @@
|
|||
<div class="panel-option-section__body" ng-if="ctrl.alert">
|
||||
<div class="edit-tab-with-sidemenu">
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alertNotifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.changeTabIndex(2)">State history</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
</li>
|
||||
</ul>
|
||||
</aside>
|
||||
<div ng-if="ctrl.panel.alert">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Rule</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for"
|
||||
spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured
|
||||
threshold it
|
||||
will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert
|
||||
rule
|
||||
has
|
||||
been firing for more than For duration, it will change to Alerting and send alert
|
||||
notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="edit-tab-content">
|
||||
<div ng-if="ctrl.subTabIndex === 0">
|
||||
<div class="alert alert-error m-b-2" ng-show="ctrl.error">
|
||||
<i class="fa fa-warning"></i> {{ctrl.error}}
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">Conditions</h4>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index"
|
||||
property="conditionModel.operator.type" options="ctrl.evalOperators"
|
||||
custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9"
|
||||
part="conditionModel.reducerPart"
|
||||
handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart"
|
||||
handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions"
|
||||
custom="false" css-class="query-keyword"
|
||||
on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-hide="conditionModel.evaluator.params.length === 0"
|
||||
ng-model="conditionModel.evaluator.params[0]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
<label class="gf-form-label query-keyword"
|
||||
ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any"
|
||||
ng-if="conditionModel.evaluator.params.length === 2"
|
||||
ng-model="conditionModel.evaluator.params[1]"
|
||||
ng-change="ctrl.evaluatorParamsChanged()" />
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Alert Config</h5>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-6">Name</span>
|
||||
<input type="text" class="gf-form-input width-20" ng-model="ctrl.alert.name">
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-9">Evaluate every</span>
|
||||
<input class="gf-form-input max-width-6" type="text" ng-model="ctrl.alert.frequency">
|
||||
</div>
|
||||
<div class="gf-form max-width-11">
|
||||
<label class="gf-form-label width-5">For</label>
|
||||
<input type="text" class="gf-form-input max-width-6" ng-model="ctrl.alert.for" spellcheck='false' placeholder="5m">
|
||||
<info-popover mode="right-absolute">
|
||||
If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending.
|
||||
Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
|
||||
</info-popover>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<h5 class="section-heading">Conditions</h5>
|
||||
<div class="gf-form-inline" ng-repeat="conditionModel in ctrl.conditionModels">
|
||||
<div class="gf-form">
|
||||
<metric-segment-model css-class="query-keyword width-5" ng-if="$index" property="conditionModel.operator.type" options="ctrl.evalOperators" custom="false"></metric-segment-model>
|
||||
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part width-9" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
<span class="gf-form-label query-keyword">OF</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
|
||||
</query-part-editor>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment-model property="conditionModel.evaluator.type" options="ctrl.evalFunctions" custom="false" css-class="query-keyword" on-change="ctrl.evaluatorTypeChanged(conditionModel.evaluator)"></metric-segment-model>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
|
||||
<input class="gf-form-input max-width-9" type="number" step="any" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()">
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label">
|
||||
<a class="pointer" tabindex="1" ng-click="ctrl.removeCondition($index)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</a>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group">
|
||||
<h4 class="section-heading">No Data & Error Handling</h4>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If no data or all values are null</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState"
|
||||
ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<label class="gf-form-label dropdown">
|
||||
<a class="pointer dropdown-toggle" data-toggle="dropdown">
|
||||
<i class="fa fa-plus"></i>
|
||||
</a>
|
||||
<ul class="dropdown-menu" role="menu">
|
||||
<li ng-repeat="ct in ctrl.conditionTypes" role="menuitem">
|
||||
<a ng-click="ctrl.addCondition(ct.value);">{{ct.text}}</a>
|
||||
</li>
|
||||
</ul>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-15">If execution error or timeout</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState"
|
||||
ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If no data or all values are null</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.noDataState" ng-options="f.value as f.text for f in ctrl.noDataModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-18">If execution error or timeout</span>
|
||||
<span class="gf-form-label query-keyword">SET STATE TO</span>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input" ng-model="ctrl.alert.executionErrorState" ng-options="f.value as f.text for f in ctrl.executionErrorModes">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row">
|
||||
<button class="btn btn-inverse" ng-click="ctrl.test()">
|
||||
Test Rule
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testing">
|
||||
Evaluating rule <i class="fa fa-spinner fa-spin"></i>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.testResult">
|
||||
<json-tree root-name="result" object="ctrl.testResult" start-expanded="true"></json-tree>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" ng-if="ctrl.subTabIndex === 1">
|
||||
<h5 class="section-heading">Notifications</h5>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form max-width-30">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
<span class="gf-form-label" ng-repeat="nc in ctrl.alertNotifications" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
<metric-segment segment="ctrl.addNotificationSegment" get-options="ctrl.getNotifications()" on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message" placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group" style="max-width: 720px;" ng-if="ctrl.subTabIndex === 2">
|
||||
<button class="btn btn-mini btn-danger pull-right" ng-click="ctrl.clearHistory()"><i class="fa fa-trash"></i> Clear history</button>
|
||||
<h5 class="section-heading" style="whitespace: nowrap">
|
||||
State history <span class="muted small">(last 50 state changes)</span>
|
||||
</h5>
|
||||
|
||||
<div ng-show="ctrl.alertHistory.length === 0">
|
||||
<br>
|
||||
<i>No state changes recorded</i>
|
||||
</div>
|
||||
|
||||
<ol class="alert-rule-list" >
|
||||
<li class="alert-rule-item" ng-repeat="al in ctrl.alertHistory">
|
||||
<div class="alert-rule-item__icon {{al.stateModel.stateClass}}">
|
||||
<i class="{{al.stateModel.iconClass}}"></i>
|
||||
</div>
|
||||
<div class="alert-rule-item__body">
|
||||
<div class="alert-rule-item__header">
|
||||
<div class="alert-rule-item__text">
|
||||
<span class="{{al.stateModel.stateClass}}">{{al.stateModel.text}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="alert-list-info">{{al.info}}</span>
|
||||
</div>
|
||||
<div class="alert-rule-item__time">
|
||||
<span>{{al.time}}</span>
|
||||
</div>
|
||||
</li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-group p-t-4 p-b-4" ng-if="!ctrl.alert">
|
||||
<div class="empty-list-cta">
|
||||
<div class="empty-list-cta__title">Panel has no alert rule defined</div>
|
||||
<button class="empty-list-cta__button btn btn-xlarge btn-success" ng-click="ctrl.enable()">
|
||||
<i class="icon-gf icon-gf-alert"></i>
|
||||
Create Alert
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="panel-option-section">
|
||||
<div class="panel-option-section__header">Notifications</div>
|
||||
<div class="panel-option-section__body">
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Send to</span>
|
||||
</div>
|
||||
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
|
||||
<span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
|
||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
||||
</span>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<metric-segment segment="ctrl.addNotificationSegment"
|
||||
get-options="ctrl.getNotifications()"
|
||||
on-change="ctrl.notificationAdded()"></metric-segment>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form gf-form--v-stretch">
|
||||
<span class="gf-form-label width-8">Message</span>
|
||||
<textarea class="gf-form-input" rows="10" ng-model="ctrl.alert.message"
|
||||
placeholder="Notification message details..."></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,20 +1,30 @@
|
|||
// Libraries
|
||||
import React, { PureComponent } from 'react';
|
||||
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
// Services & Utils
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import appEvents from 'app/core/app_events';
|
||||
|
||||
// Components
|
||||
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
||||
import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA';
|
||||
import StateHistory from './StateHistory';
|
||||
import 'app/features/alerting/AlertTabCtrl';
|
||||
|
||||
// Types
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { PanelModel } from '../panel_model';
|
||||
|
||||
interface Props {
|
||||
angularPanel?: AngularComponent;
|
||||
dashboard: DashboardModel;
|
||||
panel: PanelModel;
|
||||
}
|
||||
|
||||
export class AlertTab extends PureComponent<Props> {
|
||||
element: any;
|
||||
component: AngularComponent;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
}
|
||||
panelCtrl: any;
|
||||
|
||||
componentDidMount() {
|
||||
if (this.shouldLoadAlertTab()) {
|
||||
|
|
@ -29,7 +39,7 @@ export class AlertTab extends PureComponent<Props> {
|
|||
}
|
||||
|
||||
shouldLoadAlertTab() {
|
||||
return this.props.angularPanel && this.element;
|
||||
return this.props.angularPanel && this.element && !this.component;
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
|
@ -51,21 +61,80 @@ export class AlertTab extends PureComponent<Props> {
|
|||
return;
|
||||
}
|
||||
|
||||
const panelCtrl = scope.$$childHead.ctrl;
|
||||
this.panelCtrl = scope.$$childHead.ctrl;
|
||||
const loader = getAngularLoader();
|
||||
const template = '<alert-tab />';
|
||||
|
||||
const scopeProps = {
|
||||
ctrl: panelCtrl,
|
||||
ctrl: this.panelCtrl,
|
||||
};
|
||||
|
||||
this.component = loader.load(this.element, scopeProps, template);
|
||||
}
|
||||
|
||||
stateHistory = (): EditorToolbarView => {
|
||||
return {
|
||||
title: 'State history',
|
||||
render: () => {
|
||||
return (
|
||||
<StateHistory
|
||||
dashboard={this.props.dashboard}
|
||||
panelId={this.props.panel.id}
|
||||
onRefresh={this.panelCtrl.refresh}
|
||||
/>
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
deleteAlert = (): EditorToolbarView => {
|
||||
const { panel } = this.props;
|
||||
return {
|
||||
title: 'Delete',
|
||||
btnType: 'danger',
|
||||
onClick: () => {
|
||||
appEvents.emit('confirm-modal', {
|
||||
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: 'fa-trash',
|
||||
yesText: 'Delete',
|
||||
onConfirm: () => {
|
||||
delete panel.alert;
|
||||
panel.thresholds = [];
|
||||
this.panelCtrl.alertState = null;
|
||||
this.panelCtrl.render();
|
||||
this.forceUpdate();
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
onAddAlert = () => {
|
||||
this.panelCtrl._enableAlert();
|
||||
this.component.digest();
|
||||
this.forceUpdate();
|
||||
};
|
||||
|
||||
render() {
|
||||
const { alert } = this.props.panel;
|
||||
|
||||
const toolbarItems = alert ? [this.stateHistory(), this.deleteAlert()] : [];
|
||||
|
||||
const model = {
|
||||
title: 'Panel has no alert rule defined',
|
||||
icon: 'icon-gf icon-gf-alert',
|
||||
onClick: this.onAddAlert,
|
||||
buttonTitle: 'Create Alert',
|
||||
};
|
||||
|
||||
return (
|
||||
<EditorTabBody heading="Alert" toolbarItems={[]}>
|
||||
<div ref={element => (this.element = element)} />
|
||||
<EditorTabBody heading="Alert" toolbarItems={toolbarItems}>
|
||||
<>
|
||||
<div ref={element => (this.element = element)} />
|
||||
{!alert && <EmptyListCTA model={model} />}
|
||||
</>
|
||||
</EditorTabBody>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,21 +10,22 @@ interface Props {
|
|||
children: JSX.Element;
|
||||
heading: string;
|
||||
renderToolbar?: () => JSX.Element;
|
||||
toolbarItems?: EditorToolBarView[];
|
||||
toolbarItems?: EditorToolbarView[];
|
||||
}
|
||||
|
||||
export interface EditorToolBarView {
|
||||
export interface EditorToolbarView {
|
||||
title?: string;
|
||||
heading?: string;
|
||||
imgSrc?: string;
|
||||
icon?: string;
|
||||
disabled?: boolean;
|
||||
onClick?: () => void;
|
||||
render: (closeFunction?: any) => JSX.Element | JSX.Element[];
|
||||
render?: () => JSX.Element;
|
||||
action?: () => void;
|
||||
btnType?: 'danger';
|
||||
}
|
||||
|
||||
interface State {
|
||||
openView?: EditorToolBarView;
|
||||
openView?: EditorToolbarView;
|
||||
isOpen: boolean;
|
||||
fadeIn: boolean;
|
||||
}
|
||||
|
|
@ -48,7 +49,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||
this.setState({ fadeIn: true });
|
||||
}
|
||||
|
||||
onToggleToolBarView = (item: EditorToolBarView) => {
|
||||
onToggleToolBarView = (item: EditorToolbarView) => {
|
||||
this.setState({
|
||||
openView: item,
|
||||
isOpen: !this.state.isOpen,
|
||||
|
|
@ -74,12 +75,15 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||
return state;
|
||||
}
|
||||
|
||||
renderButton(view: EditorToolBarView) {
|
||||
renderButton(view: EditorToolbarView) {
|
||||
const onClick = () => {
|
||||
if (view.onClick) {
|
||||
view.onClick();
|
||||
}
|
||||
this.onToggleToolBarView(view);
|
||||
|
||||
if (view.render) {
|
||||
this.onToggleToolBarView(view);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -91,7 +95,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
|||
);
|
||||
}
|
||||
|
||||
renderOpenView(view: EditorToolBarView) {
|
||||
renderOpenView(view: EditorToolbarView) {
|
||||
return (
|
||||
<PanelOptionSection title={view.title || view.heading} onClose={this.onCloseOpenView}>
|
||||
{view.render()}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export class PanelEditor extends PureComponent<PanelEditorProps> {
|
|||
case 'queries':
|
||||
return <QueriesTab panel={panel} dashboard={dashboard} />;
|
||||
case 'alert':
|
||||
return <AlertTab angularPanel={angularPanel} />;
|
||||
return <AlertTab angularPanel={angularPanel} dashboard={dashboard} panel={panel} />;
|
||||
case 'visualization':
|
||||
return (
|
||||
<VisualizationTab
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Libraries
|
||||
import React, { SFC, PureComponent } from 'react';
|
||||
import React, { PureComponent, SFC } from 'react';
|
||||
import _ from 'lodash';
|
||||
|
||||
// Components
|
||||
import './../../panel/metrics_tab';
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import 'app/features/panel/metrics_tab';
|
||||
import { EditorTabBody, EditorToolbarView} from './EditorTabBody';
|
||||
import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker';
|
||||
import { QueryInspector } from './QueryInspector';
|
||||
import { QueryOptions } from './QueryOptions';
|
||||
|
|
@ -13,14 +13,14 @@ import { PanelOptionSection } from './PanelOptionSection';
|
|||
|
||||
// Services
|
||||
import { getDatasourceSrv } from 'app/features/plugins/datasource_srv';
|
||||
import { getBackendSrv, BackendSrv } from 'app/core/services/backend_srv';
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { BackendSrv, getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
import config from 'app/core/config';
|
||||
|
||||
// Types
|
||||
import { PanelModel } from '../panel_model';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import { DataSourceSelectItem, DataQuery } from 'app/types';
|
||||
import { DataQuery, DataSourceSelectItem } from 'app/types';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -204,12 +204,12 @@ export class QueriesTab extends PureComponent<Props, State> {
|
|||
const { panel } = this.props;
|
||||
const { currentDS, isAddingMixed } = this.state;
|
||||
|
||||
const queryInspector = {
|
||||
const queryInspector: EditorToolbarView = {
|
||||
title: 'Query Inspector',
|
||||
render: this.renderQueryInspector,
|
||||
};
|
||||
|
||||
const dsHelp = {
|
||||
const dsHelp: EditorToolbarView = {
|
||||
heading: 'Help',
|
||||
icon: 'fa fa-question',
|
||||
render: this.renderHelp,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,110 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
import alertDef from '../../alerting/state/alertDef';
|
||||
import { getBackendSrv } from 'app/core/services/backend_srv';
|
||||
import { DashboardModel } from '../dashboard_model';
|
||||
import appEvents from '../../../core/app_events';
|
||||
|
||||
interface Props {
|
||||
dashboard: DashboardModel;
|
||||
panelId: number;
|
||||
onRefresh: () => void;
|
||||
}
|
||||
|
||||
interface State {
|
||||
stateHistoryItems: any[];
|
||||
}
|
||||
|
||||
class StateHistory extends PureComponent<Props, State> {
|
||||
state = {
|
||||
stateHistoryItems: [],
|
||||
};
|
||||
|
||||
componentDidMount(): void {
|
||||
const { dashboard, panelId } = this.props;
|
||||
|
||||
getBackendSrv()
|
||||
.get(`/api/annotations?dashboardId=${dashboard.id}&panelId=${panelId}&limit=50&type=alert`)
|
||||
.then(res => {
|
||||
const items = res.map(item => {
|
||||
return {
|
||||
stateModel: alertDef.getStateDisplayModel(item.newState),
|
||||
time: dashboard.formatDate(item.time, 'MMM D, YYYY HH:mm:ss'),
|
||||
info: alertDef.getAlertAnnotationInfo(item),
|
||||
};
|
||||
});
|
||||
|
||||
this.setState({
|
||||
stateHistoryItems: items,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
clearHistory = () => {
|
||||
const { dashboard, onRefresh, panelId } = this.props;
|
||||
|
||||
appEvents.emit('confirm-modal', {
|
||||
title: 'Delete Alert History',
|
||||
text: 'Are you sure you want to remove all history & annotations for this alert?',
|
||||
icon: 'fa-trash',
|
||||
yesText: 'Yes',
|
||||
onConfirm: () => {
|
||||
getBackendSrv()
|
||||
.post('/api/annotations/mass-delete', {
|
||||
dashboardId: dashboard.id,
|
||||
panelId: panelId,
|
||||
})
|
||||
.then(() => {
|
||||
onRefresh();
|
||||
});
|
||||
|
||||
this.setState({
|
||||
stateHistoryItems: [],
|
||||
});
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const { stateHistoryItems } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
{stateHistoryItems.length > 0 && (
|
||||
<div className="p-b-1">
|
||||
<span className="muted">Last 50 state changes</span>
|
||||
<button className="btn btn-mini btn-danger pull-right" onClick={this.clearHistory}>
|
||||
<i className="fa fa-trash" /> {` Clear history`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<ol className="alert-rule-list">
|
||||
{stateHistoryItems.length > 0 ? (
|
||||
stateHistoryItems.map((item, index) => {
|
||||
return (
|
||||
<li className="alert-rule-item" key={`${item.time}-${index}`}>
|
||||
<div className={`alert-rule-item__icon ${item.stateModel.stateClass}`}>
|
||||
<i className={item.stateModel.iconClass} />
|
||||
</div>
|
||||
<div className="alert-rule-item__body">
|
||||
<div className="alert-rule-item__header">
|
||||
<p className="alert-rule-item__name">{item.alertName}</p>
|
||||
<div className="alert-rule-item__text">
|
||||
<span className={`${item.stateModel.stateClass}`}>{item.stateModel.text}</span>
|
||||
</div>
|
||||
</div>
|
||||
{item.info}
|
||||
</div>
|
||||
<div className="alert-rule-item__time">{item.time}</div>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<i>No state changes recorded</i>
|
||||
)}
|
||||
</ol>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default StateHistory;
|
||||
|
|
@ -2,10 +2,10 @@
|
|||
import React, { PureComponent } from 'react';
|
||||
|
||||
// Utils & Services
|
||||
import { getAngularLoader, AngularComponent } from 'app/core/services/AngularLoader';
|
||||
import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader';
|
||||
|
||||
// Components
|
||||
import { EditorTabBody } from './EditorTabBody';
|
||||
import { EditorTabBody, EditorToolbarView } from './EditorTabBody';
|
||||
import { VizTypePicker } from './VizTypePicker';
|
||||
import { PluginHelp } from 'app/core/components/PluginHelp/PluginHelp';
|
||||
import { FadeIn } from 'app/core/components/Animations/FadeIn';
|
||||
|
|
@ -206,7 +206,7 @@ export class VisualizationTab extends PureComponent<Props, State> {
|
|||
const { plugin } = this.props;
|
||||
const { isVizPickerOpen, searchQuery } = this.state;
|
||||
|
||||
const pluginHelp = {
|
||||
const pluginHelp: EditorToolbarView = {
|
||||
heading: 'Help',
|
||||
icon: 'fa fa-question',
|
||||
render: this.renderHelp,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,7 @@
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue