feat(alerting): progress on alerting UI and model, refactoring of dashboard parser and tests into extractor component, moved tests from sqlstore to alerting package

This commit is contained in:
Torkel Ödegaard 2016-06-11 10:13:33 +02:00
parent 1fa9ae810b
commit 2b4a9954b1
13 changed files with 441 additions and 270 deletions

View File

@ -151,15 +151,13 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) {
}
if setting.AlertingEnabled {
saveAlertCommand := m.SaveAlertsCommand{
DashboardId: cmd.Result.Id,
OrgId: c.OrgId,
UserId: c.UserId,
Alerts: alerting.ParseAlertsFromDashboard(&cmd),
alertCmd := alerting.UpdateDashboardAlertsCommand{
OrgId: c.OrgId,
UserId: c.UserId,
Dashboard: cmd.Result,
}
err = bus.Dispatch(&saveAlertCommand)
if err != nil {
if err := bus.Dispatch(&alertCmd); err != nil {
c.JsonApiErr(500, "Failed to save alerts", err)
return
}

View File

@ -14,6 +14,9 @@ type AlertRuleModel struct {
Name string
Description string
State string
Scheduler int64
Enabled bool
Frequency int
Created time.Time
Updated time.Time
@ -21,6 +24,8 @@ type AlertRuleModel struct {
Expression *simplejson.Json
}
type AlertRules []*AlertRuleModel
func (this AlertRuleModel) TableName() string {
return "alert_rule"
}
@ -83,7 +88,7 @@ type SaveAlertsCommand struct {
UserId int64
OrgId int64
Alerts []*AlertRuleModel
Alerts AlertRules
}
type DeleteAlertCommand struct {

View File

@ -0,0 +1,76 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
)
type AlertRule struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Description string
State string
Warning Level
Critical Level
Query AlertQuery
Transform string
TransformParams simplejson.Json
Transformer Transformer
}
func NewAlertRuleFromDBModel(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@ -0,0 +1,89 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
m "github.com/grafana/grafana/pkg/models"
)
type UpdateDashboardAlertsCommand struct {
UserId int64
OrgId int64
Dashboard *m.Dashboard
}
func init() {
bus.AddHandler("alerting", updateDashboardAlerts)
}
func updateDashboardAlerts(cmd *UpdateDashboardAlertsCommand) error {
saveRulesCmd := m.SaveAlertsCommand{
OrgId: cmd.OrgId,
UserId: cmd.UserId,
}
extractor := NewAlertRuleExtractor(cmd.Dashboard, cmd.OrgId)
rules, err := extractor.GetRuleModels()
if err != nil {
return err
}
saveRulesCmd.Alerts = rules
if bus.Dispatch(&saveRulesCmd); err != nil {
return err
}
return nil
}
func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@ -1,135 +0,0 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
func ParseAlertsFromDashboard(cmd *m.SaveDashboardCommand) []*m.AlertRuleModel {
alerts := make([]*m.AlertRuleModel, 0)
for _, rowObj := range cmd.Dashboard.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
alerting := panel.Get("alerting")
alert := &m.AlertRuleModel{
DashboardId: cmd.Result.Id,
OrgId: cmd.Result.OrgId,
PanelId: panel.Get("id").MustInt64(),
Id: alerting.Get("id").MustInt64(),
Name: alerting.Get("name").MustString(),
Description: alerting.Get("description").MustString(),
}
log.Info("Alertrule: %v", alert.Name)
valueQuery := alerting.Get("query")
valueQueryRef := valueQuery.Get("refId").MustString()
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == valueQueryRef {
datsourceName := ""
if target.Get("datasource").MustString() != "" {
datsourceName = target.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
datsourceName = panel.Get("datasource").MustString()
}
if datsourceName == "" {
query := &m.GetDataSourcesQuery{OrgId: cmd.OrgId}
if err := bus.Dispatch(query); err == nil {
for _, ds := range query.Result {
if ds.IsDefault {
alerting.SetPath([]string{"query", "datasourceId"}, ds.Id)
}
}
}
} else {
query := &m.GetDataSourceByNameQuery{
Name: panel.Get("datasource").MustString(),
OrgId: cmd.OrgId,
}
bus.Dispatch(query)
alerting.SetPath([]string{"query", "datasourceId"}, query.Result.Id)
}
targetQuery := target.Get("target").MustString()
if targetQuery != "" {
alerting.SetPath([]string{"query", "query"}, targetQuery)
}
}
}
alert.Expression = alerting
_, err := ConvetAlertModelToAlertRule(alert)
if err == nil && alert.ValidToSave() {
alerts = append(alerts, alert)
} else {
log.Error2("Failed to parse model from expression", "error", err)
}
}
}
return alerts
}
func ConvetAlertModelToAlertRule(ruleDef *m.AlertRuleModel) (*AlertRule, error) {
model := &AlertRule{}
model.Id = ruleDef.Id
model.OrgId = ruleDef.OrgId
model.Name = ruleDef.Name
model.Description = ruleDef.Description
model.State = ruleDef.State
critical := ruleDef.Expression.Get("critical")
model.Critical = Level{
Operator: critical.Get("op").MustString(),
Level: critical.Get("level").MustFloat64(),
}
warning := ruleDef.Expression.Get("warning")
model.Warning = Level{
Operator: warning.Get("op").MustString(),
Level: warning.Get("level").MustFloat64(),
}
model.Frequency = ruleDef.Expression.Get("frequency").MustInt64()
model.Transform = ruleDef.Expression.Get("transform").Get("type").MustString()
model.TransformParams = *ruleDef.Expression.Get("transform")
if model.Transform == "aggregation" {
model.Transformer = &AggregationTransformer{
Method: ruleDef.Expression.Get("transform").Get("method").MustString(),
}
}
query := ruleDef.Expression.Get("query")
model.Query = AlertQuery{
Query: query.Get("query").MustString(),
DatasourceId: query.Get("datasourceId").MustInt64(),
From: query.Get("from").MustString(),
To: query.Get("to").MustString(),
Aggregator: query.Get("agg").MustString(),
}
if model.Query.Query == "" {
return nil, fmt.Errorf("missing query.query")
}
if model.Query.DatasourceId == 0 {
return nil, fmt.Errorf("missing query.datasourceId")
}
return model, nil
}

View File

@ -0,0 +1,120 @@
package alerting
import (
"errors"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
m "github.com/grafana/grafana/pkg/models"
)
type AlertRuleExtractor struct {
Dash *m.Dashboard
OrgId int64
log log.Logger
}
func NewAlertRuleExtractor(dash *m.Dashboard, orgId int64) *AlertRuleExtractor {
return &AlertRuleExtractor{
Dash: dash,
OrgId: orgId,
log: log.New("alerting.extractor"),
}
}
func (e *AlertRuleExtractor) lookupDatasourceId(dsName string) (int64, error) {
if dsName == "" {
query := &m.GetDataSourcesQuery{OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return 0, err
} else {
for _, ds := range query.Result {
if ds.IsDefault {
return ds.Id, nil
}
}
}
} else {
query := &m.GetDataSourceByNameQuery{Name: dsName, OrgId: e.OrgId}
if err := bus.Dispatch(query); err != nil {
return 0, err
} else {
return query.Result.Id, nil
}
}
return 0, errors.New("Could not find datasource id for " + dsName)
}
func (e *AlertRuleExtractor) GetRuleModels() (m.AlertRules, error) {
rules := make(m.AlertRules, 0)
for _, rowObj := range e.Dash.Data.Get("rows").MustArray() {
row := simplejson.NewFromAny(rowObj)
for _, panelObj := range row.Get("panels").MustArray() {
panel := simplejson.NewFromAny(panelObj)
jsonRule := panel.Get("alerting")
// check if marked for deletion
deleted := jsonRule.Get("deleted").MustBool()
if deleted {
e.log.Info("Deleted alert rule found")
continue
}
ruleModel := &m.AlertRuleModel{
DashboardId: e.Dash.Id,
OrgId: e.OrgId,
PanelId: panel.Get("id").MustInt64(),
Id: jsonRule.Get("id").MustInt64(),
Name: jsonRule.Get("name").MustString(),
Scheduler: jsonRule.Get("scheduler").MustInt64(),
Enabled: jsonRule.Get("enabled").MustBool(),
Description: jsonRule.Get("description").MustString(),
}
valueQuery := jsonRule.Get("query")
valueQueryRef := valueQuery.Get("refId").MustString()
for _, targetsObj := range panel.Get("targets").MustArray() {
target := simplejson.NewFromAny(targetsObj)
if target.Get("refId").MustString() == valueQueryRef {
dsName := ""
if target.Get("datasource").MustString() != "" {
dsName = target.Get("datasource").MustString()
} else if panel.Get("datasource").MustString() != "" {
dsName = panel.Get("datasource").MustString()
}
if datasourceId, err := e.lookupDatasourceId(dsName); err != nil {
return nil, err
} else {
valueQuery.SetPath([]string{"datasourceId"}, datasourceId)
}
targetQuery := target.Get("target").MustString()
if targetQuery != "" {
jsonRule.SetPath([]string{"query", "query"}, targetQuery)
}
}
}
ruleModel.Expression = jsonRule
// validate
_, err := NewAlertRuleFromDBModel(ruleModel)
if err == nil && ruleModel.ValidToSave() {
rules = append(rules, ruleModel)
} else {
e.log.Error("Failed to extract alert rules from dashboard", "error", err)
return nil, errors.New("Failed to extract alert rules from dashboard")
}
}
}
return rules, nil
}

View File

@ -1,17 +1,17 @@
package sqlstore
package alerting
import (
"testing"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestAlertModelParsing(t *testing.T) {
func TestAlertRuleExtraction(t *testing.T) {
Convey("Parsing alert info from json", t, func() {
Convey("Parsing alert rules from dashboard json", t, func() {
Convey("Parsing and validating alerts from dashboards", func() {
json := `{
"id": 57,
@ -37,13 +37,15 @@ func TestAlertModelParsing(t *testing.T) {
],
"datasource": null,
"alerting": {
"name": "Alerting Panel Title alert",
"description": "description",
"name": "name1",
"description": "desc1",
"scheduler": 1,
"enabled": true,
"critical": {
"level": 20,
"op": ">"
},
"frequency": 10,
"frequency": "60s",
"query": {
"from": "5m",
"refId": "A",
@ -51,12 +53,12 @@ func TestAlertModelParsing(t *testing.T) {
},
"transform": {
"method": "avg",
"name": "aggregation"
"type": "aggregation"
},
"warning": {
"level": 10,
"op": ">"
}
}
}
},
{
@ -70,13 +72,15 @@ func TestAlertModelParsing(t *testing.T) {
],
"datasource": "graphite2",
"alerting": {
"name": "Alerting Panel Title alert",
"description": "description",
"name": "name2",
"description": "desc2",
"scheduler": 0,
"enabled": true,
"critical": {
"level": 20,
"op": ">"
},
"frequency": 10,
"frequency": "60s",
"query": {
"from": "5m",
"refId": "A",
@ -145,7 +149,10 @@ func TestAlertModelParsing(t *testing.T) {
],
"title": "Broken influxdb panel",
"transform": "table",
"type": "table"
"type": "table",
"alerting": {
"deleted": true
}
}
],
"title": "New row"
@ -153,51 +160,62 @@ func TestAlertModelParsing(t *testing.T) {
]
}`
dashboardJSON, _ := simplejson.NewJson([]byte(json))
cmd := &m.SaveDashboardCommand{
Dashboard: dashboardJSON,
UserId: 1,
OrgId: 1,
Overwrite: true,
Result: &m.Dashboard{
Id: 1,
},
}
dashJson, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
InitTestDB(t)
dash := m.NewDashboardFromJson(dashJson)
extractor := NewAlertRuleExtractor(dash, 1)
AddDataSource(&m.AddDataSourceCommand{
Name: "graphite2",
OrgId: 1,
Type: m.DS_INFLUXDB,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
IsDefault: false,
Database: "site",
// mock data
defaultDs := &m.DataSource{Id: 12, OrgId: 2, Name: "I am default", IsDefault: true}
graphite2Ds := &m.DataSource{Id: 15, OrgId: 2, Name: "graphite2"}
bus.AddHandler("test", func(query *m.GetDataSourcesQuery) error {
query.Result = []*m.DataSource{defaultDs, graphite2Ds}
return nil
})
AddDataSource(&m.AddDataSourceCommand{
Name: "InfluxDB",
OrgId: 1,
Type: m.DS_GRAPHITE,
Access: m.DS_ACCESS_DIRECT,
Url: "http://test",
IsDefault: true,
bus.AddHandler("test", func(query *m.GetDataSourceByNameQuery) error {
if query.Name == defaultDs.Name {
query.Result = defaultDs
}
if query.Name == graphite2Ds.Name {
query.Result = graphite2Ds
}
return nil
})
alerts := alerting.ParseAlertsFromDashboard(cmd)
alerts, err := extractor.GetRuleModels()
Convey("Get rules without error", func() {
So(err, ShouldBeNil)
})
Convey("all properties have been set", func() {
So(alerts, ShouldNotBeEmpty)
So(len(alerts), ShouldEqual, 2)
for _, v := range alerts {
So(v.DashboardId, ShouldEqual, 1)
So(v.PanelId, ShouldNotEqual, 0)
So(v.DashboardId, ShouldEqual, 57)
So(v.Name, ShouldNotBeEmpty)
So(v.Description, ShouldNotBeEmpty)
}
Convey("should extract scheduler property", func() {
So(alerts[0].Scheduler, ShouldEqual, 1)
So(alerts[1].Scheduler, ShouldEqual, 0)
})
Convey("should extract panel idc", func() {
So(alerts[0].PanelId, ShouldEqual, 3)
So(alerts[1].PanelId, ShouldEqual, 4)
})
Convey("should extract name and desc", func() {
So(alerts[0].Name, ShouldEqual, "name1")
So(alerts[0].Description, ShouldEqual, "desc1")
So(alerts[1].Name, ShouldEqual, "name2")
So(alerts[1].Description, ShouldEqual, "desc2")
})
})
})
})

View File

@ -1,9 +1,5 @@
package alerting
import (
"github.com/grafana/grafana/pkg/components/simplejson"
)
type AlertJob struct {
Offset int64
Delay bool
@ -21,23 +17,6 @@ type AlertResult struct {
AlertJob *AlertJob
}
type AlertRule struct {
Id int64
OrgId int64
DashboardId int64
PanelId int64
Frequency int64
Name string
Description string
State string
Warning Level
Critical Level
Query AlertQuery
Transform string
TransformParams simplejson.Json
Transformer Transformer
}
type Level struct {
Operator string
Level float64

View File

@ -17,6 +17,9 @@ func addAlertMigrations(mg *Migrator) {
{Name: "description", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "state", Type: DB_NVarchar, Length: 255, Nullable: false},
{Name: "expression", Type: DB_Text, Nullable: false},
{Name: "scheduler", Type: DB_BigInt, Nullable: false},
{Name: "frequency", Type: DB_BigInt, Nullable: false},
{Name: "enabled", Type: DB_Bool, Nullable: false},
{Name: "created", Type: DB_DateTime, Nullable: false},
{Name: "updated", Type: DB_DateTime, Nullable: false},
},

View File

@ -24,6 +24,7 @@ export class AlertTabCtrl {
panelCtrl: any;
alerting: any;
metricTargets = [{ refId: '- select query -' } ];
schedulers = [{text: 'Grafana', value: 1}, {text: 'External', value: 0}];
transforms = [
{
text: 'Aggregation',
@ -33,24 +34,23 @@ export class AlertTabCtrl {
text: 'Linear Forecast',
type: 'forecast',
},
{
text: 'Percent Change',
type: 'percent_change',
},
{
text: 'Query diff',
type: 'query_diff',
},
];
aggregators = ['avg', 'sum', 'min', 'max', 'last'];
rule: any;
query: any;
queryParams: any;
transformDef: any;
trasnformQuery: any;
levelOpList = [
{text: '>', value: '>'},
{text: '<', value: '<'},
{text: '=', value: '='},
];
defaultValues = {
frequency: 10,
frequency: '60s',
notify: [],
enabled: false,
scheduler: 1,
warning: { op: '>', level: undefined },
critical: { op: '>', level: undefined },
query: {
@ -139,8 +139,18 @@ export class AlertTabCtrl {
}
}
markAsDeleted() {
this.panel.alerting = this.defaultValues;
delete() {
this.rule = this.panel.alerting = this.defaultValues;
this.rule.deleted = true;
}
enable() {
delete this.rule.deleted;
this.rule.enabled = true;
}
disable() {
this.rule.enabled = false;
}
thresholdsUpdated() {

View File

@ -1,15 +1,13 @@
<div class="gf-form-group" >
<h5 class="section-heading">Alert Rule</h5>
<div class="editor-row">
<div class="gf-form-group section" >
<h5 class="section-heading">Alert Query</h5>
<div class="gf-form-inline">
<div class="gf-form">
<query-part-editor
class="gf-form-label query-part"
part="ctrl.query"
part-updated="ctrl.queryUpdated()">
</query-part-editor>
</div>
<div class="gf-form">
<span class="gf-form-label">Transform using</span>
@ -33,77 +31,86 @@
</div>
<div class="gf-form" ng-if="ctrl.transformDef.type === 'forecast'">
<span class="gf-form-label">Timespan</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.rule.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
<input class="gf-form-input max-width-5" type="text" ng-model="ctrl.rule.transform.timespan" ng-change="ctrl.ruleUpdated()"></input>
</div>
</div>
</div>
<div class="gf-form-group" >
<div class="gf-form-group section">
<h5 class="section-heading">Levels</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-warn"></i>
Warn if value
Warn if
</span>
<span class="gf-form-label">
&gt;
</span>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.warnLevel" ng-change="alertTab.thresholdsUpdated()"></input>
<metric-segment-model property="ctrl.rule.warning.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.warnLevel" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
<div class="gf-form">
<span class="gf-form-label">
<i class="icon-gf icon-gf-warn alert-icon-critical"></i>
Critcal if value
Critcal if
</span>
<span class="gf-form-label">
&gt;
</span>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.critLevel" ng-change="alertTab.thresholdsUpdated()"></input>
<metric-segment-model property="ctrl.rule.critical.op" options="ctrl.levelOpList" custom="false" css-class="query-segment-operator"></metric-segment-model>
<input class="gf-form-input max-width-7" type="number" ng-model="ctrl.rule.critLevel" ng-change="ctrl.thresholdsUpdated()"></input>
</div>
</div>
</div>
</div>
<!-- <div class="gf&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label width&#45;12">Aggregation method</span> -->
<!-- <div class="gf&#45;form&#45;select&#45;wrapper max&#45;width&#45;10"> -->
<!-- <select class="gf&#45;form&#45;input" -->
<!-- ng&#45;model="ctrl.panel.alerting.aggregator" -->
<!-- ng&#45;options="oper as oper for oper in alertTab.aggregators"></select> -->
<!-- </div> -->
<!-- </div> -->
<!-- -->
<!-- <div class="gf&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label width&#45;12">Query range (seconds)</span> -->
<!-- <input class="gf&#45;form&#45;input max&#45;width&#45;10" type="number" -->
<!-- ng&#45;model="ctrl.panel.alerting.queryRange" placeholder="3600"></input> -->
<!-- </div> -->
<!-- -->
<!-- <div class="gf&#45;form"> -->
<!-- <span class="gf&#45;form&#45;label width&#45;12">Frequency (seconds)</span> -->
<!-- <input class="gf&#45;form&#45;input max&#45;width&#45;10" type="number" -->
<!-- ng&#45;model="ctrl.panel.alerting.frequency" placeholder="60"></input> -->
<!-- </div> -->
<!-- </div> -->
<div>
<div class="editor-row">
<div class="gf-form-group section">
<h5 class="section-heading">Alert info</h5>
<div class="gf-form">
<span class="gf-form-label width-10">Alert name</span>
<input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
</div>
<h5 class="section-heading">Execution</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
<span class="gf-form-label">Scheduler</span>
<div class="gf-form-select-wrapper">
<select class="gf-form-input"
ng-model="ctrl.rule.scheduler"
ng-options="f.value as f.text for f in ctrl.schedulers">
</select>
</div>
</div>
<div class="gf-form">
<textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
<span class="gf-form-label">Evaluate every</span>
<input class="gf-form-input max-width-7" type="text" ng-model="ctrl.rule.frequency"></input>
</div>
</div>
</div>
<div class="gf-form-group section">
<h5 class="section-heading">Notifications</h5>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label">Groups</span>
<bootstrap-tagsinput ng-model="ctrl.rule.notify" tagclass="label label-tag" placeholder="add tags">
</bootstrap-tagsinput>
</div>
</div>
</div>
</div>
<div class="editor-row">
<div class="gf-form-button-row">
<button class="btn btn-warning" ng-click="alertTab.markAsDeleted()">Delete Alert</button>
<div class="gf-form-group section">
<h5 class="section-heading">Information</h5>
<div class="gf-form">
<span class="gf-form-label width-10">Alert name</span>
<input type="text" class="gf-form-input width-22" ng-model="ctrl.panel.alerting.name">
</div>
<div class="gf-form-inline">
<div class="gf-form">
<span class="gf-form-label width-10" style="margin-top: -73px;">Alert description</span>
</div>
<div class="gf-form">
<textarea rows="5" ng-model="ctrl.panel.alerting.description" class="gf-form-input width-22"></textarea>
</div>
</div>
</div>
<div class="editor-row">
<div class="gf-form-button-row">
<button class="btn btn-danger" ng-click="ctrl.delete()" ng-show="ctrl.rule.enabled">Delete</button>
<button class="btn btn-success" ng-click="ctrl.enable()" ng-hide="ctrl.rule.enabled">Enable</button>
<button class="btn btn-secondary" ng-click="ctrl.disable()" ng-show="ctrl.rule.enabled">Disable</button>
</div>
</div>

View File

@ -7,6 +7,7 @@
background-color: $input-bg;
input {
display: inline-block;
border: none;
border-right: 1px solid $tight-form-border;
margin: 0px;

View File

@ -35,7 +35,7 @@
this.inputSize = Math.max(1, this.placeholderText.length);
this.$container = $('<div class="bootstrap-tagsinput"></div>');
this.$input = $('<input class="tight-form-input" size="' +
this.$input = $('<input class="gf-form-input" size="' +
this.inputSize + '" type="text" placeholder="' + this.placeholderText + '"/>').appendTo(this.$container);
this.$element.after(this.$container);