Commit 7c339f07 by Torkel Ödegaard

feat(alerting): show alertin state in panel header, closes #6136

parent 2c4524bb
......@@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) {
}
}
func GetAlertStatesForDashboard(c *middleware.Context) Response {
dashboardId := c.QueryInt64("dashboardId")
if dashboardId == 0 {
return ApiError(400, "Missing query parameter dashboardId", nil)
}
query := models.GetAlertStatesForDashboardQuery{
OrgId: c.OrgId,
DashboardId: c.QueryInt64("dashboardId"),
}
if err := bus.Dispatch(&query); err != nil {
return ApiError(500, "Failed to fetch alert states", err)
}
return Json(200, query.Result)
}
// GET /api/alerts
func GetAlerts(c *middleware.Context) Response {
query := models.GetAlertsQuery{
......
......@@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) {
r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest))
r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert))
r.Get("/", wrap(GetAlerts))
r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard))
})
r.Get("/alert-notifications", wrap(GetAlertNotifications))
......
......@@ -135,3 +135,18 @@ type GetAlertByIdQuery struct {
Result *Alert
}
type GetAlertStatesForDashboardQuery struct {
OrgId int64
DashboardId int64
Result []*AlertStateInfoDTO
}
type AlertStateInfoDTO struct {
Id int64 `json:"id"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
State AlertStateType `json:"state"`
NewStateDate time.Time `json:"newStateDate"`
}
......@@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) {
continue
}
// backward compatability check, can be removed later
enabled, hasEnabled := jsonAlert.CheckGet("enabled")
if !hasEnabled || !enabled.MustBool() {
if hasEnabled && enabled.MustBool() == false {
continue
}
......
......@@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"name": "name1",
"message": "desc1",
"handler": 1,
"enabled": true,
"frequency": "60s",
"conditions": [
{
......@@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) {
"name": "name2",
"message": "desc2",
"handler": 0,
"enabled": true,
"frequency": "60s",
"severity": "warning",
"conditions": [
......
......@@ -17,6 +17,7 @@ func init() {
bus.AddHandler("sql", DeleteAlertById)
bus.AddHandler("sql", GetAllAlertQueryHandler)
bus.AddHandler("sql", SetAlertState)
bus.AddHandler("sql", GetAlertStatesForDashboard)
}
func GetAlertById(query *m.GetAlertByIdQuery) error {
......@@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error {
return nil
})
}
func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error {
var rawSql = `SELECT
id,
dashboard_id,
panel_id,
state,
new_state_date
FROM alert
WHERE org_id = ? AND dashboard_id = ?`
query.Result = make([]*m.AlertStateInfoDTO, 0)
err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result)
return err
}
......@@ -48,18 +48,17 @@ export class AlertTabCtrl {
$onInit() {
this.addNotificationSegment = this.uiSegmentSrv.newPlusButton();
this.initModel();
this.validateModel();
// subscribe to graph threshold handle changes
var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this);
this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler);
// set panel alert edit mode
this.$scope.$on("$destroy", () => {
this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler);
this.panelCtrl.editingThresholds = false;
this.panelCtrl.render();
});
// subscribe to graph threshold handle changes
this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this));
// build notification model
this.notifications = [];
this.alertNotifications = [];
......@@ -68,21 +67,8 @@ export class AlertTabCtrl {
return this.backendSrv.get('/api/alert-notifications').then(res => {
this.notifications = res;
_.each(this.alert.notifications, item => {
var model = _.find(this.notifications, {id: item.id});
if (model) {
model.iconClass = this.getNotificationIcon(model.type);
this.alertNotifications.push(model);
}
});
_.each(this.notifications, item => {
if (item.isDefault) {
item.iconClass = this.getNotificationIcon(item.type);
item.bgColor = "#00678b";
this.alertNotifications.push(item);
}
});
this.initModel();
this.validateModel();
});
}
......@@ -143,9 +129,8 @@ export class AlertTabCtrl {
}
initModel() {
var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false};
if (!this.alert.enabled) {
var alert = this.alert = this.panel.alert;
if (!alert) {
return;
}
......@@ -169,6 +154,22 @@ export class AlertTabCtrl {
ThresholdMapper.alertToGraphThresholds(this.panel);
for (let addedNotification of alert.notifications) {
var model = _.find(this.notifications, {id: addedNotification.id});
if (model) {
model.iconClass = this.getNotificationIcon(model.type);
this.alertNotifications.push(model);
}
}
for (let notification of this.notifications) {
if (notification.isDefault) {
notification.iconClass = this.getNotificationIcon(notification.type);
notification.bgColor = "#00678b";
this.alertNotifications.push(notification);
}
}
this.panelCtrl.editingThresholds = true;
this.panelCtrl.render();
}
......@@ -193,7 +194,7 @@ export class AlertTabCtrl {
}
validateModel() {
if (!this.alert.enabled) {
if (!this.alert) {
return;
}
......@@ -310,17 +311,17 @@ export class AlertTabCtrl {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
this.alert = this.panel.alert = {enabled: false};
delete this.panel.alert;
this.alert = null;
this.panel.thresholds = [];
this.conditionModels = [];
this.panelCtrl.render();
}
});
}
enable() {
this.alert.enabled = true;
this.panel.alert = {};
this.initModel();
}
......
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert.enabled">
<div class="edit-tab-with-sidemenu" ng-if="ctrl.alert">
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-class="{active: ctrl.subTabIndex === 0}">
......@@ -151,7 +151,7 @@
</div>
</div>
<div class="gf-form-group" ng-if="!ctrl.alert.enabled">
<div class="gf-form-group" ng-if="!ctrl.alert">
<div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.enable()">
<i class="icon-gf icon-gf-alert"></i>
......
......@@ -9,6 +9,7 @@ import coreModule from 'app/core/core_module';
export class AnnotationsSrv {
globalAnnotationsPromise: any;
alertStatesPromise: any;
/** @ngInject */
constructor(private $rootScope,
......@@ -22,14 +23,27 @@ export class AnnotationsSrv {
clearCache() {
this.globalAnnotationsPromise = null;
this.alertStatesPromise = null;
}
getAnnotations(options) {
return this.$q.all([
this.getGlobalAnnotations(options),
this.getPanelAnnotations(options)
]).then(allResults => {
return _.flattenDeep(allResults);
this.getPanelAnnotations(options),
this.getAlertStates(options)
]).then(results => {
// combine the annotations and flatten results
var annotations = _.flattenDeep([results[0], results[1]]);
// look for alert state for this panel
var alertState = _.find(results[2], {panelId: options.panel.id});
return {
annotations: annotations,
alertState: alertState,
};
}).catch(err => {
this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]);
});
......@@ -39,7 +53,7 @@ export class AnnotationsSrv {
var panel = options.panel;
var dashboard = options.dashboard;
if (panel && panel.alert && panel.alert.enabled) {
if (panel && panel.alert) {
return this.backendSrv.get('/api/annotations', {
from: options.range.from.valueOf(),
to: options.range.to.valueOf(),
......@@ -54,6 +68,28 @@ export class AnnotationsSrv {
return this.$q.when([]);
}
getAlertStates(options) {
if (!options.dashboard.id) {
return this.$q.when([]);
}
// ignore if no alerts
if (options.panel && !options.panel.alert) {
return this.$q.when([]);
}
if (options.range.raw.to !== 'now') {
return this.$q.when([]);
}
if (this.alertStatesPromise) {
return this.alertStatesPromise;
}
this.alertStatesPromise = this.backendSrv.get('/api/alerts/states-for-dashboard', {dashboardId: options.dashboard.id});
return this.alertStatesPromise;
}
getGlobalAnnotations(options) {
var dashboard = options.dashboard;
......
......@@ -159,7 +159,7 @@ export class DashNavCtrl {
var confirmText = "";
var text2 = $scope.dashboard.title;
var alerts = $scope.dashboard.rows.reduce((memo, row) => {
memo += row.panels.filter(panel => panel.alert && panel.alert.enabled).length;
memo += row.panels.filter(panel => panel.alert).length;
return memo;
}, 0);
......
......@@ -131,7 +131,9 @@ class MetricsPanelCtrl extends PanelCtrl {
var intervalOverride = this.panel.interval;
// if no panel interval check datasource
if (!intervalOverride && this.datasource && this.datasource.interval) {
if (intervalOverride) {
intervalOverride = this.templateSrv.replace(intervalOverride, this.panel.scopedVars);
} else if (this.datasource && this.datasource.interval) {
intervalOverride = this.datasource.interval;
}
......
......@@ -6,7 +6,7 @@ import $ from 'jquery';
var module = angular.module('grafana.directives');
var panelTemplate = `
<div class="panel-container" ng-class="{'panel-transparent': ctrl.panel.transparent}">
<div class="panel-container">
<div class="panel-header">
<span class="alert-error panel-error small pointer" ng-if="ctrl.error" ng-click="ctrl.openInspector()">
<span data-placement="top" bs-tooltip="ctrl.error">
......@@ -65,6 +65,26 @@ module.directive('grafanaPanel', function() {
link: function(scope, elem) {
var panelContainer = elem.find('.panel-container');
var ctrl = scope.ctrl;
// the reason for handling these classes this way is for performance
// limit the watchers on panels etc
ctrl.events.on('render', () => {
panelContainer.toggleClass('panel-transparent', ctrl.panel.transparent === true);
panelContainer.toggleClass('panel-has-alert', ctrl.panel.alert !== undefined);
if (panelContainer.hasClass('panel-has-alert')) {
panelContainer.removeClass('panel-alert-state--ok panel-alert-state--alerting');
}
// set special class for ok, or alerting states
if (ctrl.alertState) {
if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') {
panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state);
}
}
});
scope.$watchGroup(['ctrl.fullscreen', 'ctrl.containerHeight'], function() {
panelContainer.css({minHeight: ctrl.containerHeight});
elem.toggleClass('panel-fullscreen', ctrl.fullscreen ? true : false);
......
......@@ -12,6 +12,7 @@ function (angular, $, _, Tether) {
.directive('panelMenu', function($compile, linkSrv) {
var linkTemplate =
'<span class="panel-title drag-handle pointer">' +
'<span class="icon-gf panel-alert-icon"></span>' +
'<span class="panel-title-text drag-handle">{{ctrl.panel.title | interpolateTemplateVars:this}}</span>' +
'<span class="panel-links-btn"><i class="fa fa-external-link"></i></span>' +
'<span class="panel-time-info" ng-show="ctrl.timeInfo"><i class="fa fa-clock-o"></i> {{ctrl.timeInfo}}</span>' +
......
......@@ -8,11 +8,11 @@
<span class="gf-form-label width-6">Span</span>
<select class="gf-form-input gf-size-auto" ng-model="ctrl.panel.span" ng-options="f for f in [0,1,2,3,4,5,6,7,8,9,10,11,12]"></select>
</div>
<div class="gf-form max-width-26">
<div class="gf-form">
<span class="gf-form-label width-8">Height</span>
<input type="text" class="gf-form-input max-width-6" ng-model='ctrl.panel.height' placeholder="100px"></input>
<editor-checkbox text="Transparent" model="ctrl.panel.transparent"></editor-checkbox>
</div>
<gf-form-switch class="gf-form" label="Transparent" checked="ctrl.panel.transparent" on-change="ctrl.render()"></gf-form-switch>
</div>
<div class="gf-form-inline">
......
......@@ -62,7 +62,7 @@ module.directive('grafanaGraph', function($rootScope, timeSrv) {
if (!data) {
return;
}
annotations = data.annotations || annotations;
annotations = ctrl.annotations;
render_panel();
});
......
......@@ -22,6 +22,9 @@ class GraphCtrl extends MetricsPanelCtrl {
hiddenSeries: any = {};
seriesList: any = [];
dataList: any = [];
annotations: any = [];
alertState: any;
annotationsPromise: any;
datapointsCount: number;
datapointsOutside: boolean;
......@@ -167,11 +170,11 @@ class GraphCtrl extends MetricsPanelCtrl {
onDataError(err) {
this.seriesList = [];
this.annotations = [];
this.render([]);
}
onDataReceived(dataList) {
this.dataList = dataList;
this.seriesList = this.processor.getSeriesList({dataList: dataList, range: this.range});
......@@ -186,9 +189,10 @@ class GraphCtrl extends MetricsPanelCtrl {
}
}
this.annotationsPromise.then(annotations => {
this.annotationsPromise.then(result => {
this.loading = false;
this.seriesList.annotations = annotations;
this.alertState = result.alertState;
this.annotations = result.annotations;
this.render(this.seriesList);
}, () => {
this.loading = false;
......
......@@ -13,7 +13,7 @@ export class ThresholdFormCtrl {
constructor($scope) {
this.panel = this.panelCtrl.panel;
if (this.panel.alert && this.panel.alert.enabled) {
if (this.panel.alert) {
this.disabled = true;
}
......
......@@ -34,10 +34,7 @@
ng-change="editor.render()"
ng-model-onblur>
</div>
<gf-form-switch class="gf-form" label-class="width-4"
label="Scroll"
checked="editor.panel.scroll"
change="editor.render()"></gf-form-switch>
<gf-form-switch class="gf-form" label-class="width-4" label="Scroll" checked="editor.panel.scroll" on-change="editor.render()"></gf-form-switch>
<div class="gf-form max-width-17">
<label class="gf-form-label width-6">Font size</label>
<div class="gf-form-select-wrapper max-width-15">
......
......@@ -38,3 +38,33 @@
top: 2px;
}
}
.panel-has-alert {
.panel-alert-icon:before {
content: "\e611";
position: relative;
top: 1px;
left: -3px;
}
}
.panel-alert-state {
&--alerting {
box-shadow: 0 0 10px $critical;
.panel-alert-icon:before {
color: $critical;
content: "\e610";
}
}
&--ok {
//box-shadow: 0 0 5px rgba(0,200,0,10.8);
.panel-alert-icon:before {
color: $online;
content: "\e610";
}
}
}
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