mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge branch 'master' of github.com:grafana/grafana
Conflicts: pkg/services/alerting/eval_context.go
This commit is contained in:
commit
fc8f0721cd
@ -11,6 +11,9 @@
|
||||
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
|
||||
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
|
||||
|
||||
### Breaking changes
|
||||
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
|
||||
|
||||
# 3.1.2 (unreleased)
|
||||
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
|
||||
* **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)
|
||||
|
@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Starts and stops a single grafana instance on this system
|
||||
Description=Grafana instance
|
||||
Documentation=http://docs.grafana.org
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
@ -1,5 +1,5 @@
|
||||
[Unit]
|
||||
Description=Starts and stops a single grafana instance on this system
|
||||
Description=Grafana instance
|
||||
Documentation=http://docs.grafana.org
|
||||
Wants=network-online.target
|
||||
After=network-online.target
|
||||
|
@ -27,6 +27,7 @@ type EvalContext struct {
|
||||
ImagePublicUrl string
|
||||
ImageOnDiskPath string
|
||||
NoDataFound bool
|
||||
RetryCount int
|
||||
}
|
||||
|
||||
type StateDescription struct {
|
||||
@ -112,5 +113,6 @@ func NewEvalContext(rule *Rule) *EvalContext {
|
||||
DoneChan: make(chan bool, 1),
|
||||
CancelChan: make(chan bool, 1),
|
||||
log: log.New("alerting.evalContext"),
|
||||
RetryCount: 0,
|
||||
}
|
||||
}
|
||||
|
@ -8,6 +8,10 @@ import (
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
)
|
||||
|
||||
var (
|
||||
MaxRetries int = 1
|
||||
)
|
||||
|
||||
type DefaultEvalHandler struct {
|
||||
log log.Logger
|
||||
alertJobTimeout time.Duration
|
||||
@ -21,7 +25,6 @@ func NewEvalHandler() *DefaultEvalHandler {
|
||||
}
|
||||
|
||||
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
|
||||
go e.eval(context)
|
||||
|
||||
select {
|
||||
@ -29,14 +32,28 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
|
||||
context.Error = fmt.Errorf("Timeout")
|
||||
context.EndTime = time.Now()
|
||||
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
|
||||
e.retry(context)
|
||||
case <-context.DoneChan:
|
||||
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
|
||||
}
|
||||
|
||||
if context.Error != nil {
|
||||
e.retry(context)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DefaultEvalHandler) retry(context *EvalContext) {
|
||||
e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
|
||||
|
||||
context.RetryCount++
|
||||
if context.RetryCount > MaxRetries {
|
||||
context.DoneChan = make(chan bool, 1)
|
||||
context.CancelChan = make(chan bool, 1)
|
||||
e.Eval(context)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *DefaultEvalHandler) eval(context *EvalContext) {
|
||||
|
||||
for _, condition := range context.Rule.Conditions {
|
||||
condition.Eval(context)
|
||||
|
||||
|
@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) {
|
||||
handler.eval(context)
|
||||
So(context.Firing, ShouldEqual, false)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
@ -1,6 +1,10 @@
|
||||
package alerting
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type EvalHandler interface {
|
||||
Eval(context *EvalContext)
|
||||
@ -15,6 +19,7 @@ type Notifier interface {
|
||||
Notify(alertResult *EvalContext)
|
||||
GetType() string
|
||||
NeedsImage() bool
|
||||
MatchSeverity(result models.AlertSeverityType) bool
|
||||
}
|
||||
|
||||
type Condition interface {
|
||||
|
@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) MatchSeverity(result m.AlertSeverityType) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (n *RootNotifier) Notify(context *EvalContext) {
|
||||
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
|
||||
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
|
||||
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
|
||||
if err != nil {
|
||||
n.log.Error("Failed to read notifications", "error", err)
|
||||
return
|
||||
@ -87,7 +91,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
|
||||
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
|
||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
||||
|
||||
if err := bus.Dispatch(query); err != nil {
|
||||
@ -96,17 +100,19 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Not
|
||||
|
||||
var result []Notifier
|
||||
for _, notification := range query.Result {
|
||||
if not, err := n.getNotifierFor(notification); err != nil {
|
||||
if not, err := n.createNotifierFor(notification); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
result = append(result, not)
|
||||
if shouldUseNotification(not, context) {
|
||||
result = append(result, not)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
|
||||
factory, found := notifierFactories[model.Type]
|
||||
if !found {
|
||||
return nil, errors.New("Unsupported notification type")
|
||||
@ -115,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err
|
||||
return factory(model)
|
||||
}
|
||||
|
||||
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
|
||||
if !context.Firing {
|
||||
return true
|
||||
}
|
||||
|
||||
if context.Error != nil {
|
||||
return true
|
||||
}
|
||||
|
||||
return notifier.MatchSeverity(context.Rule.Severity)
|
||||
}
|
||||
|
||||
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
|
||||
|
||||
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)
|
||||
|
@ -1,114 +1,82 @@
|
||||
package alerting
|
||||
|
||||
// func TestAlertNotificationExtraction(t *testing.T) {
|
||||
// Convey("Notifier tests", t, func() {
|
||||
// Convey("rules for sending notifications", func() {
|
||||
// dummieNotifier := NotifierImpl{}
|
||||
//
|
||||
// result := &AlertResult{
|
||||
// State: alertstates.Critical,
|
||||
// }
|
||||
//
|
||||
// notifier := &Notification{
|
||||
// Name: "Test Notifier",
|
||||
// Type: "TestType",
|
||||
// SendCritical: true,
|
||||
// SendWarning: true,
|
||||
// }
|
||||
//
|
||||
// Convey("Should send notification", func() {
|
||||
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue)
|
||||
// })
|
||||
//
|
||||
// Convey("warn:false and state:warn should not send", func() {
|
||||
// result.State = alertstates.Warn
|
||||
// notifier.SendWarning = false
|
||||
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse)
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// Convey("Parsing alert notification from settings", func() {
|
||||
// Convey("Parsing email", func() {
|
||||
// Convey("empty settings should return error", func() {
|
||||
// json := `{ }`
|
||||
//
|
||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
// model := &m.AlertNotification{
|
||||
// Name: "ops",
|
||||
// Type: "email",
|
||||
// Settings: settingsJSON,
|
||||
// }
|
||||
//
|
||||
// _, err := NewNotificationFromDBModel(model)
|
||||
// So(err, ShouldNotBeNil)
|
||||
// })
|
||||
//
|
||||
// Convey("from settings", func() {
|
||||
// json := `
|
||||
// {
|
||||
// "to": "ops@grafana.org"
|
||||
// }`
|
||||
//
|
||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
// model := &m.AlertNotification{
|
||||
// Name: "ops",
|
||||
// Type: "email",
|
||||
// Settings: settingsJSON,
|
||||
// }
|
||||
//
|
||||
// not, err := NewNotificationFromDBModel(model)
|
||||
//
|
||||
// So(err, ShouldBeNil)
|
||||
// So(not.Name, ShouldEqual, "ops")
|
||||
// So(not.Type, ShouldEqual, "email")
|
||||
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier")
|
||||
//
|
||||
// email := not.Notifierr.(*EmailNotifier)
|
||||
// So(email.To, ShouldEqual, "ops@grafana.org")
|
||||
// })
|
||||
// })
|
||||
//
|
||||
// Convey("Parsing webhook", func() {
|
||||
// Convey("empty settings should return error", func() {
|
||||
// json := `{ }`
|
||||
//
|
||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
// model := &m.AlertNotification{
|
||||
// Name: "ops",
|
||||
// Type: "webhook",
|
||||
// Settings: settingsJSON,
|
||||
// }
|
||||
//
|
||||
// _, err := NewNotificationFromDBModel(model)
|
||||
// So(err, ShouldNotBeNil)
|
||||
// })
|
||||
//
|
||||
// Convey("from settings", func() {
|
||||
// json := `
|
||||
// {
|
||||
// "url": "http://localhost:3000",
|
||||
// "username": "username",
|
||||
// "password": "password"
|
||||
// }`
|
||||
//
|
||||
// settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
// model := &m.AlertNotification{
|
||||
// Name: "slack",
|
||||
// Type: "webhook",
|
||||
// Settings: settingsJSON,
|
||||
// }
|
||||
//
|
||||
// not, err := NewNotificationFromDBModel(model)
|
||||
//
|
||||
// So(err, ShouldBeNil)
|
||||
// So(not.Name, ShouldEqual, "slack")
|
||||
// So(not.Type, ShouldEqual, "webhook")
|
||||
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier")
|
||||
//
|
||||
// webhook := not.Notifierr.(*WebhookNotifier)
|
||||
// So(webhook.Url, ShouldEqual, "http://localhost:3000")
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"fmt"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
type FakeNotifier struct {
|
||||
FakeMatchResult bool
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) GetType() string {
|
||||
return "FakeNotifier"
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) NeedsImage() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
|
||||
|
||||
func (fn *FakeNotifier) MatchSeverity(result models.AlertSeverityType) bool {
|
||||
return fn.FakeMatchResult
|
||||
}
|
||||
|
||||
func TestAlertNotificationExtraction(t *testing.T) {
|
||||
|
||||
Convey("Notifier tests", t, func() {
|
||||
Convey("none firing alerts", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: false,
|
||||
Rule: &Rule{
|
||||
Severity: models.AlertSeverityCritical,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("exeuction error cannot be ignored", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Error: fmt.Errorf("I used to be a programmer just like you"),
|
||||
Rule: &Rule{
|
||||
Severity: models.AlertSeverityCritical,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{
|
||||
Severity: models.AlertSeverityCritical,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: true}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("firing alert that dont match", func() {
|
||||
ctx := &EvalContext{
|
||||
Firing: true,
|
||||
Rule: &Rule{
|
||||
Severity: models.AlertSeverityCritical,
|
||||
},
|
||||
}
|
||||
notifier := &FakeNotifier{FakeMatchResult: false}
|
||||
|
||||
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
@ -1,8 +1,34 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type NotifierBase struct {
|
||||
Name string
|
||||
Type string
|
||||
Name string
|
||||
Type string
|
||||
SeverityFilter models.AlertSeverityType
|
||||
}
|
||||
|
||||
func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase {
|
||||
base := NotifierBase{Name: name, Type: notifierType}
|
||||
|
||||
severityFilter := models.AlertSeverityType(model.Get("severityFilter").MustString(""))
|
||||
|
||||
if severityFilter == models.AlertSeverityCritical || severityFilter == models.AlertSeverityWarning {
|
||||
base.SeverityFilter = severityFilter
|
||||
}
|
||||
|
||||
return base
|
||||
}
|
||||
|
||||
func (n *NotifierBase) MatchSeverity(result models.AlertSeverityType) bool {
|
||||
if !n.SeverityFilter.IsValid() {
|
||||
return true
|
||||
}
|
||||
|
||||
return n.SeverityFilter == result
|
||||
}
|
||||
|
||||
func (n *NotifierBase) GetType() string {
|
||||
|
36
pkg/services/alerting/notifiers/base_test.go
Normal file
36
pkg/services/alerting/notifiers/base_test.go
Normal file
@ -0,0 +1,36 @@
|
||||
package notifiers
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
func TestBaseNotifier(t *testing.T) {
|
||||
Convey("Parsing base notification severity", t, func() {
|
||||
|
||||
Convey("matches", func() {
|
||||
json := `
|
||||
{
|
||||
"severityFilter": "critical"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
not := NewNotifierBase("ops", "email", settingsJSON)
|
||||
So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue)
|
||||
})
|
||||
|
||||
Convey("does not match", func() {
|
||||
json := `
|
||||
{
|
||||
"severityFilter": "critical"
|
||||
}`
|
||||
|
||||
settingsJSON, _ := simplejson.NewJson([]byte(json))
|
||||
not := NewNotifierBase("ops", "email", settingsJSON)
|
||||
So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse)
|
||||
})
|
||||
})
|
||||
}
|
@ -1 +0,0 @@
|
||||
package notifiers
|
@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &EmailNotifier{
|
||||
NotifierBase: NotifierBase{
|
||||
Name: model.Name,
|
||||
Type: model.Type,
|
||||
},
|
||||
Addresses: strings.Split(addressesString, "\n"),
|
||||
log: log.New("alerting.notifier.email"),
|
||||
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||
Addresses: strings.Split(addressesString, "\n"),
|
||||
log: log.New("alerting.notifier.email"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &SlackNotifier{
|
||||
NotifierBase: NotifierBase{
|
||||
Name: model.Name,
|
||||
Type: model.Type,
|
||||
},
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.slack"),
|
||||
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
log: log.New("alerting.notifier.slack"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
|
||||
}
|
||||
|
||||
return &WebhookNotifier{
|
||||
NotifierBase: NotifierBase{
|
||||
Name: model.Name,
|
||||
Type: model.Type,
|
||||
},
|
||||
Url: url,
|
||||
User: model.Settings.Get("user").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
log: log.New("alerting.notifier.webhook"),
|
||||
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
|
||||
Url: url,
|
||||
User: model.Settings.Get("user").MustString(),
|
||||
Password: model.Settings.Get("password").MustString(),
|
||||
log: log.New("alerting.notifier.webhook"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -28,7 +28,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
|
||||
Settings: cmd.Settings,
|
||||
}
|
||||
|
||||
notifiers, err := notifier.getNotifierFor(model)
|
||||
notifiers, err := notifier.createNotifierFor(model)
|
||||
|
||||
if err != nil {
|
||||
log.Error2("Failed to create notifier", "error", err.Error())
|
||||
|
@ -108,6 +108,13 @@ export class AlertTabCtrl {
|
||||
}));
|
||||
}
|
||||
|
||||
changeTabIndex(newTabIndex) {
|
||||
this.subTabIndex = newTabIndex;
|
||||
|
||||
if (this.subTabIndex === 2) {
|
||||
this.getAlertHistory();
|
||||
}
|
||||
}
|
||||
|
||||
notificationAdded() {
|
||||
var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});
|
||||
|
@ -17,7 +17,9 @@ export class AlertNotificationEditCtrl {
|
||||
} else {
|
||||
this.model = {
|
||||
type: 'email',
|
||||
settings: {},
|
||||
settings: {
|
||||
severityFilter: 'none'
|
||||
},
|
||||
isDefault: false
|
||||
};
|
||||
}
|
||||
|
@ -2,15 +2,15 @@
|
||||
<aside class="edit-sidemenu-aside">
|
||||
<ul class="edit-sidemenu">
|
||||
<li ng-class="{active: ctrl.subTabIndex === 0}">
|
||||
<a ng-click="ctrl.subTabIndex = 0">Alert Config</a>
|
||||
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 1}">
|
||||
<a ng-click="ctrl.subTabIndex = 1">
|
||||
<a ng-click="ctrl.changeTabIndex(1)">
|
||||
Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
|
||||
</a>
|
||||
</li>
|
||||
<li ng-class="{active: ctrl.subTabIndex === 2}">
|
||||
<a ng-click="ctrl.subTabIndex = 2">Alert History</a>
|
||||
<a ng-click="ctrl.changeTabIndex(2)">Alert History</a>
|
||||
</li>
|
||||
<li>
|
||||
<a ng-click="ctrl.delete()">Delete</a>
|
||||
|
@ -12,11 +12,11 @@
|
||||
|
||||
<div class="gf-form-group">
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Name</span>
|
||||
<span class="gf-form-label width-12">Name</span>
|
||||
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-8">Type</span>
|
||||
<span class="gf-form-label width-12">Type</span>
|
||||
<div class="gf-form-select-wrapper width-15">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.model.type"
|
||||
@ -25,11 +25,20 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<span class="gf-form-label width-12">Severity filter</span>
|
||||
<div class="gf-form-select-wrapper width-15">
|
||||
<select class="gf-form-input"
|
||||
ng-model="ctrl.model.settings.severityFilter"
|
||||
ng-options="t for t in ['none', 'critical', 'warning']">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form">
|
||||
<gf-form-switch
|
||||
class="gf-form"
|
||||
label="All alerts"
|
||||
label-class="width-8"
|
||||
label="Send on all alerts"
|
||||
label-class="width-12"
|
||||
checked="ctrl.model.isDefault"
|
||||
tooltip="Use this notification for all alerts">
|
||||
</gf-form-switch>
|
||||
@ -91,5 +100,5 @@
|
||||
<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
Loading…
Reference in New Issue
Block a user