From 6d0d07a55b0adf1de199cc1fcdf593786c7f6b27 Mon Sep 17 00:00:00 2001 From: Nick Triller Date: Mon, 28 May 2018 16:15:31 +0200 Subject: [PATCH 001/125] Document oauth_auto_login setting --- docs/sources/auth/overview.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index a372600ac46..d4360d554c1 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings. ```bash [auth] -disable_login_form ⁼ true +disable_login_form = true +``` + +### Automatic OAuth login + +Set to true to attempt login with OAuth automatically, skipping the login screen. +This setting is ignored if multiple OAuth providers are configured. +Defaults to `false`. + +```bash +[auth] +oauth_auto_login = true ``` ### Hide sign-out menu From 3414be18bc46167f7493d12360327d2c5477f1c4 Mon Sep 17 00:00:00 2001 From: Nick Triller Date: Mon, 28 May 2018 16:16:48 +0200 Subject: [PATCH 002/125] Implement oauth_auto_login setting Redirect in backend --- pkg/api/login.go | 22 ++++++++++++++++++++++ pkg/setting/setting.go | 2 ++ 2 files changed, 24 insertions(+) diff --git a/pkg/api/login.go b/pkg/api/login.go index 1083f89adfd..05afc40e59a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { viewData.Settings["loginError"] = loginError } + if tryOAuthAutoLogin(c) { + return + } + if !tryLoginUsingRememberCookie(c) { c.HTML(200, ViewIndex, viewData) return @@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { c.Redirect(setting.AppSubUrl + "/") } +func tryOAuthAutoLogin(c *m.ReqContext) bool { + if !setting.OAuthAutoLogin { + return false + } + oauthInfos := setting.OAuthService.OAuthInfos + if len(oauthInfos) != 1 { + log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.") + return false + } + for key := range setting.OAuthService.OAuthInfos { + redirectUrl := setting.AppSubUrl + "/login/" + key + log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl) + c.Redirect(redirectUrl, 307) + return true + } + return false +} + func tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 58901e55c6b..7543d91c463 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -108,6 +108,7 @@ var ( ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string + OAuthAutoLogin bool ViewersCanEdit bool // Http auth @@ -622,6 +623,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { auth := iniFile.Section("auth") DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) + OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) SignoutRedirectUrl = auth.Key("signout_redirect_url").String() // anonymous access From ccfd9c89b2645fde4b12aad0819c178ef5afb979 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Nov 2018 16:04:38 +0100 Subject: [PATCH 003/125] introduces hard coded deboucing for alerting --- pkg/services/alerting/eval_context.go | 21 ++- pkg/services/alerting/eval_context_test.go | 186 +++++++++++++-------- pkg/services/alerting/result_handler.go | 3 + pkg/services/alerting/rule.go | 5 + 4 files changed, 145 insertions(+), 70 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..49e28bbf5ec 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -69,7 +69,7 @@ func (c *EvalContext) GetStateModel() *StateDescription { Text: "Alerting", } default: - panic("Unknown rule state " + c.Rule.State) + panic("Unknown rule state for alert notifications " + c.Rule.State) } } @@ -125,11 +125,26 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.PrevAlertState } return c.Rule.ExecutionErrorState.ToAlertState() + } - } else if c.Firing { + if c.Firing && c.Rule.DebounceDuration != 0 { + since := time.Now().Sub(c.Rule.LastStateChange) + if since > c.Rule.DebounceDuration { + return m.AlertStateAlerting + } + + if c.PrevAlertState == m.AlertStateAlerting { + return m.AlertStateAlerting + } + + return m.AlertStatePending + } + + if c.Firing { return m.AlertStateAlerting + } - } else if c.NoDataFound { + if c.NoDataFound { c.log.Info("Alert Rule returned no data", "ruleId", c.Rule.Id, "name", c.Rule.Name, diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 750fa959683..2abf581d830 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -2,11 +2,11 @@ package alerting import ( "context" - "fmt" + "errors" "testing" + "time" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" ) func TestStateIsUpdatedWhenNeeded(t *testing.T) { @@ -31,71 +31,123 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) { }) } -func TestAlertingEvalContext(t *testing.T) { - Convey("Should compute and replace properly new rule state", t, func() { +func TestGetStateFromEvalContext(t *testing.T) { + tcs := []struct { + name string + expected models.AlertStateType + applyFn func(ec *EvalContext) + focus bool + }{ + { + name: "ok -> alerting", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.Firing = true + ec.PrevAlertState = models.AlertStateOK + }, + }, + { + name: "ok -> error(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + }, + }, + { + name: "ok -> pending. since its been firing for less than FOR", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + ec.Rule.DebounceDuration = time.Minute * 5 + }, + }, + { + name: "ok -> alerting. since its been firing for more than FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "alerting -> alerting. should not update regardless of FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateAlerting + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "ok -> ok. should not update regardless of FOR", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "ok -> error(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "pending -> error(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "ok -> no_data(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + }, + }, + { + name: "ok -> no_data(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + { + name: "pending -> no_data(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + } + + for _, tc := range tcs { ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - dummieError := fmt.Errorf("dummie error") - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Firing = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - - Convey("ok -> no_data(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataSetAlerting - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - }) + tc.applyFn(ctx) + have := ctx.GetNewState() + if have != tc.expected { + t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have)) + } + } } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 420ffeb9a55..ce12a8a6b96 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { // when two servers are raising. This makes sure that the server // with the last state change always sends a notification. evalContext.Rule.StateChanges = cmd.Result.StateChanges + + // Update the last state change of the alert rule in memory + evalContext.Rule.LastStateChange = time.Now() } // save annotation diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 999611f15c4..3fb69b48f9f 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" "strconv" + "time" "github.com/grafana/grafana/pkg/components/simplejson" @@ -18,6 +19,8 @@ type Rule struct { Frequency int64 Name string Message string + LastStateChange time.Time + DebounceDuration time.Duration NoDataState m.NoDataOption ExecutionErrorState m.ExecutionErrorOption State m.AlertStateType @@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency model.State = ruleDef.State + model.LastStateChange = ruleDef.NewStateDate + model.DebounceDuration = time.Minute * 2 // hard coded for now model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges From 2d3a5754891ddc093524dc72138272a72160694f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 09:00:56 +0100 Subject: [PATCH 004/125] adds db migration for debounce_duration --- pkg/models/alert.go | 27 ++++++++++--------- pkg/services/alerting/rule.go | 2 +- pkg/services/sqlstore/migrations/alert_mig.go | 4 +++ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index ba1fc0779ba..e35ba106688 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -59,19 +59,20 @@ func (s ExecutionErrorOption) ToAlertState() AlertStateType { } type Alert struct { - Id int64 - Version int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Name string - Message string - Severity string - State AlertStateType - Handler int64 - Silenced bool - ExecutionError string - Frequency int64 + Id int64 + Version int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Name string + Message string + Severity string //Unused + State AlertStateType + Handler int64 //Unused + Silenced bool + ExecutionError string + Frequency int64 + DebounceDuration time.Duration EvalData *simplejson.Json NewStateDate time.Time diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 3fb69b48f9f..c9fbddbf393 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = time.Minute * 2 // hard coded for now + model.DebounceDuration = time.Duration(ruleDef.DebounceDuration) model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 198a47b50ff..f575fb3b02b 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state)) mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) + + mg.AddMigration("Add decounce_duration to alert table", NewAddColumnMigration(alertV1, &Column{ + Name: "debounce_duration", Type: DB_BigInt, Nullable: true, + })) } From 4526660cb2ad1e5a1b555cf15b4c89c4e471ad6d Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 10:38:02 +0100 Subject: [PATCH 005/125] wire up debounce setting in the ui --- pkg/services/alerting/extractor.go | 27 +- pkg/services/sqlstore/alert.go | 3 +- public/app/features/alerting/AlertTabCtrl.ts | 1 + .../features/alerting/partials/alert_tab.html | 272 +++++++++--------- 4 files changed, 164 insertions(+), 139 deletions(-) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index edfab2dedee..221d58feaf2 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,6 +2,7 @@ package alerting import ( "errors" + "time" "fmt" @@ -113,15 +114,25 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } + rawDebouce := jsonAlert.Get("debounceDuration").MustString() + var debounceDuration time.Duration + if rawDebouce != "" { + debounceDuration, err = time.ParseDuration(rawDebouce) + if err != nil { + return nil, ValidationError{Reason: "Could not parse debounceDuration"} + } + } + alert := &m.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgID, - PanelId: panelID, - Id: jsonAlert.Get("id").MustInt64(), - Name: jsonAlert.Get("name").MustString(), - Handler: jsonAlert.Get("handler").MustInt64(), - Message: jsonAlert.Get("message").MustString(), - Frequency: frequency, + DashboardId: e.Dash.Id, + OrgId: e.OrgID, + PanelId: panelID, + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Handler: jsonAlert.Get("handler").MustInt64(), + Message: jsonAlert.Get("message").MustString(), + Frequency: frequency, + DebounceDuration: debounceDuration, } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 2f17402b80c..88ffa3a9b4e 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -193,7 +193,8 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message") + sess.MustCols("message", "debounce_duration") + _, err := sess.ID(alert.Id).Update(alert) if err != nil { return err diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 146b7026353..43752d0a8c9 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,6 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; + alert.debounceDuration = alert.debounceDuration || '5m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index cb101672aa4..de5fc4df382 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,147 +1,159 @@
- -
-
-
- {{ctrl.error}} -
+
+
+
+ {{ctrl.error}} +
-
-
Alert Config
-
- Name - - Evaluate every - -
-
+
+
Alert Config
+
+ Name + +
+
+
+ Evaluate every + +
+
+ + + + Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. + This should reduce false positive alerts and avoid flapping alerts. + +
+
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+
Conditions
+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
- -
-
+
+ +
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+
+ If no data or all values are null + SET STATE TO +
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ If execution error or timeout + SET STATE TO +
+ +
+
-
- -
-
+
+ +
+
-
- Evaluating rule -
+
+ Evaluating rule +
-
- -
-
+
+ +
+
-
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
+
+
Notifications
+
+
+ Send to + +  {{nc.name}}  + + + +
+
+
+ Message + +
+
-
- -
- State history (last 50 state changes) -
+
+ +
+ State history (last 50 state changes) +
-
-
- No state changes recorded -
+
+
+ No state changes recorded +
  1. From 6f748d8a96ee5a1ee21fb0f9c9309724086252eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 13:53:47 +0100 Subject: [PATCH 006/125] fixes go meta lint issue --- pkg/services/alerting/rule.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index c9fbddbf393..ac1885fb380 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = time.Duration(ruleDef.DebounceDuration) + model.DebounceDuration = ruleDef.DebounceDuration model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges From d25284a36441ef3ec2f1e8953acbd7fb4680f7a9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 10:23:43 +0100 Subject: [PATCH 007/125] introduce state `unknown` for rules that have not been evaluated yet --- pkg/api/alerting.go | 2 +- pkg/models/alert.go | 8 +++++++- pkg/services/alerting/eval_context.go | 5 +++++ pkg/services/alerting/notifiers/base.go | 5 +++++ pkg/services/alerting/notifiers/base_test.go | 16 ++++++++++++++++ pkg/services/sqlstore/alert.go | 6 +++--- pkg/services/sqlstore/alert_test.go | 4 ++-- public/app/features/alerting/state/alertDef.ts | 7 +++++++ 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a936d696207..b007b3a3492 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -291,7 +291,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response { return Error(500, "", err) } - var response m.AlertStateType = m.AlertStatePending + var response m.AlertStateType = m.AlertStateUnknown pausedState := "un-paused" if cmd.Paused { response = m.AlertStatePaused diff --git a/pkg/models/alert.go b/pkg/models/alert.go index e35ba106688..37f40134796 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -19,6 +19,7 @@ const ( AlertStateAlerting AlertStateType = "alerting" AlertStateOK AlertStateType = "ok" AlertStatePending AlertStateType = "pending" + AlertStateUnknown AlertStateType = "unknown" ) const ( @@ -39,7 +40,12 @@ var ( ) func (s AlertStateType) IsValid() bool { - return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending + return s == AlertStateOK || + s == AlertStateNoData || + s == AlertStatePaused || + s == AlertStatePending || + s == AlertStateAlerting || + s == AlertStateUnknown } func (s NoDataOption) IsValid() bool { diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 49e28bbf5ec..8986af85406 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -68,6 +68,11 @@ func (c *EvalContext) GetStateModel() *StateDescription { Color: "#D63232", Text: "Alerting", } + case m.AlertStateUnknown: + return &StateDescription{ + Color: "888888", + Text: "Unknown", + } default: panic("Unknown rule state for alert notifications " + c.Rule.State) } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index d141d6cd257..35d3ff518a0 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -67,6 +67,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC } // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK { + return false + } + + // Do not notify when we become OK from pending if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5062828cb4f..388c2db17ee 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -132,6 +132,22 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateOK, state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()}, + expect: true, + }, + { + name: "unknown -> ok", + prevState: m.AlertStateUnknown, + newState: m.AlertStateOK, + state: &m.AlertNotificationState{}, + + expect: false, + }, + { + name: "unknown -> alerting", + prevState: m.AlertStateUnknown, + newState: m.AlertStateAlerting, + state: &m.AlertNotificationState{}, + expect: true, }, } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 88ffa3a9b4e..78a71cc8497 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -205,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS } else { alert.Updated = timeNow() alert.Created = timeNow() - alert.State = m.AlertStatePending + alert.State = m.AlertStateUnknown alert.NewStateDate = timeNow() _, err := sess.Insert(alert) @@ -300,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error { params = append(params, string(m.AlertStatePaused)) params = append(params, timeNow()) } else { - params = append(params, string(m.AlertStatePending)) + params = append(params, string(m.AlertStateUnknown)) params = append(params, timeNow()) } @@ -324,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error { if cmd.Paused { newState = string(m.AlertStatePaused) } else { - newState = string(m.AlertStatePending) + newState = string(m.AlertStateUnknown) } res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow()) diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index d97deb45f0e..40867e96b4d 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) { So(alert.DashboardId, ShouldEqual, testDash.Id) So(alert.PanelId, ShouldEqual, 1) So(alert.Name, ShouldEqual, "Alerting title") - So(alert.State, ShouldEqual, "pending") + So(alert.State, ShouldEqual, m.AlertStateUnknown) So(alert.NewStateDate, ShouldNotBeNil) So(alert.EvalData, ShouldNotBeNil) So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test") @@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) { So(query.Result[0].Name, ShouldEqual, "Name") Convey("Alert state should not be updated", func() { - So(query.Result[0].State, ShouldEqual, "pending") + So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown) }) }) diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index 11d2aafaa7f..378be0afb91 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -99,6 +99,13 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-warning', }; } + case 'unknown': { + return { + text: 'UNKNOWN', + iconClass: 'fa fa-question', + stateClass: 'alert-state-paused', + }; + } } throw { message: 'Unknown alert state' }; From ccd89eee974d08f7f74f7859767e8f32a347e2a5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 11:05:30 +0100 Subject: [PATCH 008/125] renames `debouceduration` to `for` --- pkg/models/alert.go | 28 +++++++++---------- pkg/services/alerting/eval_context.go | 4 +-- pkg/services/alerting/eval_context_test.go | 8 +++--- pkg/services/alerting/extractor.go | 28 +++++++++---------- pkg/services/alerting/rule.go | 4 +-- pkg/services/sqlstore/migrations/alert_mig.go | 4 +-- public/app/features/alerting/AlertTabCtrl.ts | 2 +- .../features/alerting/partials/alert_tab.html | 12 ++++---- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 37f40134796..760e9eada48 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -65,20 +65,20 @@ func (s ExecutionErrorOption) ToAlertState() AlertStateType { } type Alert struct { - Id int64 - Version int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Name string - Message string - Severity string //Unused - State AlertStateType - Handler int64 //Unused - Silenced bool - ExecutionError string - Frequency int64 - DebounceDuration time.Duration + Id int64 + Version int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Name string + Message string + Severity string //Unused + State AlertStateType + Handler int64 //Unused + Silenced bool + ExecutionError string + Frequency int64 + For time.Duration EvalData *simplejson.Json NewStateDate time.Time diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 8986af85406..208fe1d188b 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -132,9 +132,9 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.Rule.ExecutionErrorState.ToAlertState() } - if c.Firing && c.Rule.DebounceDuration != 0 { + if c.Firing && c.Rule.For != 0 { since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.DebounceDuration { + if since > c.Rule.For { return m.AlertStateAlerting } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 2abf581d830..cc0bed79d10 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -62,7 +62,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) - ec.Rule.DebounceDuration = time.Minute * 5 + ec.Rule.For = time.Minute * 5 }, }, { @@ -72,7 +72,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { @@ -82,7 +82,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateAlerting ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { @@ -91,7 +91,7 @@ func TestGetStateFromEvalContext(t *testing.T) { applyFn: func(ec *EvalContext) { ec.PrevAlertState = models.AlertStateOK ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 221d58feaf2..244dc0a0770 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -114,25 +114,25 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } - rawDebouce := jsonAlert.Get("debounceDuration").MustString() - var debounceDuration time.Duration - if rawDebouce != "" { - debounceDuration, err = time.ParseDuration(rawDebouce) + rawFow := jsonAlert.Get("for").MustString() + var forValue time.Duration + if rawFow != "" { + forValue, err = time.ParseDuration(rawFow) if err != nil { - return nil, ValidationError{Reason: "Could not parse debounceDuration"} + return nil, ValidationError{Reason: "Could not parse for"} } } alert := &m.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgID, - PanelId: panelID, - Id: jsonAlert.Get("id").MustInt64(), - Name: jsonAlert.Get("name").MustString(), - Handler: jsonAlert.Get("handler").MustInt64(), - Message: jsonAlert.Get("message").MustString(), - Frequency: frequency, - DebounceDuration: debounceDuration, + DashboardId: e.Dash.Id, + OrgId: e.OrgID, + PanelId: panelID, + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Handler: jsonAlert.Get("handler").MustInt64(), + Message: jsonAlert.Get("message").MustString(), + Frequency: frequency, + For: forValue, } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index ac1885fb380..d2a505145ac 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -20,7 +20,7 @@ type Rule struct { Name string Message string LastStateChange time.Time - DebounceDuration time.Duration + For time.Duration NoDataState m.NoDataOption ExecutionErrorState m.ExecutionErrorOption State m.AlertStateType @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = ruleDef.DebounceDuration + model.For = ruleDef.For model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index f575fb3b02b..b5aeb26483c 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -134,7 +134,7 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) - mg.AddMigration("Add decounce_duration to alert table", NewAddColumnMigration(alertV1, &Column{ - Name: "debounce_duration", Type: DB_BigInt, Nullable: true, + mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{ + Name: "for", Type: DB_BigInt, Nullable: true, })) } diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 43752d0a8c9..758b3273d1a 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,7 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; - alert.debounceDuration = alert.debounceDuration || '5m'; + alert.for = alert.for || '5m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index de5fc4df382..676a1d32937 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -28,16 +28,16 @@
    Alert Config
    Name - +
    - Evaluate every - + Evaluate every +
    -
    - - +
    + + Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. This should reduce false positive alerts and avoid flapping alerts. From ae2d536740136ba538b5d24605b48905dacfdc8e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 13:14:02 +0100 Subject: [PATCH 009/125] adds tests for extracting for property --- pkg/services/alerting/extractor.go | 9 ++++----- pkg/services/alerting/extractor_test.go | 20 ++++++++++++------- .../collapsed-panels.json | 0 .../dash-without-id.json | 0 .../graphite-alert.json | 1 + .../influxdb-alert.json | 0 .../panel-with-id-0.json | 0 .../panels-missing-id.json | 0 .../{test-data => testdata}/v5-dashboard.json | 0 9 files changed, 18 insertions(+), 12 deletions(-) rename pkg/services/alerting/{test-data => testdata}/collapsed-panels.json (100%) rename pkg/services/alerting/{test-data => testdata}/dash-without-id.json (100%) rename pkg/services/alerting/{test-data => testdata}/graphite-alert.json (98%) rename pkg/services/alerting/{test-data => testdata}/influxdb-alert.json (100%) rename pkg/services/alerting/{test-data => testdata}/panel-with-id-0.json (100%) rename pkg/services/alerting/{test-data => testdata}/panels-missing-id.json (100%) rename pkg/services/alerting/{test-data => testdata}/v5-dashboard.json (100%) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 244dc0a0770..0d902b388a8 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,9 +2,8 @@ package alerting import ( "errors" - "time" - "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -114,10 +113,10 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } - rawFow := jsonAlert.Get("for").MustString() + rawFor := jsonAlert.Get("for").MustString() var forValue time.Duration - if rawFow != "" { - forValue, err = time.ParseDuration(rawFow) + if rawFor != "" { + forValue, err = time.ParseDuration(rawFor) if err != nil { return nil, ValidationError{Reason: "Could not parse for"} } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index e2dc01a1181..d03565ead90 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -3,6 +3,7 @@ package alerting import ( "io/ioutil" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - json, err := ioutil.ReadFile("./test-data/graphite-alert.json") + json, err := ioutil.ReadFile("./testdata/graphite-alert.json") So(err, ShouldBeNil) Convey("Extractor should not modify the original json", func() { @@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) { So(alerts[1].PanelId, ShouldEqual, 4) }) + Convey("should extract for param", func() { + So(alerts[0].For, ShouldEqual, time.Minute*2) + So(alerts[1].For, ShouldEqual, time.Duration(0)) + }) + Convey("should extract name and desc", func() { So(alerts[0].Name, ShouldEqual, "name1") So(alerts[0].Message, ShouldEqual, "desc1") @@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panels missing id should return error", func() { - panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json") + panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithoutId) @@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panel with id set to zero should return error", func() { - panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json") + panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithIdZero) @@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse alerts from dashboard without rows", func() { - json, err := ioutil.ReadFile("./test-data/v5-dashboard.json") + json, err := ioutil.ReadFile("./testdata/v5-dashboard.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard containing influxdb alert", func() { - json, err := ioutil.ReadFile("./test-data/influxdb-alert.json") + json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Should be able to extract collapsed panels", func() { - json, err := ioutil.ReadFile("./test-data/collapsed-panels.json") + json, err := ioutil.ReadFile("./testdata/collapsed-panels.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard without id and containing an alert", func() { - json, err := ioutil.ReadFile("./test-data/dash-without-id.json") + json, err := ioutil.ReadFile("./testdata/dash-without-id.json") So(err, ShouldBeNil) dashJSON, err := simplejson.NewJson(json) diff --git a/pkg/services/alerting/test-data/collapsed-panels.json b/pkg/services/alerting/testdata/collapsed-panels.json similarity index 100% rename from pkg/services/alerting/test-data/collapsed-panels.json rename to pkg/services/alerting/testdata/collapsed-panels.json diff --git a/pkg/services/alerting/test-data/dash-without-id.json b/pkg/services/alerting/testdata/dash-without-id.json similarity index 100% rename from pkg/services/alerting/test-data/dash-without-id.json rename to pkg/services/alerting/testdata/dash-without-id.json diff --git a/pkg/services/alerting/test-data/graphite-alert.json b/pkg/services/alerting/testdata/graphite-alert.json similarity index 98% rename from pkg/services/alerting/test-data/graphite-alert.json rename to pkg/services/alerting/testdata/graphite-alert.json index 5f23e224f9a..3cb4ae1dd22 100644 --- a/pkg/services/alerting/test-data/graphite-alert.json +++ b/pkg/services/alerting/testdata/graphite-alert.json @@ -23,6 +23,7 @@ "message": "desc1", "handler": 1, "frequency": "60s", + "for": "2m", "conditions": [ { "type": "query", diff --git a/pkg/services/alerting/test-data/influxdb-alert.json b/pkg/services/alerting/testdata/influxdb-alert.json similarity index 100% rename from pkg/services/alerting/test-data/influxdb-alert.json rename to pkg/services/alerting/testdata/influxdb-alert.json diff --git a/pkg/services/alerting/test-data/panel-with-id-0.json b/pkg/services/alerting/testdata/panel-with-id-0.json similarity index 100% rename from pkg/services/alerting/test-data/panel-with-id-0.json rename to pkg/services/alerting/testdata/panel-with-id-0.json diff --git a/pkg/services/alerting/test-data/panels-missing-id.json b/pkg/services/alerting/testdata/panels-missing-id.json similarity index 100% rename from pkg/services/alerting/test-data/panels-missing-id.json rename to pkg/services/alerting/testdata/panels-missing-id.json diff --git a/pkg/services/alerting/test-data/v5-dashboard.json b/pkg/services/alerting/testdata/v5-dashboard.json similarity index 100% rename from pkg/services/alerting/test-data/v5-dashboard.json rename to pkg/services/alerting/testdata/v5-dashboard.json From 3789583014fc6d1e2de0eb03b479953feea0e696 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 13:51:35 +0100 Subject: [PATCH 010/125] for: use 0m as default for existing alerts and 5m for new --- public/app/features/alerting/AlertTabCtrl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 758b3273d1a..2efd3c062c4 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,7 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; - alert.for = alert.for || '5m'; + alert.for = alert.for || '0m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; @@ -355,6 +355,7 @@ export class AlertTabCtrl { enable() { this.panel.alert = {}; this.initModel(); + this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes } evaluatorParamsChanged() { From 0ddfd92f8c5fa9c3a4a2204de21f76db21c8c73c Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Nov 2018 22:30:23 +0100 Subject: [PATCH 011/125] adds debounce duration for alert dashboards in ha_test --- devenv/docker/ha_test/docker-compose.yaml | 2 +- devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml index ce8630d88a4..1195e2a977c 100644 --- a/devenv/docker/ha_test/docker-compose.yaml +++ b/devenv/docker/ha_test/docker-compose.yaml @@ -9,7 +9,7 @@ services: - /var/run/docker.sock:/tmp/docker.sock:ro db: - image: mysql + image: mysql:5.6 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: grafana diff --git a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet index 86ded7e79d6..e9b8abfbb9c 100644 --- a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet +++ b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet @@ -39,6 +39,7 @@ local alertDashboardTemplate = { "executionErrorState": "alerting", "frequency": "10s", "handler": 1, + "for": "1m", "name": "bulk alerting", "noDataState": "no_data", "notifications": [ From 975f0aa064634c9181418cf50e8598d5a0cb747a Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 11:25:37 +0100 Subject: [PATCH 012/125] alerting: adds docs about the for setting --- docs/sources/alerting/rules.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 488619055e2..2e4a7e5c191 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of ## Rule Config -{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table** panels as well in a future release. @@ -48,6 +48,16 @@ panels as well in a future release. Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule. +### For + +> This setting is available in Grafana 5.4 and above. + +The `For` setting allows you to specify a duration for which the alert has to violate the threshold before switching to `Alerting` state and sending notifications. This is useful when you want to reduce the amount of false positive alerts and problems from which the system selfheal. Which in case a human does not need to be woken up. + +Typically, it's always a good idea to use this setting since its often worse to get false positive than wait a few minutes before the alert notification triggers. + +{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + ### Conditions Currently the only condition type that exists is a `Query` condition that allows you to @@ -57,11 +67,11 @@ specify a query letter, time range and an aggregation function. ### Query condition example ```sql -avg() OF query(A, 5m, now) IS BELOW 14 +avg() OF query(A, 15m, now) IS BELOW 14 ``` - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function. -- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. +- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. - `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold. The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially. From aa1b80fe45b2a8fdf7afcb3e62e6ba54cbed112a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Nov 2018 14:16:58 +0100 Subject: [PATCH 013/125] docs: improve helper test for `For` --- docs/sources/alerting/rules.md | 2 +- public/app/features/alerting/partials/alert_tab.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 2e4a7e5c191..387132ecfeb 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -52,7 +52,7 @@ Here you can specify the name of the alert rule and how often the scheduler shou > This setting is available in Grafana 5.4 and above. -The `For` setting allows you to specify a duration for which the alert has to violate the threshold before switching to `Alerting` state and sending notifications. This is useful when you want to reduce the amount of false positive alerts and problems from which the system selfheal. Which in case a human does not need to be woken up. +If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. Typically, it's always a good idea to use this setting since its often worse to get false positive than wait a few minutes before the alert notification triggers. diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 676a1d32937..2b5e9bdf0cb 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -39,8 +39,8 @@ - Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. - This should reduce false positive alerts and avoid flapping alerts. + If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
    From 2fb78a50d6a1572debb2f1227adaba1f29295af3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 12 Nov 2018 10:50:56 +0100 Subject: [PATCH 014/125] minor fixes based on code review --- pkg/services/alerting/eval_context.go | 4 ++-- pkg/services/sqlstore/alert.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 208fe1d188b..23d3efa8bea 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -70,11 +70,11 @@ func (c *EvalContext) GetStateModel() *StateDescription { } case m.AlertStateUnknown: return &StateDescription{ - Color: "888888", + Color: "#888888", Text: "Unknown", } default: - panic("Unknown rule state for alert notifications " + c.Rule.State) + panic("Unknown rule state for alert " + c.Rule.State) } } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 78a71cc8497..62ab348664f 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -193,7 +193,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message", "debounce_duration") + sess.MustCols("message", "for") _, err := sess.ID(alert.Id).Update(alert) if err != nil { From ff0ed06441f5e151a5d314e1513edd3750d56408 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 12 Nov 2018 17:40:05 +0000 Subject: [PATCH 015/125] Explore: Don't suggest term items when text follows Tab completion gets in the way when constructing a query from the inside out: ``` up| => |up => sum(|up) ``` At that point the language provider will not suggest anything. --- .../prometheus/language_provider.ts | 31 +++++-- .../specs/language_provider.test.ts | 89 ++++++++++++++++--- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index ac54b08526d..326ab93f2ef 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -78,9 +78,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; // Keep this DOM-free for testing - provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput { + provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { // Syntax spans have 3 classes by default. More indicate a recognized token const tokenRecognized = wrapperClasses.length > 3; + + // Local text properties + const empty = value.document.text.length === 0; + const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); + const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; + const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -90,13 +97,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { return this.getAggregationCompletionItems.apply(this, arguments); + } else if (empty) { + return this.getEmptyCompletionItems(context || {}); } else if ( // Show default suggestions in a couple of scenarios (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - (prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace + // Empty prefix, but not directly following a closing brace (e.g., `]|`), or not succeeded by anything except a closing parens, e.g., `sum(|)` + (prefix === '' && !text.match(/^[\]})\s]+$/) && (!nextCharacter || nextCharacter === ')')) || text.match(/[+\-*/^%]/) // Anything after binary operator ) { - return this.getEmptyCompletionItems(context || {}); + return this.getTermCompletionItems(); } return { @@ -106,8 +116,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { getEmptyCompletionItems(context: any): TypeaheadOutput { const { history } = context; - const { metrics } = this; - const suggestions: CompletionItemGroup[] = []; + let suggestions: CompletionItemGroup[] = []; if (history && history.length > 0) { const historyItems = _.chain(history) @@ -126,13 +135,23 @@ export default class PromQlLanguageProvider extends LanguageProvider { }); } + const termCompletionItems = this.getTermCompletionItems(); + suggestions = [...suggestions, ...termCompletionItems.suggestions]; + + return { suggestions }; + } + + getTermCompletionItems(): TypeaheadOutput { + const { metrics } = this; + const suggestions: CompletionItemGroup[] = []; + suggestions.push({ prefixMatch: true, label: 'Functions', items: FUNCTIONS.map(setFunctionKind), }); - if (metrics) { + if (metrics && metrics.length > 0) { suggestions.push({ label: 'Metrics', items: metrics.map(wrapLabel), diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 784a8b59739..bcb8cb34082 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -7,18 +7,47 @@ describe('Language completion provider', () => { metadataRequest: () => ({ data: { data: [] } }), }; - it('returns default suggestions on emtpty context', () => { - const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + describe('empty query suggestions', () => { + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + ]); + }); + + it('returns default suggestions with metrics on emtpty context when metrics were provided', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); }); describe('range suggestions', () => { it('returns range suggestions in range context', () => { const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); + const value = Plain.deserialize('1'); + const result = instance.provideCompletionItems({ + text: '1', + prefix: '1', + value, + wrapperClasses: ['context-range'], + }); expect(result.context).toBe('context-range'); expect(result.refresher).toBeUndefined(); expect(result.suggestions).toEqual([ @@ -31,20 +60,54 @@ describe('Language completion provider', () => { }); describe('metric suggestions', () => { - it('returns metrics suggestions by default', () => { + it('returns metrics and function suggestions in an unknown context', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] }); + const value = Plain.deserialize('a'); + const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); }); - it('returns default suggestions after a binary operator', () => { + it('returns metrics and function suggestions after a binary operator', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] }); + const value = Plain.deserialize('*'); + const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); + + it('returns no suggestions at the beginning of a non-empty function', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize('sum(up)'); + const range = value.selection.merge({ + anchorOffset: 4, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + value: valueWithSelection, + wrapperClasses: [], + }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); }); }); From 1958de72207b15eb0b55604dca0bcdc37dfcaa07 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 13 Nov 2018 11:51:06 +0100 Subject: [PATCH 016/125] devenv: update alerting with testdata dashboard --- devenv/dev-dashboards/testdata_alerts.json | 796 ++++++++++++++------- 1 file changed, 543 insertions(+), 253 deletions(-) diff --git a/devenv/dev-dashboards/testdata_alerts.json b/devenv/dev-dashboards/testdata_alerts.json index 8c2edebf155..8fd7d4d9db5 100644 --- a/devenv/dev-dashboards/testdata_alerts.json +++ b/devenv/dev-dashboards/testdata_alerts.json @@ -1,250 +1,546 @@ { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 60 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "frequency": "60s", + "handler": 1, + "name": "TestData - Always OK", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always OK", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": "125", + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "executionErrorState": "alerting", + "for": "0m", + "frequency": "60s", + "handler": 1, + "name": "TestData - Always Alerting", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always Alerting", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "5m", + "frequency": "1m", + "handler": 1, + "name": "TestData - No data", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 5, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "no_data_points", + "stringInput": "", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 1 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "No data", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "1m", + "frequency": "1m", + "handler": 1, + "name": "TestData - Always Alerting with For", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 6, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always Alerting with For", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], "revision": 2, - "title": "Alerting with TestData", + "schemaVersion": 16, + "style": "dark", "tags": [ "grafana-test" ], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": 255.625, - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 60 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always OK", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 3, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "1,20,90,30,5,0", - "target": "" - } - ], - "thresholds": [ - { - "value": 60, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always OK", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": "125", - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 177 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always Alerting", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 4, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "200,445,100,150,200,220,190", - "target": "" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 177 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always Alerting", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - } - ], + "templating": { + "list": [] + }, "time": { "from": "now-6h", "to": "now" @@ -274,14 +570,8 @@ "30d" ] }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 4, - "links": [], - "gnetId": null -} + "timezone": "browser", + "title": "Alerting with TestData", + "uid": "7MeksYbmk", + "version": 1 +} \ No newline at end of file From 63be43e3b2c1ce5cfa0c8db3fca20b6f591bdb11 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 21 Jun 2018 14:41:47 +0200 Subject: [PATCH 017/125] graph: Time region support --- public/app/plugins/panel/graph/graph.ts | 6 + public/app/plugins/panel/graph/module.ts | 2 + .../graph/specs/time_region_manager.test.ts | 217 +++++++++++++++ .../app/plugins/panel/graph/tab_display.html | 9 + .../plugins/panel/graph/thresholds_form.html | 77 ++++++ .../plugins/panel/graph/thresholds_form.ts | 82 +----- .../panel/graph/time_region_manager.ts | 249 ++++++++++++++++++ .../panel/graph/time_regions_form.html | 64 +++++ .../plugins/panel/graph/time_regions_form.ts | 73 +++++ 9 files changed, 698 insertions(+), 81 deletions(-) create mode 100644 public/app/plugins/panel/graph/specs/time_region_manager.test.ts create mode 100644 public/app/plugins/panel/graph/thresholds_form.html create mode 100644 public/app/plugins/panel/graph/time_region_manager.ts create mode 100644 public/app/plugins/panel/graph/time_regions_form.html create mode 100644 public/app/plugins/panel/graph/time_regions_form.ts diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 01afd0716e6..c5f98792568 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -16,6 +16,7 @@ import { tickStep } from 'app/core/utils/ticks'; import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; +import { TimeRegionManager } from './time_region_manager'; import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; @@ -38,6 +39,7 @@ class GraphElement { panelWidth: number; eventManager: EventManager; thresholdManager: ThresholdManager; + timeRegionManager: TimeRegionManager; legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { @@ -49,6 +51,7 @@ class GraphElement { this.panelWidth = 0; this.eventManager = new EventManager(this.ctrl); this.thresholdManager = new ThresholdManager(this.ctrl); + this.timeRegionManager = new TimeRegionManager(this.ctrl); this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => { return this.sortedSeries; }); @@ -125,6 +128,7 @@ class GraphElement { onPanelTeardown() { this.thresholdManager = null; + this.timeRegionManager = null; if (this.plot) { this.plot.destroy(); @@ -215,6 +219,7 @@ class GraphElement { } this.thresholdManager.draw(plot); + this.timeRegionManager.draw(plot); } processOffsetHook(plot, gridMargin) { @@ -293,6 +298,7 @@ class GraphElement { this.prepareXAxis(options, this.panel); this.configureYAxisOptions(this.data, options); this.thresholdManager.addFlotOptions(options, this.panel); + this.timeRegionManager.addFlotOptions(options, this.panel); this.eventManager.addFlotEvents(this.annotations, options); this.sortedSeries = this.sortSeries(this.data, this.panel); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a6c5190d937..5b48636de5f 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -1,6 +1,7 @@ import './graph'; import './series_overrides_ctrl'; import './thresholds_form'; +import './time_regions_form'; import template from './template'; import _ from 'lodash'; @@ -111,6 +112,7 @@ class GraphCtrl extends MetricsPanelCtrl { // other style overrides seriesOverrides: [], thresholds: [], + timeRegions: [], }; /** @ngInject */ diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts new file mode 100644 index 00000000000..d1b2290cb61 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -0,0 +1,217 @@ +import { TimeRegionManager, colorModes } from '../time_region_manager'; +import moment from 'moment'; + +describe('TimeRegionManager', () => { + function plotOptionsScenario(desc, func) { + describe(desc, () => { + const ctx: any = { + panel: { + timeRegions: [], + }, + options: { + grid: { markings: [] }, + }, + panelCtrl: { + range: {}, + dashboard: { + isTimezoneUtc: () => false, + }, + }, + }; + + ctx.setup = (regions, from, to) => { + ctx.panel.timeRegions = regions; + ctx.panelCtrl.range.from = from; + ctx.panelCtrl.range.to = to; + const manager = new TimeRegionManager(ctx.panelCtrl); + manager.addFlotOptions(ctx.options, ctx.panel); + }; + + ctx.printScenario = () => { + console.log(`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`); + ctx.options.grid.markings.forEach((m, i) => { + console.log( + `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}` + ); + }); + }; + + func(ctx); + }); + } + + describe('When creating plot markings', () => { + plotOptionsScenario('for day of week region', ctx => { + const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; + const from = moment('2018-01-01 00:00'); + const to = moment('2018-01-01 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add fill', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[0].xaxis.from).format()).toBe(from.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(to.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + }); + + it('should add line before', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[1].xaxis.from).format()).toBe(from.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(from.format()); + expect(markings[1].color).toBe(colorModes.red.color.line); + }); + + it('should add line after', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[2].xaxis.from).format()).toBe(to.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(to.format()); + expect(markings[2].color).toBe(colorModes.red.color.line); + }); + }); + + plotOptionsScenario('for time from region', ctx => { + const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 00:00'); + const to = moment('2018-01-03 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + const firstFill = moment(from.add(5, 'hours')); + expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + const secondFill = moment(firstFill).add(1, 'days'); + expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + const thirdFill = moment(secondFill).add(1, 'days'); + expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for time to region', ctx => { + const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-02-01 00:00'); + const to = moment('2018-02-03 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + const firstFill = moment(from.add(5, 'hours')); + expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + const secondFill = moment(firstFill).add(1, 'days'); + expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + const thirdFill = moment(secondFill).add(1, 'days'); + expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from/to region', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from region', ctx => { + const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week to region', ctx => { + const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + }); +}); diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index ebc6cf9b18e..d407f30ffc8 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -14,6 +14,11 @@ Thresholds ({{ctrl.panel.thresholds.length}})
  2. +
  3. + + Time regions ({{ctrl.panel.timeRegions.length}}) + +
  4. @@ -132,4 +137,8 @@
+
+ +
+
diff --git a/public/app/plugins/panel/graph/thresholds_form.html b/public/app/plugins/panel/graph/thresholds_form.html new file mode 100644 index 00000000000..81877150a47 --- /dev/null +++ b/public/app/plugins/panel/graph/thresholds_form.html @@ -0,0 +1,77 @@ +
+
Thresholds
+

+ Visual thresholds options disabled. + Visit the Alert tab update your thresholds.
+ To re-enable thresholds, the alert rule must be deleted from this panel. +

+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ + + + +
+ + + +
+ + + + +
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index 5f1edb8aa9a..4f480873d5b 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -58,90 +58,10 @@ export class ThresholdFormCtrl { } } -const template = ` -
-
Thresholds
-

- Visual thresholds options disabled. - Visit the Alert tab update your thresholds.
- To re-enable thresholds, the alert rule must be deleted from this panel. -

-
-
-
- -
- -
-
- -
- -
- -
- -
- -
-
- - - -
- - - - -
- - - -
- - - - -
- -
- -
- -
-
- -
- -
-
- -
- -
-
-
-`; - coreModule.directive('graphThresholdForm', () => { return { restrict: 'E', - template: template, + templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html', controller: ThresholdFormCtrl, bindToController: true, controllerAs: 'ctrl', diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts new file mode 100644 index 00000000000..c3c6aadaa31 --- /dev/null +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -0,0 +1,249 @@ +import 'vendor/flot/jquery.flot'; +import _ from 'lodash'; +import moment from 'moment'; +import config from 'app/core/config'; + +export const colorModes = { + custom: { title: 'Custom' }, + red: { + title: 'Red', + color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' }, + }, + yellow: { + title: 'Yellow', + color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, + }, + green: { + title: 'Green', + color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' }, + }, + background3: { + themeDependent: true, + title: 'Background (3%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.03)', line: 'rgba(255, 255, 255, 0.1)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.03)', line: 'rgba(0, 0, 0, 0.1)' }, + }, + background6: { + themeDependent: true, + title: 'Background (6%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.06)', line: 'rgba(255, 255, 255, 0.15)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.06)', line: 'rgba(0, 0, 0, 0.15)' }, + }, + background9: { + themeDependent: true, + title: 'Background (9%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + }, +}; + +export function getColorModes() { + return _.map(Object.keys(colorModes), key => { + return { + key: key, + value: colorModes[key].title, + }; + }); +} + +function getColor(timeRegion) { + if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) { + timeRegion.colorMode = 'red'; + } + + if (timeRegion.colorMode === 'custom') { + return { + fill: timeRegion.fillColor, + line: timeRegion.lineColor, + }; + } + + const colorMode = colorModes[timeRegion.colorMode]; + if (colorMode.themeDependent === true) { + return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor; + } + + return colorMode.color; +} + +export class TimeRegionManager { + plot: any; + timeRegions: any; + + constructor(private panelCtrl) {} + + draw(plot) { + this.timeRegions = this.panelCtrl.panel.timeRegions; + this.plot = plot; + } + + addFlotOptions(options, panel) { + if (!panel.timeRegions || panel.timeRegions.length === 0) { + return; + } + + const tRange = this.panelCtrl.dashboard.isTimezoneUtc() + ? { from: this.panelCtrl.range.from, to: this.panelCtrl.range.to } + : { from: this.panelCtrl.range.from.local(), to: this.panelCtrl.range.to.local() }; + + let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; + + for (i = 0; i < panel.timeRegions.length; i++) { + timeRegion = panel.timeRegions[i]; + + if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) { + continue; + } + + hRange = { + from: this.parseTimeRange(timeRegion.from), + to: this.parseTimeRange(timeRegion.to), + }; + + if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) { + timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek; + } + + if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) { + timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek; + } + + if (timeRegion.fromDayOfWeek) { + hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek); + } + + if (timeRegion.toDayOfWeek) { + hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek); + } + + if (!hRange.from.h && hRange.to.h) { + hRange.from = hRange.to; + } + + if (hRange.from.h && !hRange.to.h) { + hRange.to = hRange.from; + } + + if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) { + hRange.from.h = 0; + hRange.from.m = 0; + hRange.from.s = 0; + } + + if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) { + hRange.to.h = 23; + hRange.to.m = 59; + hRange.to.s = 59; + } + + if (!hRange.from || !hRange.to) { + continue; + } + + regions = []; + + if ( + hRange.from.h >= tRange.from.hour() && + hRange.from.h <= tRange.from.hour() && + hRange.from.m >= tRange.from.minute() && + hRange.from.m <= tRange.from.minute() && + hRange.to.h >= tRange.to.hour() && + hRange.to.h <= tRange.to.hour() && + hRange.to.m >= tRange.to.minute() && + hRange.to.m <= tRange.to.minute() + ) { + regions.push({ from: tRange.from.valueOf(), to: tRange.to.startOf('hour').valueOf() }); + } else { + fromStart = moment(tRange.from); + fromStart.set('hour', 0); + fromStart.set('minute', 0); + fromStart.set('second', 0); + fromStart.add(hRange.from.h, 'hours'); + fromStart.add(hRange.from.m, 'minutes'); + fromStart.add(hRange.from.s, 'seconds'); + + while (fromStart.unix() <= tRange.to.unix()) { + while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) { + fromStart.add(24, 'hours'); + } + + if (fromStart.unix() > tRange.to.unix()) { + break; + } + + fromEnd = moment(fromStart); + + if (hRange.from.h <= hRange.to.h) { + fromEnd.add(hRange.to.h - hRange.from.h, 'hours'); + } else if (hRange.from.h + hRange.to.h < 23) { + fromEnd.add(hRange.to.h, 'hours'); + } else { + fromEnd.add(24 - hRange.from.h, 'hours'); + } + + fromEnd.set('minute', hRange.to.m); + fromEnd.set('second', hRange.to.s); + + while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) { + fromEnd.add(24, 'hours'); + } + + regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + fromStart.add(24, 'hours'); + } + } + + timeRegionColor = getColor(timeRegion); + + for (let j = 0; j < regions.length; j++) { + const r = regions[j]; + if (timeRegion.fill) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.to }, + color: timeRegionColor.fill, + }); + } + + if (timeRegion.line) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.from }, + color: timeRegionColor.line, + }); + options.grid.markings.push({ + xaxis: { from: r.to, to: r.to }, + color: timeRegionColor.line, + }); + } + } + } + } + + parseTimeRange(str) { + const timeRegex = /^([\d]+):?(\d{2})?/; + const result = { h: null, m: null }; + const match = timeRegex.exec(str); + + if (!match) { + return result; + } + + if (match.length > 1) { + result.h = Number(match[1]); + result.m = 0; + + if (match.length > 2 && match[2] !== undefined) { + result.m = Number(match[2]); + } + + if (result.h > 23) { + result.h = 23; + } + + if (result.m > 59) { + result.m = 59; + } + } + + return result; + } +} diff --git a/public/app/plugins/panel/graph/time_regions_form.html b/public/app/plugins/panel/graph/time_regions_form.html new file mode 100644 index 00000000000..66bf4352aa5 --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.html @@ -0,0 +1,64 @@ +
+
Time regions
+
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ + + + +
+ + + +
+ + + + +
+ +
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/public/app/plugins/panel/graph/time_regions_form.ts b/public/app/plugins/panel/graph/time_regions_form.ts new file mode 100644 index 00000000000..e01ec4acd0e --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.ts @@ -0,0 +1,73 @@ +import coreModule from 'app/core/core_module'; +import { getColorModes } from './time_region_manager'; + +export class TimeRegionFormCtrl { + panelCtrl: any; + panel: any; + disabled: boolean; + colorModes: any; + + /** @ngInject */ + constructor($scope) { + this.panel = this.panelCtrl.panel; + + const unbindDestroy = $scope.$on('$destroy', () => { + this.panelCtrl.editingTimeRegions = false; + this.panelCtrl.render(); + unbindDestroy(); + }); + + this.colorModes = getColorModes(); + this.panelCtrl.editingTimeRegions = true; + } + + render() { + this.panelCtrl.render(); + } + + addTimeRegion() { + this.panel.timeRegions.push({ + op: 'time', + fromDayOfWeek: undefined, + from: undefined, + toDayOfWeek: undefined, + to: undefined, + colorMode: 'critical', + fill: true, + line: false, + }); + this.panelCtrl.render(); + } + + removeTimeRegion(index) { + this.panel.timeRegions.splice(index, 1); + this.panelCtrl.render(); + } + + onFillColorChange(index) { + return newColor => { + this.panel.timeRegions[index].fillColor = newColor; + this.render(); + }; + } + + onLineColorChange(index) { + return newColor => { + this.panel.timeRegions[index].lineColor = newColor; + this.render(); + }; + } +} + +coreModule.directive('graphTimeRegionForm', () => { + return { + restrict: 'E', + templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html', + controller: TimeRegionFormCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + panelCtrl: '=', + }, + }; +}); From e8e189d111bec5b5322f0e1309871333c691b720 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 13 Nov 2018 12:39:10 +0100 Subject: [PATCH 018/125] devenv: graph time regions test dashboard --- .../panel_tests_graph_time_regions.json | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 devenv/dev-dashboards/panel_tests_graph_time_regions.json diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json new file mode 100644 index 00000000000..a72d7d24c2a --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -0,0 +1,417 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "background6", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "timeShift": null, + "title": "Business Hours", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Sunday's 20-23", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Each day of week", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-30d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "utc", + "title": "Panel Tests - Graph (Time Regions)", + "uid": "XMjIZPmik", + "version": 43 +} \ No newline at end of file From 0f57c4b20ef99658d3f54654d45143fd635d9661 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 17:21:20 +0100 Subject: [PATCH 019/125] create time regions solely based on utc time --- .../graph/specs/time_region_manager.test.ts | 157 +++++++++++------- .../panel/graph/time_region_manager.ts | 13 +- 2 files changed, 110 insertions(+), 60 deletions(-) diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts index d1b2290cb61..35e48897282 100644 --- a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -28,7 +28,10 @@ describe('TimeRegionManager', () => { }; ctx.printScenario = () => { - console.log(`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`); + console.log( + `Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`, + ctx.panelCtrl.range.from._isUTC + ); ctx.options.grid.markings.forEach((m, i) => { console.log( `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}` @@ -40,11 +43,11 @@ describe('TimeRegionManager', () => { }); } - describe('When creating plot markings', () => { + describe('When creating plot markings using local time', () => { plotOptionsScenario('for day of week region', ctx => { const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; - const from = moment('2018-01-01 00:00'); - const to = moment('2018-01-01 23:59'); + const from = moment('2018-01-01T00:00:00+01:00'); + const to = moment('2018-01-01T23:59:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -53,30 +56,30 @@ describe('TimeRegionManager', () => { it('should add fill', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(from.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(to.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); }); it('should add line before', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[1].xaxis.from).format()).toBe(from.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(from.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.line); }); it('should add line after', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[2].xaxis.from).format()).toBe(to.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(to.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.line); }); }); plotOptionsScenario('for time from region', ctx => { const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 00:00'); - const to = moment('2018-01-03 23:59'); + const from = moment('2018-01-01T00:00+01:00'); + const to = moment('2018-01-03T23:59+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -86,27 +89,24 @@ describe('TimeRegionManager', () => { it('should add one fill at 05:00 each day', () => { const markings = ctx.options.grid.markings; - const firstFill = moment(from.add(5, 'hours')); - expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - const secondFill = moment(firstFill).add(1, 'days'); - expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - const thirdFill = moment(secondFill).add(1, 'days'); - expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for time to region', ctx => { const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; - const from = moment('2018-02-01 00:00'); - const to = moment('2018-02-03 23:59'); + const from = moment('2018-02-01T00:00+01:00'); + const to = moment('2018-02-03T23:59+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -116,27 +116,24 @@ describe('TimeRegionManager', () => { it('should add one fill at 05:00 each day', () => { const markings = ctx.options.grid.markings; - const firstFill = moment(from.add(5, 'hours')); - expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - const secondFill = moment(firstFill).add(1, 'days'); - expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - const thirdFill = moment(secondFill).add(1, 'days'); - expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week from/to region', ctx => { const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -146,24 +143,24 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week from region', ctx => { const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -173,24 +170,24 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week to region', ctx => { const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -200,18 +197,66 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); + + plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => { + const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-03-17T06:00:00+01:00'); + const to = moment('2018-04-03T06:00:00+02:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday between 20:00 and 23:00', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-03-18T21:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-03-19T00:00:00+01:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-03-25T22:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-03-26T01:00:00+02:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-04-01T22:00:00+02:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-04-02T01:00:00+02:00').format()); + }); + }); + + plotOptionsScenario('for each day of week with winter time', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-10-20T14:50:11+02:00'); + const to = moment('2018-11-07T12:56:23+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-10-21T02:00:00+02:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-10-22T01:59:59+02:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-10-28T02:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-10-29T00:59:59+01:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-11-04T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-11-05T00:59:59+01:00').format()); + }); + }); }); }); diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index c3c6aadaa31..1475ae6040a 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -82,9 +82,7 @@ export class TimeRegionManager { return; } - const tRange = this.panelCtrl.dashboard.isTimezoneUtc() - ? { from: this.panelCtrl.range.from, to: this.panelCtrl.range.to } - : { from: this.panelCtrl.range.from.local(), to: this.panelCtrl.range.to.local() }; + const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() }; let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; @@ -188,7 +186,14 @@ export class TimeRegionManager { fromEnd.add(24, 'hours'); } - regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + const outsideRange = + (fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) || + (fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix()); + + if (!outsideRange) { + regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + } + fromStart.add(24, 'hours'); } } From 2f65b061355fc6871174c43fd9b18a74b2a83dad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 17:22:34 +0100 Subject: [PATCH 020/125] devenv: graph time regions test dashboard --- .../panel_tests_graph_time_regions.json | 100 +++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index a72d7d24c2a..4cace512741 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -167,7 +167,7 @@ "fillColor": "rgba(255, 255, 255, 0.03)", "from": "20:00", "fromDayOfWeek": 7, - "line": false, + "line": true, "lineColor": "rgba(255, 255, 255, 0.2)", "op": "time", "to": "23:00", @@ -369,6 +369,100 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "timeShift": null, + "title": "05:00", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "refresh": false, @@ -410,8 +504,8 @@ "30d" ] }, - "timezone": "utc", + "timezone": "browser", "title": "Panel Tests - Graph (Time Regions)", "uid": "XMjIZPmik", - "version": 43 + "version": 1 } \ No newline at end of file From dea953003ce464e580ee3173ccc6cc71316ccf87 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 18:47:35 +0100 Subject: [PATCH 021/125] docs: description about graph panel time regions feature --- docs/sources/features/panels/graph.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md index 5a010ceca40..44fa0e7c0db 100644 --- a/docs/sources/features/panels/graph.md +++ b/docs/sources/features/panels/graph.md @@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when the graph crosses a particular threshold. +### Time Regions + +> Only available in Grafana v5.4 and above. + +{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}} + +Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours. + ## Time Range {{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}} From bae4c8d2e66a47ec63d8494ecf77c7f6e8576f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Nov 2018 13:29:09 -0800 Subject: [PATCH 022/125] wip: switch slider test --- public/app/core/components/Switch/Switch.tsx | 2 +- public/sass/components/_switch.scss | 138 +++++++------------ 2 files changed, 52 insertions(+), 88 deletions(-) diff --git a/public/app/core/components/Switch/Switch.tsx b/public/app/core/components/Switch/Switch.tsx index 09c5b894d3d..3d7bf3c5a76 100644 --- a/public/app/core/components/Switch/Switch.tsx +++ b/public/app/core/components/Switch/Switch.tsx @@ -44,7 +44,7 @@ export class Switch extends PureComponent { )}
-
); diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index c368d8ead67..b875b2f2d45 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -3,93 +3,6 @@ ============================================================ */ .gf-form-switch { - position: relative; - max-width: 4.5rem; - flex-grow: 1; - min-width: 4rem; - margin-right: $gf-form-margin; - - input { - position: absolute; - margin-left: -9999px; - visibility: hidden; - display: none; - } - - input + label { - display: block; - position: relative; - cursor: pointer; - outline: none; - user-select: none; - width: 100%; - height: 37px; - background: $input-bg; - border: 1px solid $input-border-color; - border-left: none; - border-radius: $input-border-radius; - } - - input + label::before, - input + label::after { - @include buttonBackground($input-bg, $input-bg); - - display: block; - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - color: #fff; - text-align: center; - font-size: 150%; - display: flex; - flex-direction: column; - justify-content: center; - } - - &:hover { - input + label::before { - @include buttonBackground($input-bg, lighten($input-bg, 5%)); - color: $text-color; - text-shadow: $text-shadow-faint; - } - - input + label::after { - @include buttonBackground($input-bg, lighten($input-bg, 5%)); - color: lighten($orange, 10%); - text-shadow: $text-shadow-strong; - } - } - - input + label::before { - font-family: 'FontAwesome'; - content: '\f096'; // square-o - color: $text-color-weak; - transition: transform 0.4s; - backface-visibility: hidden; - text-shadow: $text-shadow-faint; - } - - input + label::after { - content: '\f046'; // check-square-o - color: $orange; - text-shadow: $text-shadow-strong; - - font-family: 'FontAwesome'; - transition: transform 0.4s; - transform: rotateY(180deg); - backface-visibility: hidden; - } - - input:checked + label::before { - transform: rotateY(180deg); - } - - input:checked + label::after { - transform: rotateY(0); - } - &--small { max-width: 2rem; min-width: 1.5rem; @@ -174,3 +87,54 @@ gf-form-switch[disabled] { } } } + +.gf-form-switch { + position: relative; + display: inline-block; + width: 60px; + height: 34px; + border-radius: 34px; + + input { + opacity: 0; + width: 0; + height: 0; + } +} + +/* The slider */ +.gf-form-switch__slider { + position: absolute; + cursor: pointer; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: $dark-3; + transition: 0.4s; + + &::before { + position: absolute; + content: ''; + height: 26px; + width: 26px; + left: 4px; + bottom: 4px; + background: $dark-4; + transition: 0.4s; + border-radius: 50%; + } +} + +input:checked + .gf-form-switch__slider { + background-color: $dark-5; +} + +/* input:focus + .gf-form-switch__slider { */ +/* box-shadow: 0 0 1px #2196f3; */ +/* } */ + +input:checked + .gf-form-switch__slider:before { + transform: translateX(26px); + background: $blue; +} From 8fb997d935e47798879d1e6b03daefe51b2370d8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Nov 2018 23:19:35 +0100 Subject: [PATCH 023/125] should not notify when going from unknown to pending --- pkg/services/alerting/notifiers/base.go | 5 +++++ pkg/services/alerting/notifiers/base_test.go | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 35d3ff518a0..d4a9975bcba 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -71,6 +71,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC return false } + // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending { + return false + } + // Do not notify when we become OK from pending if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { return false diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 388c2db17ee..3fd4447eefe 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStatePending, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateAlerting, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStatePending, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: true, - state: &m.AlertNotificationState{}, expect: false, }, @@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateAlerting, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - state: &m.AlertNotificationState{}, expect: true, }, @@ -138,7 +131,13 @@ func TestShouldSendAlertNotification(t *testing.T) { name: "unknown -> ok", prevState: m.AlertStateUnknown, newState: m.AlertStateOK, - state: &m.AlertNotificationState{}, + + expect: false, + }, + { + name: "unknown -> pending", + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, expect: false, }, @@ -146,7 +145,6 @@ func TestShouldSendAlertNotification(t *testing.T) { name: "unknown -> alerting", prevState: m.AlertStateUnknown, newState: m.AlertStateAlerting, - state: &m.AlertNotificationState{}, expect: true, }, @@ -157,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) { State: tc.prevState, }) + if tc.state == nil { + tc.state = &m.AlertNotificationState{} + } + evalContext.Rule.State = tc.newState nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency} From 84eb3bd0958ca4606876da45c39fe1da70385ae4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Nov 2018 23:39:44 +0100 Subject: [PATCH 024/125] tests for supporting for with all alerting scenarios --- pkg/services/alerting/eval_context_test.go | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index cc0bed79d10..d9615ee4801 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -139,6 +139,50 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.NoDataFound = true }, }, + { + name: "pending -> no_data(alerting) with for duration have not passed", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> no_data(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, + { + name: "pending -> error(alerting) with for duration have not passed ", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> error(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, } for _, tc := range tcs { From 810e256787abdf395520f3b77614974da2b3bab9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Nov 2018 21:35:02 -0800 Subject: [PATCH 025/125] css update to switch slider --- public/app/core/components/switch.ts | 2 +- public/sass/components/_switch.scss | 29 ++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index d8c1da4e345..6644810afbe 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -9,7 +9,7 @@ const template = `
- +
`; diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index b875b2f2d45..f97d655f4a8 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -93,7 +93,7 @@ gf-form-switch[disabled] { display: inline-block; width: 60px; height: 34px; - border-radius: 34px; + background: black; input { opacity: 0; @@ -106,28 +106,29 @@ gf-form-switch[disabled] { .gf-form-switch__slider { position: absolute; cursor: pointer; - top: 0; - left: 0; - right: 0; - bottom: 0; - background: $dark-3; + top: 8px; + left: 14px; + right: 10px; + bottom: 10px; + background: #1d1d1d; transition: 0.4s; + border-radius: 34px; &::before { position: absolute; content: ''; - height: 26px; - width: 26px; - left: 4px; - bottom: 4px; - background: $dark-4; + height: 16px; + width: 16px; + left: 2px; + bottom: 0px; + background: #333333; transition: 0.4s; border-radius: 50%; } } input:checked + .gf-form-switch__slider { - background-color: $dark-5; + background: linear-gradient(90deg, $orange, $red); } /* input:focus + .gf-form-switch__slider { */ @@ -135,6 +136,6 @@ input:checked + .gf-form-switch__slider { /* } */ input:checked + .gf-form-switch__slider:before { - transform: translateX(26px); - background: $blue; + transform: translateX(20px); + box-shadow: 0 0 5px black; } From bdf598ccb283e569fdf62d905d1bbc84d5cdcf44 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Wed, 14 Nov 2018 21:38:53 -0800 Subject: [PATCH 026/125] minor fix --- public/app/core/components/switch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index 6644810afbe..edfed5593e0 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -9,7 +9,7 @@ const template = `
- +
`; From 28029ce4a7e6850e8a5665343507d200e9102831 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 11:04:16 +0100 Subject: [PATCH 027/125] alerting: support `for` on execution errors and notdata --- pkg/services/alerting/eval_context.go | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 23d3efa8bea..5a4b378ac28 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -118,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } +// GetNewState returns the new state from the alert rule evaluation func (c *EvalContext) GetNewState() m.AlertStateType { + ns := getNewStateInternal(c) + if ns != m.AlertStateAlerting || c.Rule.For == 0 { + return ns + } + + since := time.Now().Sub(c.Rule.LastStateChange) + if since > c.Rule.For { + return m.AlertStateAlerting + } + + if c.PrevAlertState == m.AlertStateAlerting { + return m.AlertStateAlerting + } + + return m.AlertStatePending +} + +func getNewStateInternal(c *EvalContext) m.AlertStateType { if c.Error != nil { c.log.Error("Alert Rule Result Error", "ruleId", c.Rule.Id, @@ -132,19 +151,6 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.Rule.ExecutionErrorState.ToAlertState() } - if c.Firing && c.Rule.For != 0 { - since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.For { - return m.AlertStateAlerting - } - - if c.PrevAlertState == m.AlertStateAlerting { - return m.AlertStateAlerting - } - - return m.AlertStatePending - } - if c.Firing { return m.AlertStateAlerting } From 81efc00adf7a7b0a979799e9dff40ce8037900af Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:21:30 +0100 Subject: [PATCH 028/125] set default color mode --- public/app/plugins/panel/graph/time_regions_form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/time_regions_form.ts b/public/app/plugins/panel/graph/time_regions_form.ts index e01ec4acd0e..5dc9c4016eb 100644 --- a/public/app/plugins/panel/graph/time_regions_form.ts +++ b/public/app/plugins/panel/graph/time_regions_form.ts @@ -32,7 +32,7 @@ export class TimeRegionFormCtrl { from: undefined, toDayOfWeek: undefined, to: undefined, - colorMode: 'critical', + colorMode: 'background6', fill: true, line: false, }); From 116e367e7153f34c14e27b9d24842e4707a9a5b3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:30:49 +0100 Subject: [PATCH 029/125] fix time regions mutable bug --- public/app/plugins/panel/graph/time_region_manager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index 1475ae6040a..b8ab9a856be 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -86,8 +86,10 @@ export class TimeRegionManager { let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; - for (i = 0; i < panel.timeRegions.length; i++) { - timeRegion = panel.timeRegions[i]; + const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a })); + + for (i = 0; i < timeRegionsCopy.length; i++) { + timeRegion = timeRegionsCopy[i]; if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) { continue; From bd6dc01e6b86aa6aade055851ad7ce5cd8aca7c8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:32:27 +0100 Subject: [PATCH 030/125] devenv: graph time regions test dashboard --- devenv/dev-dashboards/panel_tests_graph_time_regions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index 4cace512741..52818ca7aa1 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -167,7 +167,7 @@ "fillColor": "rgba(255, 255, 255, 0.03)", "from": "20:00", "fromDayOfWeek": 7, - "line": true, + "line": false, "lineColor": "rgba(255, 255, 255, 0.2)", "op": "time", "to": "23:00", @@ -420,7 +420,7 @@ "timeRegions": [ { "colorMode": "red", - "fill": true, + "fill": false, "from": "05:00", "line": true, "op": "time" From ce59acd141dc744dcaa3a23ec88187c8a252753f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 15 Nov 2018 11:20:27 +0000 Subject: [PATCH 031/125] Extracted language provider variables for readibility --- .../prometheus/language_provider.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 326ab93f2ef..6e6f461d341 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -79,15 +79,23 @@ export default class PromQlLanguageProvider extends LanguageProvider { // Keep this DOM-free for testing provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { - // Syntax spans have 3 classes by default. More indicate a recognized token - const tokenRecognized = wrapperClasses.length > 3; - // Local text properties const empty = value.document.text.length === 0; const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; + // Syntax spans have 3 classes by default. More indicate a recognized token + const tokenRecognized = wrapperClasses.length > 3; + // Non-empty prefix, but not inside known token + const prefixUnrecognized = prefix && !tokenRecognized; + // Prevent suggestions in `function(|suffix)` + const noSuffix = !nextCharacter || nextCharacter === ')'; + // Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it + const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; + // About to type next operand if preceded by binary operator + const isNextOperand = text.match(/[+\-*/^%]/); + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -96,16 +104,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { + // Suggestions for sum(metric) by (|) return this.getAggregationCompletionItems.apply(this, arguments); } else if (empty) { + // Suggestions for empty query field return this.getEmptyCompletionItems(context || {}); - } else if ( - // Show default suggestions in a couple of scenarios - (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - // Empty prefix, but not directly following a closing brace (e.g., `]|`), or not succeeded by anything except a closing parens, e.g., `sum(|)` - (prefix === '' && !text.match(/^[\]})\s]+$/) && (!nextCharacter || nextCharacter === ')')) || - text.match(/[+\-*/^%]/) // Anything after binary operator - ) { + } else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) { + // Show term suggestions in a couple of scenarios return this.getTermCompletionItems(); } From a70ea2101c3ddf161786dd0c9a232d73755c1daa Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:36:11 +0100 Subject: [PATCH 032/125] alertmanager: adds tests for should notify --- .../alerting/notifiers/alertmanager_test.go | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index 3549b536e48..7510742ed17 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -1,13 +1,60 @@ package notifiers import ( + "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) +func TestWhenAlertManagerShouldNotify(t *testing.T) { + tcs := []struct { + prevState m.AlertStateType + newState m.AlertStateType + + expect bool + }{ + { + prevState: m.AlertStatePending, + newState: m.AlertStateOK, + expect: false, + }, + { + prevState: m.AlertStateAlerting, + newState: m.AlertStateOK, + expect: true, + }, + { + prevState: m.AlertStateOK, + newState: m.AlertStatePending, + expect: false, + }, + { + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, + expect: false, + }, + } + + for _, tc := range tcs { + am := &AlertmanagerNotifier{log: log.New("test.logger")} + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.prevState, + }) + + evalContext.Rule.State = tc.newState + + res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{}) + if res != tc.expect { + t.Errorf("got %v expected %v", res, tc.expect) + } + } +} + func TestAlertmanagerNotifier(t *testing.T) { Convey("Alertmanager notifier tests", t, func() { From 968bfd01391b760b3a5437a7a507447f22bbc4c7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:42:47 +0100 Subject: [PATCH 033/125] adds pending state to alert list panel --- public/app/plugins/panel/alertlist/editor.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index c48b70e02c0..a05234cea3c 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -50,5 +50,6 @@ +
From e7260d77b3938188521cd2564f5f4a6a5e512371 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:46:28 +0100 Subject: [PATCH 034/125] adds pending filter for alert list page --- public/app/features/alerting/AlertRuleList.tsx | 1 + .../__snapshots__/AlertRuleList.test.tsx.snap | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index d25fc659af5..f94134f3ee1 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent { { text: 'Alerting', value: 'alerting' }, { text: 'No Data', value: 'no_data' }, { text: 'Paused', value: 'paused' }, + { text: 'Pending', value: 'pending' }, ]; componentDidMount() { diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index 4ae27213e1e..b753a852e92 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = ` > Paused + @@ -230,6 +236,12 @@ exports[`Render should render component 1`] = ` > Paused + From 7ba04466a2b305b01d1a7832bed24881b4ce6e59 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 14:30:36 +0100 Subject: [PATCH 035/125] alerting: improve annotations for pending state --- public/app/core/utils/colors.ts | 1 + public/app/features/annotations/event_manager.ts | 6 ++++++ public/app/features/panel/panel_directive.ts | 6 +++++- public/sass/pages/_alerting.scss | 7 +++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index e8a7366beb5..2ab79ba27fb 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -7,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; export const OK_COLOR = 'rgba(11, 237, 50, 1)'; export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; export const REGION_FILL_ALPHA = 0.09; const colors = [ diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index ef74ca193d4..db748e639a1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -7,6 +7,7 @@ import { OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, + PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, } from 'app/core/utils/colors'; @@ -71,6 +72,11 @@ export class EventManager { position: 'BOTTOM', markerSize: 5, }, + $__pending: { + color: PENDING_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, $__editing: { color: DEFAULT_ANNOTATION_COLOR, position: 'BOTTOM', diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 77ebf754b3a..aef7ca5e256 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -161,7 +161,11 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { panelContainer.removeClass('panel-alert-state--' + lastAlertState); } - if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') { + if ( + ctrl.alertState.state === 'ok' || + ctrl.alertState.state === 'alerting' || + ctrl.alertState.state === 'pending' + ) { panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state); } diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 90f2eb526f1..77752be11bc 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -66,6 +66,13 @@ content: '\e611'; } } + + &--pending { + .panel-alert-icon:before { + color: $warn; + content: '\e611'; + } + } } @keyframes alerting-panel { From 2e8c4699b03dc601f24804c075e443b01413d31f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 15 Nov 2018 14:42:09 +0100 Subject: [PATCH 036/125] build: internal metrics for packaging. --- .bra.toml | 4 ++-- Makefile | 2 +- build.go | 2 ++ packaging/deb/init.d/grafana-server | 2 +- packaging/deb/systemd/grafana-server.service | 1 + packaging/docker/run.sh | 1 + packaging/rpm/init.d/grafana-server | 2 +- packaging/rpm/systemd/grafana-server.service | 1 + pkg/cmd/grafana-server/main.go | 14 +++++++++++++- pkg/metrics/metrics.go | 15 +++++++++------ pkg/metrics/metrics_test.go | 3 +++ pkg/setting/setting.go | 3 +++ 12 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.bra.toml b/.bra.toml index aa7a1680adc..5be42ceebbf 100644 --- a/.bra.toml +++ b/.bra.toml @@ -1,7 +1,7 @@ [run] init_cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] watch_all = true follow_symlinks = true @@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"] build_delay = 1500 cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] diff --git a/Makefile b/Makefile index fcb740d2fac..6410714d4fc 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ build: build-go build-js build-docker-dev: @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already." - go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest + go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest cp dist/grafana-latest.linux-x64.tar.gz packaging/docker cd packaging/docker && docker build --tag grafana/grafana:dev . diff --git a/build.go b/build.go index dc789670f62..9d5216de1d0 100644 --- a/build.go +++ b/build.go @@ -128,6 +128,8 @@ func main() { if goos == linux { createLinuxPackages() } + case "pkg-archive": + grunt(gruntBuildArg("package")...) case "pkg-rpm": grunt(gruntBuildArg("release")...) diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 567da94f881..5c1d9c8271a 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function checkUser() { if [ `id -u` -ne 0 ]; then diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index acd2a360a93..b1e2e387e4d 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=deb \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index bc001bdf90a..6b368f6cc1c 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -80,6 +80,7 @@ fi exec grafana-server \ --homepath="$GF_PATHS_HOME" \ --config="$GF_PATHS_CONFIG" \ + --packaging docker \ "$@" \ cfg:default.log.mode="console" \ cfg:default.paths.data="$GF_PATHS_DATA" \ diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index cefe212116c..b7b41e58e8d 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -60,7 +60,7 @@ fi # overwrite settings from default file [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function isRunning() { status -p $PID_FILE $NAME > /dev/null 2>&1 diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index f228c8d8b14..ad5006d1d4c 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=rpm \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index c7c1ff3aff7..285bd7ff1c3 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -13,7 +13,7 @@ import ( "syscall" "time" - extensions "github.com/grafana/grafana/pkg/extensions" + "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" @@ -39,6 +39,7 @@ var buildstamp string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var pidFile = flag.String("pidfile", "", "path to pid file") +var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed") func main() { v := flag.Bool("v", false, "prints current version and exits") @@ -79,6 +80,7 @@ func main() { setting.BuildStamp = buildstampInt64 setting.BuildBranch = buildBranch setting.IsEnterprise = extensions.IsEnterprise + setting.Packaging = validPackaging(*packaging) metrics.SetBuildInformation(version, commit, buildBranch) @@ -95,6 +97,16 @@ func main() { os.Exit(code) } +func validPackaging(packaging string) string { + validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"} + for _, vt := range validTypes { + if packaging == vt { + return packaging + } + } + return "unknown" +} + func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) sighupChan := make(chan os.Signal, 1) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 5709e3e3213..326514a9687 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -313,7 +313,7 @@ func init() { // SetBuildInformation sets the build information for this binary func SetBuildInformation(version, revision, branch string) { - // We export this info twice for backwards compability. + // We export this info twice for backwards compatibility. // Once this have been released for some time we should be able to remote `M_Grafana_Version` // The reason we added a new one is that its common practice in the prometheus community // to name this metric `*_build_info` so its easy to do aggregation on all programs. @@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) { metrics := map[string]interface{}{} report := map[string]interface{}{ - "version": version, - "metrics": metrics, - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "edition": getEdition(), + "version": version, + "metrics": metrics, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "edition": getEdition(), + "packaging": setting.Packaging, } statsQuery := models.GetSystemStatsQuery{} @@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) { } metrics["stats.ds.other.count"] = dsOtherCount + metrics["stats.packaging."+setting.Packaging+".count"] = 1 + dsAccessStats := models.GetDataSourceAccessStatsQuery{} if err := bus.Dispatch(&dsAccessStats); err != nil { metricsLogger.Error("Failed to get datasource access stats", "error", err) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 43739221f1e..c27d6f64b8c 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) { setting.BasicAuthEnabled = true setting.LdapEnabled = true setting.AuthProxyEnabled = true + setting.Packaging = "deb" wg.Add(1) sendUsageStats(oauthProviders) @@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) + + So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1) }) }) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index afae642f5b3..0e0d3c3a36a 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -57,6 +57,9 @@ var ( IsEnterprise bool ApplicationName string + // packaging + Packaging = "unknown" + // Paths HomePath string PluginsPath string From caec36e7ece559cf213eb9ed240389ca56ed4c53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 15:37:46 +0100 Subject: [PATCH 037/125] alert rule have to be pending before alerting is for is specified --- pkg/services/alerting/eval_context.go | 2 +- pkg/services/alerting/eval_context_test.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 5a4b378ac28..17ed448bd2a 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -126,7 +126,7 @@ func (c *EvalContext) GetNewState() m.AlertStateType { } since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.For { + if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For { return m.AlertStateAlerting } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index d9615ee4801..4c9b88f1881 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -66,8 +66,8 @@ func TestGetStateFromEvalContext(t *testing.T) { }, }, { - name: "ok -> alerting. since its been firing for more than FOR", - expected: models.AlertStateAlerting, + name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok", + expected: models.AlertStatePending, applyFn: func(ec *EvalContext) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true @@ -75,6 +75,16 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.Rule.For = time.Minute * 2 }, }, + { + name: "pending -> alerting. since its been firing for more than FOR and prev state is pending", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.For = time.Minute * 2 + }, + }, { name: "alerting -> alerting. should not update regardless of FOR", expected: models.AlertStateAlerting, From 48905a613dc37cc3ea8e15cad51319e166d203b4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 16:00:32 +0100 Subject: [PATCH 038/125] fix pending alert annotation tooltip icon --- public/app/features/annotations/annotation_tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 16c18005204..fbe85856f31 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, if (event.alertId) { const stateModel = alertDef.getStateDisplayModel(event.newState); titleStateClass = stateModel.stateClass; - title = ` ${stateModel.text}`; + title = ` ${stateModel.text}`; text = alertDef.getAlertAnnotationInfo(event); if (event.text) { text = text + '
' + event.text; From a8e6b241d67ce98f6505f00a13864580358f0ce1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:07:42 +0100 Subject: [PATCH 039/125] changed time region color modes --- .../panel_tests_graph_time_regions.json | 2 +- .../panel/graph/time_region_manager.ts | 34 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index 52818ca7aa1..8d0bae1221c 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -63,7 +63,7 @@ "timeFrom": null, "timeRegions": [ { - "colorMode": "background6", + "colorMode": "gray", "fill": true, "fillColor": "rgba(255, 255, 255, 0.03)", "from": "08:30", diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index b8ab9a856be..95987e40dbe 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -4,37 +4,29 @@ import moment from 'moment'; import config from 'app/core/config'; export const colorModes = { - custom: { title: 'Custom' }, + gray: { + themeDependent: true, + title: 'Gray', + darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + }, red: { title: 'Red', color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' }, }, - yellow: { - title: 'Yellow', - color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, - }, green: { title: 'Green', color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' }, }, - background3: { - themeDependent: true, - title: 'Background (3%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.03)', line: 'rgba(255, 255, 255, 0.1)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.03)', line: 'rgba(0, 0, 0, 0.1)' }, + blue: { + title: 'Blue', + color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' }, }, - background6: { - themeDependent: true, - title: 'Background (6%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.06)', line: 'rgba(255, 255, 255, 0.15)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.06)', line: 'rgba(0, 0, 0, 0.15)' }, - }, - background9: { - themeDependent: true, - title: 'Background (9%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + yellow: { + title: 'Yellow', + color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, }, + custom: { title: 'Custom' }, }; export function getColorModes() { From 3b4a224a57aceab5419ae596c1b114302303d15a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:25:58 +0100 Subject: [PATCH 040/125] Add tooltip --- public/app/plugins/panel/graph/time_regions_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/time_regions_form.html b/public/app/plugins/panel/graph/time_regions_form.html index 66bf4352aa5..7292c53ec80 100644 --- a/public/app/plugins/panel/graph/time_regions_form.html +++ b/public/app/plugins/panel/graph/time_regions_form.html @@ -1,5 +1,5 @@
-
Time regions
+
Time regions All configured time regions refers to UTC time
From 8ce1cc2d52d7d6a8aa1cf6807e2df981f264d7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Nov 2018 17:33:55 +0100 Subject: [PATCH 041/125] fixed alert tab order and fixed some console logging issues --- public/app/core/services/dynamic_directive_srv.ts | 1 - public/app/core/services/keybindingSrv.ts | 4 ++-- public/app/plugins/panel/graph/module.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 9b7ede59853..c27842ab54f 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -21,7 +21,6 @@ class DynamicDirectiveSrv { } if (!directiveInfo.fn.registered) { - console.log('register panel tab'); coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); directiveInfo.fn.registered = true; } diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 6fe57dfa77a..c02f6850e8b 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -32,8 +32,8 @@ export class KeybindingSrv { this.setupGlobal(); appEvents.on('show-modal', () => (this.modalOpen = true)); - $rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true)); - $rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false)); + appEvents.on('timepickerOpen', () => (this.timepickerOpen = true)); + appEvents.on('timepickerClosed', () => (this.timepickerOpen = false)); } setupGlobal() { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a6c5190d937..68bd242a39f 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -138,7 +138,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 5); + this.addEditorTab('Alert', alertTab, 6); } this.subTabIndex = 0; From cbd4125e697a065934b956178534b1be3d6a572d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:50:18 +0100 Subject: [PATCH 042/125] changelog: add notes about closing #5930 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fceb28265..ea6b5b9732f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro) * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550) +* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930) ### Minor From 242ceb6d957449bdd49824beb038ec276daf9c6c Mon Sep 17 00:00:00 2001 From: Roland Dunn Date: Thu, 15 Nov 2018 20:12:48 +0000 Subject: [PATCH 043/125] Update google analytics code to submit full URL not just path --- public/app/core/services/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index be4371adb26..40e20b16a29 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -26,7 +26,7 @@ export class Analytics { init() { this.$rootScope.$on('$viewContentLoaded', () => { - const track = { page: this.$location.url() }; + const track = { location: this.$location.url() }; const ga = (window as any).ga || this.gaInit(); ga('set', track); ga('send', 'pageview'); From 905ef220756a17f89ef75a2dd72c53dfb1ecff5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 06:53:54 +0100 Subject: [PATCH 044/125] fixed order of time range tab --- public/app/plugins/panel/graph/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 68bd242a39f..fc335e07545 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -133,12 +133,12 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 6); + this.addEditorTab('Alert', alertTab, 5); } this.subTabIndex = 0; From 438f7d0332f1c2b61c06b5eb1d366879ec6002b3 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 16 Nov 2018 09:03:46 +0100 Subject: [PATCH 045/125] build: refactoring. --- packaging/docker/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index 6b368f6cc1c..63f20742c96 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -80,7 +80,7 @@ fi exec grafana-server \ --homepath="$GF_PATHS_HOME" \ --config="$GF_PATHS_CONFIG" \ - --packaging docker \ + --packaging=docker \ "$@" \ cfg:default.log.mode="console" \ cfg:default.paths.data="$GF_PATHS_DATA" \ From 8c7f4ac188ab33defd0dedc7732fada291dbafc2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 16 Nov 2018 13:17:41 +0300 Subject: [PATCH 046/125] fix datasource testing --- public/app/features/plugins/ds_edit_ctrl.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index c223f444ef3..1dec41f05e8 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -118,7 +118,7 @@ export class DataSourceEditCtrl { } testDatasource() { - this.datasourceSrv.get(this.current.name).then(datasource => { + return this.datasourceSrv.get(this.current.name).then(datasource => { if (!datasource.testDatasource) { return; } @@ -126,7 +126,7 @@ export class DataSourceEditCtrl { this.testing = { done: false, status: 'error' }; // make test call in no backend cache context - this.backendSrv + return this.backendSrv .withNoBackendCache(() => { return datasource .testDatasource() @@ -161,8 +161,8 @@ export class DataSourceEditCtrl { return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => { this.current = result.datasource; this.updateNav(); - this.updateFrontendSettings().then(() => { - this.testDatasource(); + return this.updateFrontendSettings().then(() => { + return this.testDatasource(); }); }); } else { From e85a3f1d04f2ba8c3ff2344633dae67aaacad1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 11:29:32 +0100 Subject: [PATCH 047/125] fix redirect issue, caused by timing of events between angular location change and redux state changes --- public/app/features/dashboard/dashboard_srv.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b1419df7376..d5695a577c5 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -77,6 +77,10 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; + // important that these happens before location redirect below + this.$rootScope.appEvent('dashboard-saved', this.dash); + this.$rootScope.appEvent('alert-success', ['Dashboard saved']); + const newUrl = locationUtil.stripBaseFromUrl(data.url); const currentPath = this.$location.path(); @@ -84,9 +88,6 @@ export class DashboardSrv { this.$location.url(newUrl).replace(); } - this.$rootScope.appEvent('dashboard-saved', this.dash); - this.$rootScope.appEvent('alert-success', ['Dashboard saved']); - return this.dash; } From 96104e437252622d74fa60311cbd5e6a634c12ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 12:39:26 +0100 Subject: [PATCH 048/125] fix: dont setViewMode when nothing has changed --- public/app/features/dashboard/view_state_srv.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 8805050831e..ff12d26233d 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -126,8 +126,7 @@ export class DashboardViewState { if (!panel.fullscreen) { this.enterFullscreen(panel); - } else { - // already in fullscreen view just update the view mode + } else if (this.dashboard.meta.isEditing !== this.state.edit) { this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { From c505fc37839ed84c597f2d8a03c54c13204c4b32 Mon Sep 17 00:00:00 2001 From: ijin08 Date: Fri, 16 Nov 2018 13:02:29 +0100 Subject: [PATCH 049/125] moved slider into label to make it clickable, styled slider in dark and light theme, created variables for slider --- public/app/core/components/switch.ts | 20 +++++++++-------- public/sass/_variables.dark.scss | 8 +++++++ public/sass/_variables.light.scss | 8 +++++++ public/sass/components/_switch.scss | 33 +++++++++++++++------------- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/public/app/core/components/switch.ts b/public/app/core/components/switch.ts index edfed5593e0..1fce79105dc 100644 --- a/public/app/core/components/switch.ts +++ b/public/app/core/components/switch.ts @@ -1,16 +1,18 @@ import coreModule from 'app/core/core_module'; const template = ` -