diff --git a/CHANGELOG.md b/CHANGELOG.md
index 0d293d04100..1242a0e1e06 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@
* **Graph Panel**: Bar width if bars was only used in series override, [#6528](https://github.com/grafana/grafana/issues/6528)
* **UI/Browser**: Fixed issue with page/view header gradient border not showing in Safari, [#6530](https://github.com/grafana/grafana/issues/6530)
* **Cloudwatch**: Fixed cloudwatch datasource requesting to many datapoints, [#6544](https://github.com/grafana/grafana/issues/6544)
+* **UX**: Panel Drop zone visible after duplicating panel, and when entering fullscreen/edit view, [#6598](https://github.com/grafana/grafana/issues/6598)
+* **Templating**: Newly added variable was not visible directly only after dashboard reload, [#6622](https://github.com/grafana/grafana/issues/6622)
+
+### Enhancements
+* **Singlestat**: Support repeated template variables in prefix/postfix [#6595](https://github.com/grafana/grafana/issues/6595)
+* **Templating**: Don't persist variable options with refresh option [#6586](https://github.com/grafana/grafana/issues/6586)
+* **Alerting**: Add ability to have OR conditions (and mixing AND & OR) [#6579](https://github.com/grafana/grafana/issues/6579)
# 4.0-beta1 (2016-11-09)
diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md
index ce9dcdf1b93..dfe84f142ec 100644
--- a/docs/sources/alerting/notifications.md
+++ b/docs/sources/alerting/notifications.md
@@ -98,6 +98,6 @@ Amazon S3 for this and Webdav. So to set that up you need to configure the
[external image uploader](/installation/configuration/#external-image-storage) in your grafana-server ini
config file.
-This is not an optional requirement, you can get slack and email notifications without setting this up.
+This is an optional requirement, you can get slack and email notifications without setting this up.
diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md
index ecab88f48d2..af3cfecfdb8 100644
--- a/docs/sources/alerting/rules.md
+++ b/docs/sources/alerting/rules.md
@@ -55,7 +55,10 @@ Currently the only condition type that exists is a `Query` condition that allows
specify a query letter, time range and an aggregation function. The letter refers to
a query you already have added in the **Metrics** tab. The result from the query and the aggregation function is
a single value that is then used in the threshold check. The query used in an alert rule cannot
-contain any template variables. Currently we only support `AND` operator between conditions.
+contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially.
+For example, we have 3 conditions in the following order:
+`condition:A(evaluates to: TRUE) OR condition:B(evaluates to: FALSE) AND condition:C(evaluates to: TRUE)`
+so the result will be calculated as ((TRUE OR FALSE) AND TRUE) = TRUE.
We plan to add other condition types in the future, like `Other Alert`, where you can include the state
of another alert in your conditions, and `Time Of Day`.
diff --git a/docs/sources/datasources/plugin_api.md b/docs/sources/datasources/plugin_api.md
index cdcaca29460..2e2121ed21a 100644
--- a/docs/sources/datasources/plugin_api.md
+++ b/docs/sources/datasources/plugin_api.md
@@ -30,11 +30,5 @@ Even though the data source type name is with lowercase `g`, the directive uses
that is how angular directives needs to be named in order to match an element with name `
%s-
%s- -` -) - var ( dunno = []byte("???") centerDot = []byte("ยท") @@ -151,21 +112,34 @@ func Recovery() macaron.Handler { panicLogger.Error("Request error", "error", err, "stack", string(stack)) - // Lookup the current responsewriter - val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) - res := val.Interface().(http.ResponseWriter) + c.Data["Title"] = "Server Error" + c.Data["AppSubUrl"] = setting.AppSubUrl + + if theErr, ok := err.(error); ok { + c.Data["Title"] = theErr.Error() + } - // respond with panic message while in development mode - var body []byte if setting.Env == setting.DEV { - res.Header().Set("Content-Type", "text/html") - body = []byte(fmt.Sprintf(panicHtml, err, err, stack)) + c.Data["ErrorMsg"] = string(stack) } - res.WriteHeader(http.StatusInternalServerError) - if nil != body { - res.Write(body) - } + c.HTML(500, "500") + + // // Lookup the current responsewriter + // val := c.GetVal(inject.InterfaceOf((*http.ResponseWriter)(nil))) + // res := val.Interface().(http.ResponseWriter) + // + // // respond with panic message while in development mode + // var body []byte + // if setting.Env == setting.DEV { + // res.Header().Set("Content-Type", "text/html") + // body = []byte(fmt.Sprintf(panicHtml, err, err, stack)) + // } + // + // res.WriteHeader(http.StatusInternalServerError) + // if nil != body { + // res.Write(body) + // } } }() diff --git a/pkg/services/alerting/conditions/query.go b/pkg/services/alerting/conditions/query.go index b73db9d590e..e58dbf1d583 100644 --- a/pkg/services/alerting/conditions/query.go +++ b/pkg/services/alerting/conditions/query.go @@ -23,6 +23,7 @@ type QueryCondition struct { Query AlertQuery Reducer QueryReducer Evaluator AlertEvaluator + Operator string HandleRequest tsdb.HandleRequestFunc } @@ -72,6 +73,7 @@ func (c *QueryCondition) Eval(context *alerting.EvalContext) (*alerting.Conditio return &alerting.ConditionResult{ Firing: evalMatchCount > 0, NoDataFound: emptySerieCount == len(seriesList), + Operator: c.Operator, EvalMatches: matches, }, nil } @@ -168,8 +170,12 @@ func NewQueryCondition(model *simplejson.Json, index int) (*QueryCondition, erro if err != nil { return nil, err } - condition.Evaluator = evaluator + + operatorJson := model.Get("operator") + operator := operatorJson.Get("type").MustString("and") + condition.Operator = operator + return &condition, nil } diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 711416970c5..2b252da8c4b 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -17,7 +17,7 @@ type EvalContext struct { EvalMatches []*EvalMatch Logs []*ResultLogEntry Error error - Description string + ConditionEvals string StartTime time.Time EndTime time.Time Rule *Rule diff --git a/pkg/services/alerting/eval_handler.go b/pkg/services/alerting/eval_handler.go index 538c639abb8..075d0833fdf 100644 --- a/pkg/services/alerting/eval_handler.go +++ b/pkg/services/alerting/eval_handler.go @@ -1,6 +1,8 @@ package alerting import ( + "strconv" + "strings" "time" "github.com/grafana/grafana/pkg/log" @@ -21,7 +23,10 @@ func NewEvalHandler() *DefaultEvalHandler { func (e *DefaultEvalHandler) Eval(context *EvalContext) { firing := true - for _, condition := range context.Rule.Conditions { + conditionEvals := "" + + for i := 0; i < len(context.Rule.Conditions); i++ { + condition := context.Rule.Conditions[i] cr, err := condition.Eval(context) if err != nil { context.Error = err @@ -32,15 +37,23 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) { break } - // break if result has not triggered yet - if cr.Firing == false { - firing = false - break + // calculating Firing based on operator + if cr.Operator == "or" { + firing = firing || cr.Firing + } else { + firing = firing && cr.Firing + } + + if i > 0 { + conditionEvals = "[" + conditionEvals + " " + strings.ToUpper(cr.Operator) + " " + strconv.FormatBool(cr.Firing) + "]" + } else { + conditionEvals = strconv.FormatBool(firing) } context.EvalMatches = append(context.EvalMatches, cr.EvalMatches...) } + context.ConditionEvals = conditionEvals + " = " + strconv.FormatBool(firing) context.Firing = firing context.EndTime = time.Now() elapsedTime := context.EndTime.Sub(context.StartTime) / time.Millisecond diff --git a/pkg/services/alerting/eval_handler_test.go b/pkg/services/alerting/eval_handler_test.go index 4c2ec24b506..84905870f52 100644 --- a/pkg/services/alerting/eval_handler_test.go +++ b/pkg/services/alerting/eval_handler_test.go @@ -8,12 +8,13 @@ import ( ) type conditionStub struct { - firing bool - matches []*EvalMatch + firing bool + operator string + matches []*EvalMatch } func (c *conditionStub) Eval(context *EvalContext) (*ConditionResult, error) { - return &ConditionResult{Firing: c.firing, EvalMatches: c.matches}, nil + return &ConditionResult{Firing: c.firing, EvalMatches: c.matches, Operator: c.operator}, nil } func TestAlertingExecutor(t *testing.T) { @@ -29,18 +30,102 @@ func TestAlertingExecutor(t *testing.T) { handler.Eval(context) So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "true = true") }) Convey("Show return false with not passing asdf", func() { context := NewEvalContext(context.TODO(), &Rule{ Conditions: []Condition{ - &conditionStub{firing: true, matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, - &conditionStub{firing: false}, + &conditionStub{firing: true, operator: "and", matches: []*EvalMatch{&EvalMatch{}, &EvalMatch{}}}, + &conditionStub{firing: false, operator: "and"}, }, }) handler.Eval(context) So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[true AND false] = false") + }) + + Convey("Show return true if any of the condition is passing with OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[true OR false] = true") + }) + + Convey("Show return false if any of the condition is failing with AND operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[true AND false] = false") + }) + + Convey("Show return true if one condition is failing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[[true AND true] OR false] = true") + }) + + Convey("Show return false if one condition is passing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[[true AND false] OR false] = false") + }) + + Convey("Show return false if a condition is failing with nested AND operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "and"}, + &conditionStub{firing: true, operator: "and"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, false) + So(context.ConditionEvals, ShouldEqual, "[[true AND false] AND true] = false") + }) + + Convey("Show return true if a condition is passing with nested OR operator", func() { + context := NewEvalContext(context.TODO(), &Rule{ + Conditions: []Condition{ + &conditionStub{firing: true, operator: "and"}, + &conditionStub{firing: false, operator: "or"}, + &conditionStub{firing: true, operator: "or"}, + }, + }) + + handler.Eval(context) + So(context.Firing, ShouldEqual, true) + So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true") }) }) } diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index cc2561473e3..566fbdb2898 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -24,6 +24,7 @@ type Notifier interface { type ConditionResult struct { Firing bool NoDataFound bool + Operator string EvalMatches []*EvalMatch } diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 809640ed4a7..bdf53798e34 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -26,11 +26,32 @@ type Rule struct { } type ValidationError struct { - Reason string + Reason string + Err error + Alertid int64 + DashboardId int64 + PanelId int64 } func (e ValidationError) Error() string { - return e.Reason + extraInfo := "" + if e.Alertid != 0 { + extraInfo = fmt.Sprintf("%s AlertId: %v", extraInfo, e.Alertid) + } + + if e.PanelId != 0 { + extraInfo = fmt.Sprintf("%s PanelId: %v ", extraInfo, e.PanelId) + } + + if e.DashboardId != 0 { + extraInfo = fmt.Sprintf("%s DashboardId: %v", extraInfo, e.DashboardId) + } + + if e.Err != nil { + return fmt.Sprintf("%s %s%s", e.Err.Error(), e.Reason, extraInfo) + } + + return fmt.Sprintf("Failed to extract alert.Reason: %s %s", e.Reason, extraInfo) } var ( @@ -83,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { for _, v := range ruleDef.Settings.Get("notifications").MustArray() { jsonModel := simplejson.NewFromAny(v) if id, err := jsonModel.Get("id").Int64(); err != nil { - return nil, ValidationError{Reason: "Invalid notification schema"} + return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Notifications = append(model.Notifications, id) } @@ -93,10 +114,10 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { conditionModel := simplejson.NewFromAny(condition) conditionType := conditionModel.Get("type").MustString() if factory, exist := conditionFactories[conditionType]; !exist { - return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType} + return nil, ValidationError{Reason: "Unknown alert condition: " + conditionType, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { if queryCondition, err := factory(conditionModel, index); err != nil { - return nil, err + return nil, ValidationError{Err: err, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} } else { model.Conditions = append(model.Conditions, queryCondition) } diff --git a/pkg/services/alerting/scheduler.go b/pkg/services/alerting/scheduler.go index b6ef1a63ff8..151f802ec15 100644 --- a/pkg/services/alerting/scheduler.go +++ b/pkg/services/alerting/scheduler.go @@ -39,6 +39,9 @@ func (s *SchedulerImpl) Update(rules []*Rule) { offset := ((rule.Frequency * 1000) / int64(len(rules))) * int64(i) job.Offset = int64(math.Floor(float64(offset) / 1000)) + if job.Offset == 0 { //zero offset causes division with 0 panics. + job.Offset = 1 + } jobs[rule.Id] = job } diff --git a/pkg/tsdb/influxdb/query.go b/pkg/tsdb/influxdb/query.go index e95b54b0a18..094f98cbe53 100644 --- a/pkg/tsdb/influxdb/query.go +++ b/pkg/tsdb/influxdb/query.go @@ -2,7 +2,6 @@ package influxdb import ( "fmt" - "strconv" "strings" "regexp" @@ -58,13 +57,12 @@ func (query *Query) renderTags() []string { } textValue := "" - numericValue, err := strconv.ParseFloat(tag.Value, 64) // quote value unless regex or number if tag.Operator == "=~" || tag.Operator == "!~" { textValue = tag.Value - } else if err == nil { - textValue = fmt.Sprintf("%v", numericValue) + } else if tag.Operator == "<" || tag.Operator == ">" { + textValue = tag.Value } else { textValue = fmt.Sprintf("'%s'", tag.Value) } diff --git a/pkg/tsdb/influxdb/query_test.go b/pkg/tsdb/influxdb/query_test.go index ee045444c9b..b6af67e5e42 100644 --- a/pkg/tsdb/influxdb/query_test.go +++ b/pkg/tsdb/influxdb/query_test.go @@ -106,13 +106,19 @@ func TestInfluxdbQueryBuilder(t *testing.T) { Convey("can render number tags", func() { query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001", Key: "key"}}} - So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = '10001'`) }) - Convey("can render number tags with decimals", func() { - query := &Query{Tags: []*Tag{&Tag{Operator: "=", Value: "10001.1", Key: "key"}}} + Convey("can render numbers less then condition tags", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: "<", Value: "10001", Key: "key"}}} - So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" = 10001.1`) + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" < 10001`) + }) + + Convey("can render number greather then condition tags", func() { + query := &Query{Tags: []*Tag{&Tag{Operator: ">", Value: "10001", Key: "key"}}} + + So(strings.Join(query.renderTags(), ""), ShouldEqual, `"key" > 10001`) }) Convey("can render string tags", func() { diff --git a/public/app/app.ts b/public/app/app.ts index c004bac4177..22431a5110c 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -40,7 +40,6 @@ export class GrafanaApp { init() { var app = angular.module('grafana', []); - app.constant('grafanaVersion', "@grafanaVersion@"); moment.locale(config.bootData.user.locale); diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index ed3425f670f..83b55ac476f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -147,9 +147,14 @@ export function grafanaAppDirective(playlistSrv, contextSrv) { } } + // mouse and keyboard is user activity body.mousemove(userActivityDetected); body.keydown(userActivityDetected); - setInterval(checkForInActiveUser, 1000); + // treat tab change as activity + document.addEventListener('visibilitychange', userActivityDetected); + + // check every 2 seconds + setInterval(checkForInActiveUser, 2000); appEvents.on('toggle-view-mode', () => { lastActivity = 0; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 8e9f64fd17f..4aa2e7eb64a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -6,7 +6,6 @@ import "./directives/dash_class"; import "./directives/confirm_click"; import "./directives/dash_edit_link"; import "./directives/dropdown_typeahead"; -import "./directives/grafana_version_check"; import "./directives/metric_segment"; import "./directives/misc"; import "./directives/ng_model_on_blur"; diff --git a/public/app/core/directives/grafana_version_check.js b/public/app/core/directives/grafana_version_check.js deleted file mode 100644 index bee437b8183..00000000000 --- a/public/app/core/directives/grafana_version_check.js +++ /dev/null @@ -1,31 +0,0 @@ -define([ - '../core_module', -], -function (coreModule) { - 'use strict'; - - coreModule.default.directive('grafanaVersionCheck', function($http, contextSrv) { - return { - restrict: 'A', - link: function(scope, elem) { - if (contextSrv.version === 'master') { - return; - } - - $http({ method: 'GET', url: 'https://grafanarel.s3.amazonaws.com/latest.json' }) - .then(function(response) { - if (!response.data || !response.data.version) { - return; - } - - if (contextSrv.version !== response.data.version) { - elem.append(' ' + - ' ' + - 'New version available: ' + response.data.version + - ''); - } - }); - } - }; - }); -}); diff --git a/public/app/core/directives/ng_model_on_blur.js b/public/app/core/directives/ng_model_on_blur.js index 6f4a55b53f0..09c87f5fabe 100644 --- a/public/app/core/directives/ng_model_on_blur.js +++ b/public/app/core/directives/ng_model_on_blur.js @@ -47,7 +47,7 @@ function (coreModule, kbn, rangeUtil) { if (ctrl.$isEmpty(modelValue)) { return true; } - if (viewValue.indexOf('$') === 0) { + if (viewValue.indexOf('$') === 0 || viewValue.indexOf('+$') === 0) { return true; // allow template variable } var info = rangeUtil.describeTextRange(viewValue); diff --git a/public/app/core/utils/kbn.js b/public/app/core/utils/kbn.js index a807a249235..78489dcae58 100644 --- a/public/app/core/utils/kbn.js +++ b/public/app/core/utils/kbn.js @@ -420,11 +420,11 @@ function($, _, moment) { kbn.valueFormats.bps = kbn.formatBuilders.decimalSIPrefix('bps'); kbn.valueFormats.Bps = kbn.formatBuilders.decimalSIPrefix('Bps'); kbn.valueFormats.KBs = kbn.formatBuilders.decimalSIPrefix('Bs', 1); - kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bits', 1); + kbn.valueFormats.Kbits = kbn.formatBuilders.decimalSIPrefix('bps', 1); kbn.valueFormats.MBs = kbn.formatBuilders.decimalSIPrefix('Bs', 2); - kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bits', 2); + kbn.valueFormats.Mbits = kbn.formatBuilders.decimalSIPrefix('bps', 2); kbn.valueFormats.GBs = kbn.formatBuilders.decimalSIPrefix('Bs', 3); - kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bits', 3); + kbn.valueFormats.Gbits = kbn.formatBuilders.decimalSIPrefix('bps', 3); // Throughput kbn.valueFormats.ops = kbn.formatBuilders.simpleCountUnit('ops'); diff --git a/public/app/features/alerting/alert_def.ts b/public/app/features/alerting/alert_def.ts index 8ee1981f3d8..52089560296 100644 --- a/public/app/features/alerting/alert_def.ts +++ b/public/app/features/alerting/alert_def.ts @@ -28,6 +28,11 @@ var evalFunctions = [ {text: 'HAS NO VALUE' , value: 'no_value'} ]; +var evalOperators = [ + {text: 'OR', value: 'or'}, + {text: 'AND', value: 'and'}, +]; + var reducerTypes = [ {text: 'avg()', value: 'avg'}, {text: 'min()', value: 'min'}, @@ -116,6 +121,7 @@ export default { getStateDisplayModel: getStateDisplayModel, conditionTypes: conditionTypes, evalFunctions: evalFunctions, + evalOperators: evalOperators, noDataModes: noDataModes, executionErrorModes: executionErrorModes, reducerTypes: reducerTypes, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index c882a6ac3bf..6757cfc0d91 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -18,6 +18,7 @@ export class AlertTabCtrl { alert: any; conditionModels: any; evalFunctions: any; + evalOperators: any; noDataModes: any; executionErrorModes: any; addNotificationSegment; @@ -41,6 +42,7 @@ export class AlertTabCtrl { this.$scope.ctrl = this; this.subTabIndex = 0; this.evalFunctions = alertDef.evalFunctions; + this.evalOperators = alertDef.evalOperators; this.conditionTypes = alertDef.conditionTypes; this.noDataModes = alertDef.noDataModes; this.executionErrorModes = alertDef.executionErrorModes; @@ -194,6 +196,7 @@ export class AlertTabCtrl { query: {params: ['A', '5m', 'now']}, reducer: {type: 'avg', params: []}, evaluator: {type: 'gt', params: [null]}, + operator: {type: 'and'}, }; } @@ -250,6 +253,7 @@ export class AlertTabCtrl { cm.queryPart = new QueryPart(source.query, alertDef.alertQueryDef); cm.reducerPart = alertDef.createReducerPart(source.reducer); cm.evaluator = source.evaluator; + cm.operator = source.operator; return cm; } diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 5b85ec105ae..f3fcf31fad6 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -38,23 +38,23 @@
[[.ErrorMsg]]+