Commit fbae6abb by Torkel Ödegaard

feat(alerting): progress on handling no data in alert query, #5860

parent b1ed641d
...@@ -33,7 +33,7 @@ var ( ...@@ -33,7 +33,7 @@ var (
M_Alerting_Result_State_Warning Counter M_Alerting_Result_State_Warning Counter
M_Alerting_Result_State_Ok Counter M_Alerting_Result_State_Ok Counter
M_Alerting_Result_State_Paused Counter M_Alerting_Result_State_Paused Counter
M_Alerting_Result_State_Pending Counter M_Alerting_Result_State_Unknown Counter
M_Alerting_Result_State_ExecutionError Counter M_Alerting_Result_State_ExecutionError Counter
M_Alerting_Active_Alerts Counter M_Alerting_Active_Alerts Counter
M_Alerting_Notification_Sent_Slack Counter M_Alerting_Notification_Sent_Slack Counter
...@@ -81,7 +81,7 @@ func initMetricVars(settings *MetricSettings) { ...@@ -81,7 +81,7 @@ func initMetricVars(settings *MetricSettings) {
M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning") M_Alerting_Result_State_Warning = RegCounter("alerting.result", "state", "warning")
M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok") M_Alerting_Result_State_Ok = RegCounter("alerting.result", "state", "ok")
M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused") M_Alerting_Result_State_Paused = RegCounter("alerting.result", "state", "paused")
M_Alerting_Result_State_Pending = RegCounter("alerting.result", "state", "pending") M_Alerting_Result_State_Unknown = RegCounter("alerting.result", "state", "unknown")
M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error") M_Alerting_Result_State_ExecutionError = RegCounter("alerting.result", "state", "execution_error")
M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts") M_Alerting_Active_Alerts = RegCounter("alerting.active_alerts")
......
...@@ -10,7 +10,7 @@ type AlertStateType string ...@@ -10,7 +10,7 @@ type AlertStateType string
type AlertSeverityType string type AlertSeverityType string
const ( const (
AlertStatePending AlertStateType = "pending" AlertStateUnknown AlertStateType = "unknown"
AlertStateExeuctionError AlertStateType = "execution_error" AlertStateExeuctionError AlertStateType = "execution_error"
AlertStatePaused AlertStateType = "paused" AlertStatePaused AlertStateType = "paused"
AlertStateCritical AlertStateType = "critical" AlertStateCritical AlertStateType = "critical"
...@@ -19,7 +19,7 @@ const ( ...@@ -19,7 +19,7 @@ const (
) )
func (s AlertStateType) IsValid() bool { func (s AlertStateType) IsValid() bool {
return s == AlertStateOK || s == AlertStatePending || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning return s == AlertStateOK || s == AlertStateUnknown || s == AlertStateExeuctionError || s == AlertStatePaused || s == AlertStateCritical || s == AlertStateWarning
} }
const ( const (
......
...@@ -5,25 +5,21 @@ import ( ...@@ -5,25 +5,21 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
"github.com/grafana/grafana/pkg/tsdb"
) )
var ( var (
defaultTypes []string = []string{"gt", "lt"} defaultTypes []string = []string{"gt", "lt"}
rangedTypes []string = []string{"within_range", "outside_range"} rangedTypes []string = []string{"within_range", "outside_range"}
paramlessTypes []string = []string{"no_value"}
) )
type AlertEvaluator interface { type AlertEvaluator interface {
Eval(timeSeries *tsdb.TimeSeries, reducedValue float64) bool Eval(reducedValue *float64) bool
} }
type ParameterlessEvaluator struct { type NoDataEvaluator struct{}
Type string
}
func (e *ParameterlessEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { func (e *NoDataEvaluator) Eval(reducedValue *float64) bool {
return len(series.Points) == 0 return reducedValue == nil
} }
type ThresholdEvaluator struct { type ThresholdEvaluator struct {
...@@ -47,14 +43,12 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu ...@@ -47,14 +43,12 @@ func newThresholdEvaludator(typ string, model *simplejson.Json) (*ThresholdEvalu
return defaultEval, nil return defaultEval, nil
} }
func (e *ThresholdEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { func (e *ThresholdEvaluator) Eval(reducedValue *float64) bool {
switch e.Type { switch e.Type {
case "gt": case "gt":
return reducedValue > e.Threshold return *reducedValue > e.Threshold
case "lt": case "lt":
return reducedValue < e.Threshold return *reducedValue < e.Threshold
case "no_value":
return len(series.Points) == 0
} }
return false return false
...@@ -88,12 +82,12 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e ...@@ -88,12 +82,12 @@ func newRangedEvaluator(typ string, model *simplejson.Json) (*RangedEvaluator, e
return rangedEval, nil return rangedEval, nil
} }
func (e *RangedEvaluator) Eval(series *tsdb.TimeSeries, reducedValue float64) bool { func (e *RangedEvaluator) Eval(reducedValue *float64) bool {
switch e.Type { switch e.Type {
case "within_range": case "within_range":
return (e.Lower < reducedValue && e.Upper > reducedValue) || (e.Upper < reducedValue && e.Lower > reducedValue) return (e.Lower < *reducedValue && e.Upper > *reducedValue) || (e.Upper < *reducedValue && e.Lower > *reducedValue)
case "outside_range": case "outside_range":
return (e.Upper < reducedValue && e.Lower < reducedValue) || (e.Upper > reducedValue && e.Lower > reducedValue) return (e.Upper < *reducedValue && e.Lower < *reducedValue) || (e.Upper > *reducedValue && e.Lower > *reducedValue)
} }
return false return false
...@@ -113,8 +107,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) { ...@@ -113,8 +107,8 @@ func NewAlertEvaluator(model *simplejson.Json) (AlertEvaluator, error) {
return newRangedEvaluator(typ, model) return newRangedEvaluator(typ, model)
} }
if inSlice(typ, paramlessTypes) { if typ == "no_data" {
return &ParameterlessEvaluator{Type: typ}, nil return &NoDataEvaluator{}, nil
} }
return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"} return nil, alerting.ValidationError{Reason: "Evaludator invalid evaluator type"}
......
...@@ -4,7 +4,6 @@ import ( ...@@ -4,7 +4,6 @@ import (
"testing" "testing"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
...@@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64) ...@@ -15,19 +14,7 @@ func evalutorScenario(json string, reducedValue float64, datapoints ...float64)
evaluator, err := NewAlertEvaluator(jsonModel) evaluator, err := NewAlertEvaluator(jsonModel)
So(err, ShouldBeNil) So(err, ShouldBeNil)
var timeserie [][2]float64 return evaluator.Eval(reducedValue)
dummieTimestamp := float64(521452145)
for _, v := range datapoints {
timeserie = append(timeserie, [2]float64{v, dummieTimestamp})
}
tsdb := &tsdb.TimeSeries{
Name: "test time serie",
Points: timeserie,
}
return evaluator.Eval(tsdb, reducedValue)
} }
func TestEvalutors(t *testing.T) { func TestEvalutors(t *testing.T) {
......
...@@ -40,22 +40,27 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) { ...@@ -40,22 +40,27 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) {
for _, series := range seriesList { for _, series := range seriesList {
reducedValue := c.Reducer.Reduce(series) reducedValue := c.Reducer.Reduce(series)
evalMatch := c.Evaluator.Eval(series, reducedValue) evalMatch := c.Evaluator.Eval(reducedValue)
if context.IsTestRun { if context.IsTestRun {
context.Logs = append(context.Logs, &alerting.ResultLogEntry{ context.Logs = append(context.Logs, &alerting.ResultLogEntry{
Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, reducedValue), Message: fmt.Sprintf("Condition[%d]: Eval: %v, Metric: %s, Value: %1.3f", c.Index, evalMatch, series.Name, *reducedValue),
}) })
} }
if evalMatch { if evalMatch {
context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{ context.EvalMatches = append(context.EvalMatches, &alerting.EvalMatch{
Metric: series.Name, Metric: series.Name,
Value: reducedValue, Value: *reducedValue,
}) })
} }
context.Firing = evalMatch context.Firing = evalMatch
// handle no data scenario
if reducedValue == nil {
context.NoDataFound = true
}
} }
} }
......
...@@ -3,14 +3,18 @@ package conditions ...@@ -3,14 +3,18 @@ package conditions
import "github.com/grafana/grafana/pkg/tsdb" import "github.com/grafana/grafana/pkg/tsdb"
type QueryReducer interface { type QueryReducer interface {
Reduce(timeSeries *tsdb.TimeSeries) float64 Reduce(timeSeries *tsdb.TimeSeries) *float64
} }
type SimpleReducer struct { type SimpleReducer struct {
Type string Type string
} }
func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) *float64 {
if len(series.Points) == 0 {
return nil
}
var value float64 = 0 var value float64 = 0
switch s.Type { switch s.Type {
...@@ -46,7 +50,7 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 { ...@@ -46,7 +50,7 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) float64 {
value = float64(len(series.Points)) value = float64(len(series.Points))
} }
return value return &value
} }
func NewSimpleReducer(typ string) *SimpleReducer { func NewSimpleReducer(typ string) *SimpleReducer {
......
...@@ -26,6 +26,7 @@ type EvalContext struct { ...@@ -26,6 +26,7 @@ type EvalContext struct {
dashboardSlug string dashboardSlug string
ImagePublicUrl string ImagePublicUrl string
ImageOnDiskPath string ImageOnDiskPath string
NoDataFound bool
} }
type StateDescription struct { type StateDescription struct {
......
...@@ -41,7 +41,12 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { ...@@ -41,7 +41,12 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity) ctx.Rule.State = m.AlertStateType(ctx.Rule.Severity)
annotationData = simplejson.NewFromAny(ctx.EvalMatches) annotationData = simplejson.NewFromAny(ctx.EvalMatches)
} else { } else {
ctx.Rule.State = m.AlertStateOK // handle no data case
if ctx.NoDataFound {
ctx.Rule.State = ctx.Rule.NoDataState
} else {
ctx.Rule.State = m.AlertStateOK
}
} }
countStateResult(ctx.Rule.State) countStateResult(ctx.Rule.State)
...@@ -91,8 +96,8 @@ func countStateResult(state m.AlertStateType) { ...@@ -91,8 +96,8 @@ func countStateResult(state m.AlertStateType) {
metrics.M_Alerting_Result_State_Ok.Inc(1) metrics.M_Alerting_Result_State_Ok.Inc(1)
case m.AlertStatePaused: case m.AlertStatePaused:
metrics.M_Alerting_Result_State_Paused.Inc(1) metrics.M_Alerting_Result_State_Paused.Inc(1)
case m.AlertStatePending: case m.AlertStateUnknown:
metrics.M_Alerting_Result_State_Pending.Inc(1) metrics.M_Alerting_Result_State_Unknown.Inc(1)
case m.AlertStateExeuctionError: case m.AlertStateExeuctionError:
metrics.M_Alerting_Result_State_ExecutionError.Inc(1) metrics.M_Alerting_Result_State_ExecutionError.Inc(1)
} }
......
...@@ -18,6 +18,7 @@ type Rule struct { ...@@ -18,6 +18,7 @@ type Rule struct {
Frequency int64 Frequency int64
Name string Name string
Message string Message string
NoDataState m.AlertStateType
State m.AlertStateType State m.AlertStateType
Severity m.AlertSeverityType Severity m.AlertSeverityType
Conditions []Condition Conditions []Condition
...@@ -67,6 +68,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { ...@@ -67,6 +68,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
model.Frequency = ruleDef.Frequency model.Frequency = ruleDef.Frequency
model.Severity = ruleDef.Severity model.Severity = ruleDef.Severity
model.State = ruleDef.State model.State = ruleDef.State
model.NoDataState = m.AlertStateType(ruleDef.Settings.Get("noDataState").MustString("unknown"))
for _, v := range ruleDef.Settings.Get("notifications").MustArray() { for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
jsonModel := simplejson.NewFromAny(v) jsonModel := simplejson.NewFromAny(v)
......
...@@ -4,7 +4,7 @@ import ( ...@@ -4,7 +4,7 @@ import (
"testing" "testing"
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models" m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey" . "github.com/smartystreets/goconvey/convey"
) )
...@@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) { ...@@ -45,6 +45,7 @@ func TestAlertRuleModel(t *testing.T) {
"name": "name2", "name": "name2",
"description": "desc2", "description": "desc2",
"handler": 0, "handler": 0,
"noDataMode": "critical",
"enabled": true, "enabled": true,
"frequency": "60s", "frequency": "60s",
"conditions": [ "conditions": [
...@@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) { ...@@ -63,7 +64,7 @@ func TestAlertRuleModel(t *testing.T) {
alertJSON, jsonErr := simplejson.NewJson([]byte(json)) alertJSON, jsonErr := simplejson.NewJson([]byte(json))
So(jsonErr, ShouldBeNil) So(jsonErr, ShouldBeNil)
alert := &models.Alert{ alert := &m.Alert{
Id: 1, Id: 1,
OrgId: 1, OrgId: 1,
DashboardId: 1, DashboardId: 1,
...@@ -80,6 +81,10 @@ func TestAlertRuleModel(t *testing.T) { ...@@ -80,6 +81,10 @@ func TestAlertRuleModel(t *testing.T) {
Convey("Can read notifications", func() { Convey("Can read notifications", func() {
So(len(alertRule.Notifications), ShouldEqual, 2) So(len(alertRule.Notifications), ShouldEqual, 2)
}) })
Convey("Can read noDataMode", func() {
So(len(alertRule.NoDataMode), ShouldEqual, m.AlertStateCritical)
})
}) })
}) })
} }
...@@ -159,7 +159,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor ...@@ -159,7 +159,7 @@ func upsertAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *xor
} else { } else {
alert.Updated = time.Now() alert.Updated = time.Now()
alert.Created = time.Now() alert.Created = time.Now()
alert.State = m.AlertStatePending alert.State = m.AlertStateUnknown
alert.NewStateDate = time.Now() alert.NewStateDate = time.Now()
_, err := sess.Insert(alert) _, err := sess.Insert(alert)
......
...@@ -11,6 +11,7 @@ import ( ...@@ -11,6 +11,7 @@ import (
"time" "time"
"github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/log"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb" "github.com/grafana/grafana/pkg/tsdb"
) )
...@@ -47,6 +48,10 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC ...@@ -47,6 +48,10 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
formData["target"] = []string{query.Query} formData["target"] = []string{query.Query}
} }
if setting.Env == setting.DEV {
glog.Debug("Graphite request", "params", formData)
}
req, err := e.createRequest(formData) req, err := e.createRequest(formData)
if err != nil { if err != nil {
result.Error = err result.Error = err
...@@ -71,6 +76,10 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC ...@@ -71,6 +76,10 @@ func (e *GraphiteExecutor) Execute(queries tsdb.QuerySlice, context *tsdb.QueryC
Name: series.Target, Name: series.Target,
Points: series.DataPoints, Points: series.DataPoints,
}) })
if setting.Env == setting.DEV {
glog.Debug("Graphite response", "target", series.Target, "datapoints", len(series.DataPoints))
}
} }
result.QueryResults["A"] = queryRes result.QueryResults["A"] = queryRes
......
...@@ -36,6 +36,13 @@ var reducerTypes = [ ...@@ -36,6 +36,13 @@ var reducerTypes = [
{text: 'count()', value: 'count'}, {text: 'count()', value: 'count'},
]; ];
var noDataModes = [
{text: 'OK', value: 'ok'},
{text: 'Critical', value: 'critical'},
{text: 'Warning', value: 'warning'},
{text: 'Unknown', value: 'unknown'},
];
function createReducerPart(model) { function createReducerPart(model) {
var def = new QueryPartDef({type: model.type, defaultParams: []}); var def = new QueryPartDef({type: model.type, defaultParams: []});
return new QueryPart(model, def); return new QueryPart(model, def);
...@@ -69,9 +76,9 @@ function getStateDisplayModel(state) { ...@@ -69,9 +76,9 @@ function getStateDisplayModel(state) {
stateClass: 'alert-state-warning' stateClass: 'alert-state-warning'
}; };
} }
case 'pending': { case 'unknown': {
return { return {
text: 'PENDING', text: 'UNKNOWN',
iconClass: "fa fa-question", iconClass: "fa fa-question",
stateClass: 'alert-state-warning' stateClass: 'alert-state-warning'
}; };
...@@ -100,6 +107,7 @@ export default { ...@@ -100,6 +107,7 @@ export default {
conditionTypes: conditionTypes, conditionTypes: conditionTypes,
evalFunctions: evalFunctions, evalFunctions: evalFunctions,
severityLevels: severityLevels, severityLevels: severityLevels,
noDataModes: noDataModes,
reducerTypes: reducerTypes, reducerTypes: reducerTypes,
createReducerPart: createReducerPart, createReducerPart: createReducerPart,
}; };
...@@ -13,7 +13,7 @@ export class AlertListCtrl { ...@@ -13,7 +13,7 @@ export class AlertListCtrl {
stateFilters = [ stateFilters = [
{text: 'All', value: null}, {text: 'All', value: null},
{text: 'OK', value: 'ok'}, {text: 'OK', value: 'ok'},
{text: 'Pending', value: 'pending'}, {text: 'Unknown', value: 'unknown'},
{text: 'Warning', value: 'warning'}, {text: 'Warning', value: 'warning'},
{text: 'Critical', value: 'critical'}, {text: 'Critical', value: 'critical'},
{text: 'Execution Error', value: 'execution_error'}, {text: 'Execution Error', value: 'execution_error'},
......
...@@ -18,6 +18,7 @@ export class AlertTabCtrl { ...@@ -18,6 +18,7 @@ export class AlertTabCtrl {
conditionModels: any; conditionModels: any;
evalFunctions: any; evalFunctions: any;
severityLevels: any; severityLevels: any;
noDataModes: any;
addNotificationSegment; addNotificationSegment;
notifications; notifications;
alertNotifications; alertNotifications;
...@@ -41,6 +42,7 @@ export class AlertTabCtrl { ...@@ -41,6 +42,7 @@ export class AlertTabCtrl {
this.evalFunctions = alertDef.evalFunctions; this.evalFunctions = alertDef.evalFunctions;
this.conditionTypes = alertDef.conditionTypes; this.conditionTypes = alertDef.conditionTypes;
this.severityLevels = alertDef.severityLevels; this.severityLevels = alertDef.severityLevels;
this.noDataModes = alertDef.noDataModes;
this.appSubUrl = config.appSubUrl; this.appSubUrl = config.appSubUrl;
} }
...@@ -138,6 +140,7 @@ export class AlertTabCtrl { ...@@ -138,6 +140,7 @@ export class AlertTabCtrl {
alert.conditions.push(this.buildDefaultCondition()); alert.conditions.push(this.buildDefaultCondition());
} }
alert.noDataState = alert.noDataState || 'unknown';
alert.severity = alert.severity || 'critical'; alert.severity = alert.severity || 'critical';
alert.frequency = alert.frequency || '60s'; alert.frequency = alert.frequency || '60s';
alert.handler = alert.handler || 1; alert.handler = alert.handler || 1;
......
...@@ -52,19 +52,20 @@ ...@@ -52,19 +52,20 @@
<span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span> <span class="gf-form-label query-keyword width-5" ng-if="$index">AND</span>
<span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span> <span class="gf-form-label query-keyword width-5" ng-if="$index===0">WHEN</span>
</div> </div>
<div class="gf-form"> <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 class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
<span class="gf-form-label query-keyword">OF</span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label">Reducer</span> <query-part-editor class="gf-form-label query-part" part="conditionModel.queryPart" handle-event="ctrl.handleQueryPartEvent(conditionModel, $event)">
<query-part-editor class="gf-form-label query-part" part="conditionModel.reducerPart" handle-event="ctrl.handleReducerPartEvent(conditionModel, $event)">
</query-part-editor> </query-part-editor>
</div> </div>
<div class="gf-form"> <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> <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-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input> <input class="gf-form-input max-width-7" type="number" ng-hide="conditionModel.evaluator.params.length === 0" ng-model="conditionModel.evaluator.params[0]" ng-change="ctrl.evaluatorParamsChanged()"></input>
<label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label> <label class="gf-form-label query-keyword" ng-show="conditionModel.evaluator.params.length === 2">TO</label>
<input class="gf-form-input max-width-7" type="number" ng-if="conditionModel.evaluator.params.length === 2" ng-model="conditionModel.evaluator.params[1]" ng-change="ctrl.evaluatorParamsChanged()"></input>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<label class="gf-form-label"> <label class="gf-form-label">
...@@ -88,6 +89,18 @@ ...@@ -88,6 +89,18 @@
</label> </label>
</div> </div>
</div>
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label">If no data points 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"> <div class="gf-form-button-row">
<button class="btn btn-inverse" ng-click="ctrl.test()"> <button class="btn btn-inverse" ng-click="ctrl.test()">
Test Rule Test Rule
......
...@@ -39,7 +39,6 @@ $brand-primary: $orange; ...@@ -39,7 +39,6 @@ $brand-primary: $orange;
$brand-success: $green; $brand-success: $green;
$brand-warning: $brand-primary; $brand-warning: $brand-primary;
$brand-danger: $red; $brand-danger: $red;
$brand-text-highlight: #f7941d;
// Status colors // Status colors
// ------------------------- // -------------------------
......
...@@ -44,7 +44,6 @@ $brand-primary: $orange; ...@@ -44,7 +44,6 @@ $brand-primary: $orange;
$brand-success: $green; $brand-success: $green;
$brand-warning: $orange; $brand-warning: $orange;
$brand-danger: $red; $brand-danger: $red;
$brand-text-highlight: #f7941d;
// Status colors // Status colors
// ------------------------- // -------------------------
......
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