mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(alerting): show alertin state in panel header, closes #6136
This commit is contained in:
parent
2c4524bbfd
commit
7c339f0794
@ -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,19 +48,18 @@ 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
|
||||
// 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
|
||||
// build notification model
|
||||
this.notifications = [];
|
||||
this.alertNotifications = [];
|
||||
this.alertHistory = [];
|
||||
@ -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();
|
||||
}
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
<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";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user