From 43ac79685ad9fa16593c9b0ce103058292660a17 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 15:45:11 +0100 Subject: [PATCH 01/76] delete auth token on signout --- pkg/api/common_test.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/services/auth/auth_token.go | 26 +++++++++++++++++++++++--- pkg/services/auth/auth_token_test.go | 25 +++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 5 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index eb1f89e3f22..f6c6e53e91d 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {} +func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 11740574d0b..9bb45062e00 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) {} +func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 7e9433c2d70..d9c5e897f70 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -3,6 +3,7 @@ package auth import ( "crypto/sha256" "encoding/hex" + "errors" "net/http" "net/url" "time" @@ -31,7 +32,7 @@ var ( type UserAuthTokenService interface { InitContextWithToken(ctx *models.ReqContext, orgID int64) bool UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - UserSignedOutHook(c *models.ReqContext) + UserSignedOutHook(c *models.ReqContext) error } type UserAuthTokenServiceImpl struct { @@ -111,8 +112,27 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m return nil } -func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) { - s.writeSessionCookie(c, "", -1) +func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) error { + unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) + if unhashedToken == "" { + return errors.New("cannot logout without session token") + } + + hashedToken := hashToken(unhashedToken) + + sql := `DELETE FROM user_auth_token WHERE auth_token = ?` + res, err := s.SQLStore.NewSession().Exec(sql, hashedToken) + if err != nil { + return err + } + + affected, _ := res.RowsAffected() + if affected > 0 { + s.writeSessionCookie(c, "", -1) + return nil + } + + return errors.New("failed to delete session") } func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 2f75c660d9d..0114939ea48 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -1,10 +1,13 @@ package auth import ( + "net/http" "testing" "time" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" + "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -46,6 +49,28 @@ func TestUserAuthToken(t *testing.T) { So(err, ShouldEqual, ErrAuthTokenNotFound) So(LookupToken, ShouldBeNil) }) + + Convey("signing out should delete token and cookie if present", func() { + token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "some user agent2") + So(err, ShouldBeNil) + So(token, ShouldNotBeNil) + + httpreq := &http.Request{Header: make(http.Header)} + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.AuthToken}) + + ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} + + err = userAuthTokenService.UserSignedOutHook(ctx) + So(err, ShouldBeNil) + + // makes sure we tell the browser to overwrite the cookie + So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") + + // lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) + // So(err, ShouldBeNil) + // So(lookedUp, ShouldNotBeNil) + + }) }) Convey("expires correctly", func() { From 88ca54eba96195d1fc0e0138c17d8c6991deb938 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 16:22:40 +0100 Subject: [PATCH 02/76] renames signout function --- pkg/api/common_test.go | 2 +- pkg/api/login.go | 2 +- pkg/middleware/middleware_test.go | 2 +- pkg/services/auth/auth_token.go | 4 ++-- pkg/services/auth/auth_token_test.go | 11 +++-------- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index f6c6e53e91d..fe02c94e277 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -149,4 +149,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } diff --git a/pkg/api/login.go b/pkg/api/login.go index 50c62e0835a..49da147724e 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -136,7 +136,7 @@ func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { } func (hs *HTTPServer) Logout(c *m.ReqContext) { - hs.AuthTokenService.UserSignedOutHook(c) + hs.AuthTokenService.SignOutUser(c) if setting.SignoutRedirectUrl != "" { c.Redirect(setting.SignoutRedirectUrl) diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 9bb45062e00..4679c449853 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -602,4 +602,4 @@ func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqC return nil } -func (s *fakeUserAuthTokenService) UserSignedOutHook(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index d9c5e897f70..5f8f36fc373 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -32,7 +32,7 @@ var ( type UserAuthTokenService interface { InitContextWithToken(ctx *models.ReqContext, orgID int64) bool UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - UserSignedOutHook(c *models.ReqContext) error + SignOutUser(c *models.ReqContext) error } type UserAuthTokenServiceImpl struct { @@ -112,7 +112,7 @@ func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *m return nil } -func (s *UserAuthTokenServiceImpl) UserSignedOutHook(c *models.ReqContext) error { +func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) if unhashedToken == "" { return errors.New("cannot logout without session token") diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 0114939ea48..47afe627479 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -51,7 +51,7 @@ func TestUserAuthToken(t *testing.T) { }) Convey("signing out should delete token and cookie if present", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "some user agent2") + token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "user agent") So(err, ShouldBeNil) So(token, ShouldNotBeNil) @@ -60,16 +60,11 @@ func TestUserAuthToken(t *testing.T) { ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} - err = userAuthTokenService.UserSignedOutHook(ctx) + err = userAuthTokenService.SignOutUser(ctx) So(err, ShouldBeNil) // makes sure we tell the browser to overwrite the cookie - So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") - - // lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) - // So(err, ShouldBeNil) - // So(lookedUp, ShouldNotBeNil) - + //So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") }) }) From 0442a86400552f461f7c8a807a1f2fe609fd5e98 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 31 Jan 2019 19:22:25 +0100 Subject: [PATCH 03/76] tailing grafana logs and temporaily using an older build --- devenv/docker/blocks/loki/config.yaml | 27 +++++++++++++++++++ devenv/docker/blocks/loki/docker-compose.yaml | 6 +++-- 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 devenv/docker/blocks/loki/config.yaml diff --git a/devenv/docker/blocks/loki/config.yaml b/devenv/docker/blocks/loki/config.yaml new file mode 100644 index 00000000000..9451b6ba79b --- /dev/null +++ b/devenv/docker/blocks/loki/config.yaml @@ -0,0 +1,27 @@ +server: + http_listen_port: 9080 + grpc_listen_port: 0 + +positions: + filename: /tmp/positions.yaml + +client: + url: http://loki:3100/api/prom/push + +scrape_configs: +- job_name: system + entry_parser: raw + static_configs: + - targets: + - localhost + labels: + job: varlogs + __path__: /var/log/*log +- job_name: grafana + entry_parser: raw + static_configs: + - targets: + - localhost + labels: + job: grafana + __path__: /var/log/grafana/*log diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index d6cf21f7856..c2fee15b0bb 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -5,7 +5,7 @@ networks: services: loki: - image: grafana/loki:master + image: grafana/loki:master-3e6a75e ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml @@ -13,9 +13,11 @@ services: - loki promtail: - image: grafana/promtail:master + image: grafana/promtail:master-3e6a75e volumes: + - ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml - /var/log:/var/log + - ../data/log:/var/log/grafana command: -config.file=/etc/promtail/docker-config.yaml networks: From f9bab9585a68bb65201a9f7e5c15052601546e22 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Thu, 31 Jan 2019 19:38:49 +0100 Subject: [PATCH 04/76] wip --- public/app/core/utils/explore.test.ts | 9 ++++++++- public/app/core/utils/explore.ts | 19 ++++++++++++++++--- public/app/types/explore.ts | 7 +++++++ 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index 32135eab90a..d818b2ef090 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -13,6 +13,11 @@ const DEFAULT_EXPLORE_STATE: ExploreUrlState = { datasource: null, queries: [], range: DEFAULT_RANGE, + ui: { + showingGraph: true, + showingTable: true, + showingLogs: true, + } }; describe('state functions', () => { @@ -69,9 +74,11 @@ describe('state functions', () => { to: 'now', }, }; + expect(serializeStateToUrlParam(state)).toBe( '{"datasource":"foo","queries":[{"expr":"metric{test=\\"a/b\\"}"},' + - '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"}}' + '{"expr":"super{foo=\\"x/z\\"}"}],"range":{"from":"now-5h","to":"now"},' + + '"ui":{"showingGraph":true,"showingTable":true,"showingLogs":true}}' ); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7a9f54a0cae..07c8cf1d24b 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -20,6 +20,7 @@ import { ResultType, QueryIntervals, QueryOptions, + ExploreUrlUIState, } from 'app/types/explore'; export const DEFAULT_RANGE = { @@ -27,6 +28,12 @@ export const DEFAULT_RANGE = { to: 'now', }; +export const DEFAULT_UI_STATE = { + showingTable: true, + showingGraph: true, + showingLogs: true, +}; + const MAX_HISTORY_ITEMS = 100; export const LAST_USED_DATASOURCE_KEY = 'grafana.explore.datasource'; @@ -151,6 +158,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); + // debugger if (Array.isArray(parsed)) { if (parsed.length <= 3) { throw new Error('Error parsing compact URL state for Explore.'); @@ -161,19 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { }; const datasource = parsed[2]; const queries = parsed.slice(3); - return { datasource, queries, range }; + return { datasource, queries, range, ui: DEFAULT_UI_STATE }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, queries: [], range: DEFAULT_RANGE }; + return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; } +const serializeUIState = (state: ExploreUrlUIState) => { + return Object.keys(state).map((key) => ({ [key]: state[key] })); +}; + export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { + if (compact) { - return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries]); + return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries, ...serializeUIState(urlState.ui)]); } return JSON.stringify(urlState); } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 34b7ff08c99..d035b60d86a 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -231,10 +231,17 @@ export interface ExploreItemState { tableResult?: TableModel; } +export interface ExploreUrlUIState { + showingTable: boolean; + showingGraph: boolean; + showingLogs: boolean; +} + export interface ExploreUrlState { datasource: string; queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; + ui: ExploreUrlUIState; } export interface HistoryItem { From 91bd908e03ecdfbc691c13e66cb9512535ca78fb Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 31 Jan 2019 22:24:04 +0100 Subject: [PATCH 05/76] adds more tests signing out session --- pkg/services/auth/auth_token.go | 2 +- pkg/services/auth/auth_token_test.go | 35 +++++++++++++++++++++------- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index 5f8f36fc373..deb3c1a5bba 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -86,7 +86,7 @@ func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { if setting.Env == setting.DEV { - ctx.Logger.Info("new token", "unhashed token", value) + ctx.Logger.Debug("new token", "unhashed token", value) } ctx.Resp.Header().Del("Set-Cookie") diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index 47afe627479..e58fe795b4a 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -1,13 +1,15 @@ package auth import ( + "fmt" "net/http" + "net/http/httptest" "testing" "time" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -51,20 +53,37 @@ func TestUserAuthToken(t *testing.T) { }) Convey("signing out should delete token and cookie if present", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.1.1:1234", "user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.AuthToken}) + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken}) - ctx := &models.ReqContext{Context: &macaron.Context{Req: macaron.Request{Request: httpreq}}} + ctx := &models.ReqContext{Context: &macaron.Context{ + Req: macaron.Request{Request: httpreq}, + Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), + }, + Logger: log.New("fakelogger"), + } err = userAuthTokenService.SignOutUser(ctx) So(err, ShouldBeNil) // makes sure we tell the browser to overwrite the cookie - //So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, "") + cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName) + So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader) + }) + + Convey("signing out an none existing session should return an error", func() { + httpreq := &http.Request{Header: make(http.Header)} + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: "missing-session-cookie"}) + + ctx := &models.ReqContext{Context: &macaron.Context{ + Req: macaron.Request{Request: httpreq}, + Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), + }, + Logger: log.New("fakelogger"), + } + + err = userAuthTokenService.SignOutUser(ctx) + So(err, ShouldNotBeNil) }) }) From dd5a8275f107dbc54160fc271c3230096ba3707b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Feb 2019 01:21:23 +0100 Subject: [PATCH 06/76] must return json response from /api/login/ping Even though http error 401 was returned, the result was still a http 200 --- pkg/api/login.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/pkg/api/login.go b/pkg/api/login.go index 50c62e0835a..3f2d82a6c0f 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,12 +78,13 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response { - if c.IsSignedIn || c.IsAnonymous { - return JSON(200, "Logged in") +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { + if c.IsSignedIn || (c.AllowAnonymous && c.IsAnonymous) { + c.JsonOK("Logged in") + return } - return Error(401, "Unauthorized", nil) + c.JsonApiErr(401, "Unauthorized", nil) } func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { From bd830780250880f3c9920a5c1f5c467b3e5bdb2a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 1 Feb 2019 01:22:56 +0100 Subject: [PATCH 07/76] signout user if /api/login/ping returns 401 unauthorized --- public/app/core/services/backend_srv.ts | 37 +++++++++++++++++-------- 1 file changed, 26 insertions(+), 11 deletions(-) diff --git a/public/app/core/services/backend_srv.ts b/public/app/core/services/backend_srv.ts index 38d7f2b76cb..c73cc7661f5 100644 --- a/public/app/core/services/backend_srv.ts +++ b/public/app/core/services/backend_srv.ts @@ -1,6 +1,7 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; import appEvents from 'app/core/app_events'; +import config from 'app/core/config'; import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; export class BackendSrv { @@ -103,10 +104,17 @@ export class BackendSrv { err => { // handle unauthorized if (err.status === 401 && this.contextSrv.user.isSignedIn && firstAttempt) { - return this.loginPing().then(() => { - options.retry = 1; - return this.request(options); - }); + return this.loginPing() + .then(() => { + options.retry = 1; + return this.request(options); + }) + .catch(err => { + if (err.status === 401) { + window.location.href = config.appSubUrl + '/logout'; + throw err; + } + }); } this.$timeout(this.requestErrorHandler.bind(this, err), 50); @@ -184,13 +192,20 @@ export class BackendSrv { // handle unauthorized for backend requests if (requestIsLocal && firstAttempt && err.status === 401) { - return this.loginPing().then(() => { - options.retry = 1; - if (canceler) { - canceler.resolve(); - } - return this.datasourceRequest(options); - }); + return this.loginPing() + .then(() => { + options.retry = 1; + if (canceler) { + canceler.resolve(); + } + return this.datasourceRequest(options); + }) + .catch(err => { + if (err.status === 401) { + window.location.href = config.appSubUrl + '/logout'; + throw err; + } + }); } // populate error obj on Internal Error From a1b3986532dbbb51145471304fccc6f254899bfe Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Feb 2019 09:59:53 +0100 Subject: [PATCH 08/76] always delete session cookie even if db delete fails --- devenv/docker/blocks/loki/docker-compose.yaml | 2 ++ pkg/services/auth/auth_token.go | 14 +++----------- pkg/services/auth/auth_token_test.go | 2 +- 3 files changed, 6 insertions(+), 12 deletions(-) diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index d6cf21f7856..bd4f8d3c728 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -20,3 +20,5 @@ services: -config.file=/etc/promtail/docker-config.yaml networks: - loki + depends_on: + - loki diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go index deb3c1a5bba..5cb43974d34 100644 --- a/pkg/services/auth/auth_token.go +++ b/pkg/services/auth/auth_token.go @@ -121,18 +121,10 @@ func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { hashedToken := hashToken(unhashedToken) sql := `DELETE FROM user_auth_token WHERE auth_token = ?` - res, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - if err != nil { - return err - } + _, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - affected, _ := res.RowsAffected() - if affected > 0 { - s.writeSessionCookie(c, "", -1) - return nil - } - - return errors.New("failed to delete session") + s.writeSessionCookie(c, "", -1) + return err } func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go index e58fe795b4a..312e53a3970 100644 --- a/pkg/services/auth/auth_token_test.go +++ b/pkg/services/auth/auth_token_test.go @@ -73,7 +73,7 @@ func TestUserAuthToken(t *testing.T) { Convey("signing out an none existing session should return an error", func() { httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: "missing-session-cookie"}) + httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""}) ctx := &models.ReqContext{Context: &macaron.Context{ Req: macaron.Request{Request: httpreq}, From 6ab9355146193941c4e719e8cfa43af7f382179e Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 12:33:15 +0100 Subject: [PATCH 09/76] Restoring explore panels state from URL --- public/app/core/utils/explore.test.ts | 23 +++++- public/app/core/utils/explore.ts | 38 +++++++--- public/app/features/explore/Explore.tsx | 10 ++- .../app/features/explore/state/actionTypes.ts | 2 + public/app/features/explore/state/actions.ts | 73 ++++++++++++------- public/app/features/explore/state/reducers.ts | 3 +- public/app/types/explore.ts | 4 +- 7 files changed, 109 insertions(+), 44 deletions(-) diff --git a/public/app/core/utils/explore.test.ts b/public/app/core/utils/explore.test.ts index d818b2ef090..1c00142c3b8 100644 --- a/public/app/core/utils/explore.test.ts +++ b/public/app/core/utils/explore.test.ts @@ -100,7 +100,7 @@ describe('state functions', () => { }, }; expect(serializeStateToUrlParam(state, true)).toBe( - '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"}]' + '["now-5h","now","foo",{"expr":"metric{test=\\"a/b\\"}"},{"expr":"super{foo=\\"x/z\\"}"},{"ui":[true,true,true]}]' ); }); }); @@ -125,7 +125,28 @@ describe('state functions', () => { }; const serialized = serializeStateToUrlParam(state); const parsed = parseUrlState(serialized); + expect(state).toMatchObject(parsed); + }); + it('can parse the compact serialized state into the original state', () => { + const state = { + ...DEFAULT_EXPLORE_STATE, + datasource: 'foo', + queries: [ + { + expr: 'metric{test="a/b"}', + }, + { + expr: 'super{foo="x/z"}', + }, + ], + range: { + from: 'now - 5h', + to: 'now', + }, + }; + const serialized = serializeStateToUrlParam(state, true); + const parsed = parseUrlState(serialized); expect(state).toMatchObject(parsed); }); }); diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 07c8cf1d24b..7128019b1fb 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -20,7 +20,6 @@ import { ResultType, QueryIntervals, QueryOptions, - ExploreUrlUIState, } from 'app/types/explore'; export const DEFAULT_RANGE = { @@ -154,11 +153,13 @@ export function buildQueryTransaction( export const clearQueryKeys: ((query: DataQuery) => object) = ({ key, refId, ...rest }) => rest; +const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('expr'); +const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); + export function parseUrlState(initial: string | undefined): ExploreUrlState { if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); - // debugger if (Array.isArray(parsed)) { if (parsed.length <= 3) { throw new Error('Error parsing compact URL state for Explore.'); @@ -168,8 +169,24 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - const queries = parsed.slice(3); - return { datasource, queries, range, ui: DEFAULT_UI_STATE }; + let queries = [], + ui; + + parsed.slice(3).forEach(segment => { + if (isMetricSegment(segment)) { + queries = [...queries, segment]; + } + + if (isUISegment(segment)) { + ui = { + showingGraph: segment.ui[0], + showingLogs: segment.ui[1], + showingTable: segment.ui[2], + }; + } + }); + + return { datasource, queries, range, ui }; } return parsed; } catch (e) { @@ -179,14 +196,15 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; } -const serializeUIState = (state: ExploreUrlUIState) => { - return Object.keys(state).map((key) => ({ [key]: state[key] })); -}; - export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { - if (compact) { - return JSON.stringify([urlState.range.from, urlState.range.to, urlState.datasource, ...urlState.queries, ...serializeUIState(urlState.ui)]); + return JSON.stringify([ + urlState.range.from, + urlState.range.to, + urlState.datasource, + ...urlState.queries, + { ui: [!!urlState.ui.showingGraph, !!urlState.ui.showingLogs, !!urlState.ui.showingTable] }, + ]); } return JSON.stringify(urlState); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 909c4e81b8b..d08243c7118 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -32,7 +32,7 @@ import { import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; -import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; +import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore'; import { Emitter } from 'app/core/utils/emitter'; import { ExploreToolbar } from './ExploreToolbar'; @@ -61,7 +61,7 @@ interface ExploreProps { supportsGraph: boolean | null; supportsLogs: boolean | null; supportsTable: boolean | null; - urlState: ExploreUrlState; + urlState?: ExploreUrlState; } /** @@ -107,18 +107,20 @@ export class Explore extends React.PureComponent { // Don't initialize on split, but need to initialize urlparameters when present if (!initialized) { // Load URL state and parse range - const { datasource, queries, range = DEFAULT_RANGE } = (urlState || {}) as ExploreUrlState; + const { datasource, queries, range = DEFAULT_RANGE, ui = DEFAULT_UI_STATE } = (urlState || {}) as ExploreUrlState; const initialDatasource = datasource || store.get(LAST_USED_DATASOURCE_KEY); const initialQueries: DataQuery[] = ensureQueries(queries); const initialRange = { from: parseTime(range.from), to: parseTime(range.to) }; const width = this.el ? this.el.offsetWidth : 0; + this.props.initializeExplore( exploreId, initialDatasource, initialQueries, initialRange, width, - this.exploreEvents + this.exploreEvents, + ui ); } } diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index be7d5754bbe..3a0a564b651 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -8,6 +8,7 @@ import { RangeScanner, ResultType, QueryTransaction, + ExploreUIState, } from 'app/types/explore'; export enum ActionTypes { @@ -106,6 +107,7 @@ export interface InitializeExploreAction { exploreDatasources: DataSourceSelectItem[]; queries: DataQuery[]; range: RawTimeRange; + ui: ExploreUIState; }; } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 1a11b7fcac9..02502a1d94c 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -38,6 +38,7 @@ import { ResultType, QueryOptions, QueryTransaction, + ExploreUIState, } from 'app/types/explore'; import { @@ -154,7 +155,8 @@ export function initializeExplore( queries: DataQuery[], range: RawTimeRange, containerWidth: number, - eventBridge: Emitter + eventBridge: Emitter, + ui: ExploreUIState ): ThunkResult { return async dispatch => { const exploreDatasources: DataSourceSelectItem[] = getDatasourceSrv() @@ -175,6 +177,7 @@ export function initializeExplore( exploreDatasources, queries, range, + ui, }, }); @@ -258,10 +261,7 @@ export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): Que * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, * e.g., Prometheus -> Loki queries. */ -export const loadDatasourceSuccess = ( - exploreId: ExploreId, - instance: any, -): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): LoadDatasourceSuccessAction => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -766,6 +766,11 @@ export function stateSave() { datasource: left.datasourceInstance.name, queries: left.modifiedQueries.map(clearQueryKeys), range: left.range, + ui: { + showingGraph: left.showingGraph, + showingLogs: left.showingLogs, + showingTable: left.showingTable, + }, }; urlStates.left = serializeStateToUrlParam(leftUrlState, true); if (split) { @@ -773,48 +778,64 @@ export function stateSave() { datasource: right.datasourceInstance.name, queries: right.modifiedQueries.map(clearQueryKeys), range: right.range, + ui: { + showingGraph: right.showingGraph, + showingLogs: right.showingLogs, + showingTable: right.showingTable, + }, }; + urlStates.right = serializeStateToUrlParam(rightUrlState, true); } + dispatch(updateLocation({ query: urlStates })); }; } /** - * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + * Creates action to collapse graph/logs/table panel. When panel is collapsed, + * queries won't be run */ -export function toggleGraph(exploreId: ExploreId): ThunkResult { +const togglePanelActionCreator = (type: ActionTypes.ToggleGraph | ActionTypes.ToggleTable | ActionTypes.ToggleLogs) => ( + exploreId: ExploreId +) => { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); - if (getState().explore[exploreId].showingGraph) { + let shouldRunQueries; + dispatch({ type, payload: { exploreId } }); + dispatch(stateSave()); + + switch (type) { + case ActionTypes.ToggleGraph: + shouldRunQueries = getState().explore[exploreId].showingGraph; + break; + case ActionTypes.ToggleLogs: + shouldRunQueries = getState().explore[exploreId].showingLogs; + break; + case ActionTypes.ToggleTable: + shouldRunQueries = getState().explore[exploreId].showingTable; + break; + } + + if (shouldRunQueries) { dispatch(runQueries(exploreId)); } }; -} +}; + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export const toggleGraph = togglePanelActionCreator(ActionTypes.ToggleGraph); /** * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. */ -export function toggleLogs(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); - if (getState().explore[exploreId].showingLogs) { - dispatch(runQueries(exploreId)); - } - }; -} +export const toggleLogs = togglePanelActionCreator(ActionTypes.ToggleLogs); /** * Expand/collapse the table result viewer. When collapsed, table queries won't be run. */ -export function toggleTable(exploreId: ExploreId): ThunkResult { - return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); - if (getState().explore[exploreId].showingTable) { - dispatch(runQueries(exploreId)); - } - }; -} +export const toggleTable = togglePanelActionCreator(ActionTypes.ToggleTable); /** * Resets state for explore. diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index eb67beee3b3..4ad07ddfc88 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -163,7 +163,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { } case ActionTypes.InitializeExplore: { - const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload; + const { containerWidth, eventBridge, exploreDatasources, queries, range, ui } = action.payload; return { ...state, containerWidth, @@ -173,6 +173,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { initialQueries: queries, initialized: true, modifiedQueries: queries.slice(), + ...ui, }; } diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index d035b60d86a..3abbc652c0d 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -231,7 +231,7 @@ export interface ExploreItemState { tableResult?: TableModel; } -export interface ExploreUrlUIState { +export interface ExploreUIState { showingTable: boolean; showingGraph: boolean; showingLogs: boolean; @@ -241,7 +241,7 @@ export interface ExploreUrlState { datasource: string; queries: any[]; // Should be a DataQuery, but we're going to strip refIds, so typing makes less sense range: RawTimeRange; - ui: ExploreUrlUIState; + ui: ExploreUIState; } export interface HistoryItem { From 2ddccb4a214a1828bde0ffb3c0d0191773d8146a Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 12:57:09 +0100 Subject: [PATCH 10/76] Temporarily run queries independently from UI state of explore panels --- public/app/features/explore/state/actions.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 02502a1d94c..b24532c23f4 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -577,9 +577,9 @@ export function runQueries(exploreId: ExploreId) { const { datasourceInstance, modifiedQueries, - showingLogs, - showingGraph, - showingTable, + // showingLogs, + // showingGraph, + // showingTable, supportsGraph, supportsLogs, supportsTable, @@ -596,7 +596,7 @@ export function runQueries(exploreId: ExploreId) { const interval = datasourceInstance.interval; // Keep table queries first since they need to return quickly - if (showingTable && supportsTable) { + if (/*showingTable &&*/ supportsTable) { dispatch( runQueriesForType( exploreId, @@ -611,7 +611,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (showingGraph && supportsGraph) { + if (/*showingGraph &&*/ supportsGraph) { dispatch( runQueriesForType( exploreId, @@ -625,7 +625,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (showingLogs && supportsLogs) { + if (/*showingLogs &&*/ supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } dispatch(stateSave()); From 3c358e406e20f3a47b9d49fecadd53a6a2259843 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 14:56:54 +0100 Subject: [PATCH 11/76] Make runQueries action independent from datasource loading --- public/app/features/explore/state/actions.ts | 40 ++++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index b24532c23f4..c7b47d1c3c7 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -79,7 +79,15 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance)); - dispatch(loadDatasource(exploreId, newDataSourceInstance)); + + try { + await dispatch(loadDatasource(exploreId, newDataSourceInstance)); + } catch (error) { + console.error(error); + return; + } + + dispatch(runQueries(exploreId)); }; } @@ -197,7 +205,14 @@ export function initializeExplore( } dispatch(updateDatasourceInstance(exploreId, instance)); - dispatch(loadDatasource(exploreId, instance)); + + try { + await dispatch(loadDatasource(exploreId, instance)); + } catch (error) { + console.error(error); + return; + } + dispatch(runQueries(exploreId, true)); } else { dispatch(loadDatasourceMissing(exploreId)); } @@ -343,8 +358,8 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T // Keep ID to track selection dispatch(loadDatasourcePending(exploreId, datasourceName)); - let datasourceError = null; + try { const testResult = await instance.testDatasource(); datasourceError = testResult.status === 'success' ? null : testResult.message; @@ -354,7 +369,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T if (datasourceError) { dispatch(loadDatasourceFailure(exploreId, datasourceError)); - return; + return Promise.reject(`${datasourceName} loading failed`); } if (datasourceName !== getState().explore[exploreId].requestedDatasourceName) { @@ -372,7 +387,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T } dispatch(loadDatasourceSuccess(exploreId, instance)); - dispatch(runQueries(exploreId)); + return Promise.resolve(); }; } @@ -572,14 +587,14 @@ export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult /** * Main action to run queries and dispatches sub-actions based on which result viewers are active */ -export function runQueries(exploreId: ExploreId) { +export function runQueries(exploreId: ExploreId, ignoreUIState = false) { return (dispatch, getState) => { const { datasourceInstance, modifiedQueries, - // showingLogs, - // showingGraph, - // showingTable, + showingLogs, + showingGraph, + showingTable, supportsGraph, supportsLogs, supportsTable, @@ -596,7 +611,7 @@ export function runQueries(exploreId: ExploreId) { const interval = datasourceInstance.interval; // Keep table queries first since they need to return quickly - if (/*showingTable &&*/ supportsTable) { + if ((ignoreUIState || showingTable) && supportsTable) { dispatch( runQueriesForType( exploreId, @@ -611,7 +626,7 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (/*showingGraph &&*/ supportsGraph) { + if ((ignoreUIState || showingGraph) && supportsGraph) { dispatch( runQueriesForType( exploreId, @@ -625,9 +640,10 @@ export function runQueries(exploreId: ExploreId) { ) ); } - if (/*showingLogs &&*/ supportsLogs) { + if ((ignoreUIState || showingLogs) && supportsLogs) { dispatch(runQueriesForType(exploreId, 'Logs', { interval, format: 'logs' })); } + dispatch(stateSave()); }; } From 1a0b21b8d1e2dc13037a908e7bbb2deba327acfb Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Fri, 1 Feb 2019 15:27:02 +0100 Subject: [PATCH 12/76] Minor post review changes --- public/app/core/utils/explore.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7128019b1fb..faf46118718 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -157,6 +157,8 @@ const isMetricSegment = (segment: { [key: string]: string }) => segment.hasOwnPr const isUISegment = (segment: { [key: string]: string }) => segment.hasOwnProperty('ui'); export function parseUrlState(initial: string | undefined): ExploreUrlState { + let uiState = DEFAULT_UI_STATE; + if (initial) { try { const parsed = JSON.parse(decodeURI(initial)); @@ -169,8 +171,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { to: parsed[1], }; const datasource = parsed[2]; - let queries = [], - ui; + let queries = []; parsed.slice(3).forEach(segment => { if (isMetricSegment(segment)) { @@ -178,7 +179,7 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } if (isUISegment(segment)) { - ui = { + uiState = { showingGraph: segment.ui[0], showingLogs: segment.ui[1], showingTable: segment.ui[2], @@ -186,14 +187,14 @@ export function parseUrlState(initial: string | undefined): ExploreUrlState { } }); - return { datasource, queries, range, ui }; + return { datasource, queries, range, ui: uiState }; } return parsed; } catch (e) { console.error(e); } } - return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: DEFAULT_UI_STATE }; + return { datasource: null, queries: [], range: DEFAULT_RANGE, ui: uiState }; } export function serializeStateToUrlParam(urlState: ExploreUrlState, compact?: boolean): string { From bd6fed54de73d195df50a7e02666a454e3120726 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Fri, 1 Feb 2019 15:45:47 +0100 Subject: [PATCH 13/76] first stuff --- .../ValueMappingsEditor.story.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx new file mode 100644 index 00000000000..31ba6454753 --- /dev/null +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { storiesOf } from '@storybook/react'; +import { action } from '@storybook/addon-actions'; +import { ValueMappingsEditor } from './ValueMappingsEditor'; +import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; + +const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); + +ValueMappingsEditorStories.addDecorator(withCenteredStory); + +ValueMappingsEditorStories.add('default', () => { + return ( + { + action('Mapping changed'); + }} + /> + ); +}); From 9ac960a80380b6b7612c443c041c0055bbd95759 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Sat, 2 Feb 2019 00:48:13 +0100 Subject: [PATCH 14/76] did not add file, removing centerered --- .../ValueMappingsEditor/ValueMappingsEditor.story.tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx index 31ba6454753..d6c8cec8c1e 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -2,12 +2,9 @@ import React from 'react'; import { storiesOf } from '@storybook/react'; import { action } from '@storybook/addon-actions'; import { ValueMappingsEditor } from './ValueMappingsEditor'; -import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); -ValueMappingsEditorStories.addDecorator(withCenteredStory); - ValueMappingsEditorStories.add('default', () => { return ( Date: Sun, 3 Feb 2019 16:29:35 -0500 Subject: [PATCH 15/76] Add AWS/Neptune to metricsMap and dimensionsMap --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index dfa03d2dfa9..f898a65f911 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -95,6 +95,7 @@ func init() { "AWS/Logs": {"IncomingBytes", "IncomingLogEvents", "ForwardedBytes", "ForwardedLogEvents", "DeliveryErrors", "DeliveryThrottling"}, "AWS/ML": {"PredictCount", "PredictFailureCount"}, "AWS/NATGateway": {"PacketsOutToDestination", "PacketsOutToSource", "PacketsInFromSource", "PacketsInFromDestination", "BytesOutToDestination", "BytesOutToSource", "BytesInFromSource", "BytesInFromDestination", "ErrorPortAllocation", "ActiveConnectionCount", "ConnectionAttemptCount", "ConnectionEstablishedCount", "IdleTimeoutCount", "PacketsDropCount"}, + "AWS/Neptune": {"CPUUtilization", "ClusterReplicaLag", "ClusterReplicaLagMaximum", "ClusterReplicaLagMinimum", "EngineUptime", "FreeableMemory", "FreeLocalStorage", "GremlinHttp1xx", "GremlinHttp2xx", "GremlinHttp4xx", "GremlinHttp5xx", "GremlinErrors", "GremlinRequests", "GremlinRequestsPerSec", "GremlinWebSocketSuccess", "GremlinWebSocketClientErrors", "GremlinWebSocketServerErrors", "GremlinWebSocketAvailableConnections", "Http1xx", "Http2xx", "Http4xx", "Http5xx", "Http100", "Http101", "Http200", "Http400", "Http403", "Http405", "Http413", "Http429", "Http500", "Http501", "LoaderErrors", "LoaderRequests", "NetworkReceiveThroughput", "NetworkThroughput", "NetworkTransmitThroughput", "SparqlHttp1xx", "SparqlHttp2xx", "SparqlHttp4xx", "SparqlHttp5xx", "SparqlErrors", "SparqlRequests", "SparqlRequestsPerSec", "StatusErrors", "StatusRequests", "VolumeBytesUsed", "VolumeReadIOPs", "VolumeWriteIOPs"}, "AWS/NetworkELB": {"ActiveFlowCount", "ConsumedLCUs", "HealthyHostCount", "NewFlowCount", "ProcessedBytes", "TCP_Client_Reset_Count", "TCP_ELB_Reset_Count", "TCP_Target_Reset_Count", "UnHealthyHostCount"}, "AWS/OpsWorks": {"cpu_idle", "cpu_nice", "cpu_system", "cpu_user", "cpu_waitio", "load_1", "load_5", "load_15", "memory_buffers", "memory_cached", "memory_free", "memory_swap", "memory_total", "memory_used", "procs"}, "AWS/Redshift": {"CPUUtilization", "DatabaseConnections", "HealthStatus", "MaintenanceMode", "NetworkReceiveThroughput", "NetworkTransmitThroughput", "PercentageDiskSpaceUsed", "QueriesCompletedPerSecond", "QueryDuration", "QueryRuntimeBreakdown", "ReadIOPS", "ReadLatency", "ReadThroughput", "WLMQueriesCompletedPerSecond", "WLMQueryDuration", "WLMQueueLength", "WriteIOPS", "WriteLatency", "WriteThroughput"}, @@ -149,6 +150,7 @@ func init() { "AWS/Logs": {"LogGroupName", "DestinationType", "FilterName"}, "AWS/ML": {"MLModelId", "RequestMode"}, "AWS/NATGateway": {"NatGatewayId"}, + "AWS/Neptune": {"DBClusterIdentifier", "Role", "DatabaseClass", "EngineName"}, "AWS/NetworkELB": {"LoadBalancer", "TargetGroup", "AvailabilityZone"}, "AWS/OpsWorks": {"StackId", "LayerId", "InstanceId"}, "AWS/Redshift": {"NodeID", "ClusterIdentifier", "latency", "service class", "wmlid"}, From 43f8098981e0328d6d06e75cd746cf5f4b1e9912 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 28 Jan 2019 17:41:33 +0100 Subject: [PATCH 16/76] Removed the on every key change event --- public/app/features/explore/QueryField.tsx | 68 ++++++++++++---------- 1 file changed, 36 insertions(+), 32 deletions(-) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 85315d2bdef..db6efb88f52 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -132,11 +132,11 @@ export class QueryField extends React.PureComponent { + onChange = ({ value }, invokeParentOnValueChanged?: boolean) => { const documentChanged = value.document !== this.state.value.document; const prevValue = this.state.value; @@ -144,7 +144,7 @@ export class QueryField extends React.PureComponent { if (documentChanged) { const textChanged = Plain.serialize(prevValue) !== Plain.serialize(value); - if (textChanged) { + if (textChanged && invokeParentOnValueChanged) { this.handleChangeValue(); } } @@ -288,8 +288,37 @@ export class QueryField extends React.PureComponent { + handleEnterAndTabKey = change => { const { typeaheadIndex, suggestions } = this.state; + if (this.menuEl) { + // Dont blur input + event.preventDefault(); + if (!suggestions || suggestions.length === 0) { + return undefined; + } + + const suggestion = getSuggestionByIndex(suggestions, typeaheadIndex); + const nextChange = this.applyTypeahead(change, suggestion); + + const insertTextOperation = nextChange.operations.find(operation => operation.type === 'insert_text'); + if (insertTextOperation) { + const suggestionText = insertTextOperation.text; + this.placeholdersBuffer.setNextPlaceholderValue(suggestionText); + if (this.placeholdersBuffer.hasPlaceholders()) { + nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus(); + } + } + + return true; + } else { + this.handleChangeValue(); + + return undefined; + } + }; + + onKeyDown = (event, change) => { + const { typeaheadIndex } = this.state; switch (event.key) { case 'Escape': { @@ -312,27 +341,7 @@ export class QueryField extends React.PureComponent operation.type === 'insert_text'); - if (insertTextOperation) { - const suggestionText = insertTextOperation.text; - this.placeholdersBuffer.setNextPlaceholderValue(suggestionText); - if (this.placeholdersBuffer.hasPlaceholders()) { - nextChange.move(this.placeholdersBuffer.getNextMoveOffset()).focus(); - } - } - - return true; - } + return this.handleEnterAndTabKey(change); break; } @@ -364,12 +373,7 @@ export class QueryField extends React.PureComponent { if (this.mounted) { - this.setState({ - suggestions: [], - typeaheadIndex: 0, - typeaheadPrefix: '', - typeaheadContext: null, - }); + this.setState({ suggestions: [], typeaheadIndex: 0, typeaheadPrefix: '', typeaheadContext: null }); this.resetTimer = null; } }; @@ -396,7 +400,7 @@ export class QueryField extends React.PureComponent { // Manually triggering change const change = this.applyTypeahead(this.state.value.change(), item); - this.onChange(change); + this.onChange(change, true); }; updateMenu = () => { From acea1d7f0015d0014364d84d190f5e5b0eafae71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Fri, 1 Feb 2019 11:55:01 +0100 Subject: [PATCH 17/76] Alignment of interfaces and components --- packages/grafana-ui/src/types/plugin.ts | 18 ++++++++- public/app/features/explore/QueryField.tsx | 34 ++++++++--------- public/app/features/explore/QueryRow.tsx | 14 ++++--- .../loki/components/LokiQueryField.tsx | 38 +++++++++---------- .../prometheus/components/PromQueryField.tsx | 35 +++++++++-------- public/app/store/configureStore.ts | 4 +- 6 files changed, 77 insertions(+), 66 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 00735827825..1be862e17f3 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -41,6 +41,12 @@ export interface DataSourceApi { pluginExports?: PluginExports; } +export interface ExploreDataSourceApi extends DataSourceApi { + modifyQuery?(query: TQuery, action: any): TQuery; + getHighlighterExpression?(query: TQuery): string; + languageProvider?: any; +} + export interface QueryEditorProps { datasource: DSType; query: TQuery; @@ -48,6 +54,16 @@ export interface QueryEditorProps void; } +export interface ExploreQueryFieldProps { + datasource: DSType; + initialQuery: TQuery; + error?: string | JSX.Element; + hint?: QueryHint; + history: any[]; + onExecuteQuery?: () => void; + onQueryChange?: (value: TQuery) => void; +} + export interface PluginExports { Datasource?: DataSourceApi; QueryCtrl?: any; @@ -55,7 +71,7 @@ export interface PluginExports { ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; - ExploreQueryField?: any; + ExploreQueryField?: ComponentClass>; ExploreStartPage?: any; // Panel plugin diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index db6efb88f52..880bedd7905 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -33,10 +33,9 @@ export interface QueryFieldProps { cleanText?: (text: string) => string; disabled?: boolean; initialQuery: string | null; - onBlur?: () => void; - onFocus?: () => void; + onExecuteQuery?: () => void; + onQueryChange?: (value: string) => void; onTypeahead?: (typeahead: TypeaheadInput) => TypeaheadOutput; - onValueChanged?: (value: string) => void; onWillApplySuggestion?: (suggestion: string, state: QueryFieldState) => string; placeholder?: string; portalOrigin?: string; @@ -145,7 +144,7 @@ export class QueryField extends React.PureComponent { + executeOnQueryChangeAndExecuteQueries = () => { // Send text change to parent - const { onValueChanged } = this.props; - if (onValueChanged) { - onValueChanged(Plain.serialize(this.state.value)); + const { onQueryChange, onExecuteQuery } = this.props; + if (onQueryChange) { + onQueryChange(Plain.serialize(this.state.value)); + } + + if (onExecuteQuery) { + onExecuteQuery(); } }; @@ -311,7 +314,7 @@ export class QueryField extends React.PureComponent { - const { onBlur } = this.props; // If we dont wait here, menu clicks wont work because the menu // will be gone. this.resetTimer = setTimeout(this.resetTypeahead, 100); // Disrupting placeholder entry wipes all remaining placeholders needing input this.placeholdersBuffer.clearPlaceholders(); - if (onBlur) { - onBlur(); - } + + this.executeOnQueryChangeAndExecuteQueries(); }; - handleFocus = () => { - const { onFocus } = this.props; - if (onFocus) { - onFocus(); - } - }; + handleFocus = () => {}; onClickMenu = (item: CompletionItem) => { // Manually triggering change diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index f6181161d56..7de728edb99 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -20,7 +20,7 @@ import { // Types import { StoreState } from 'app/types'; -import { RawTimeRange, DataQuery, QueryHint } from '@grafana/ui'; +import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; @@ -37,7 +37,7 @@ interface QueryRowProps { changeQuery: typeof changeQuery; className?: string; exploreId: ExploreId; - datasourceInstance: any; + datasourceInstance: ExploreDataSourceApi; highlightLogsExpression: typeof highlightLogsExpression; history: HistoryItem[]; index: number; @@ -115,13 +115,15 @@ export class QueryRow extends PureComponent { {QueryField ? ( ) : ( void; - onPressEnter?: () => void; - onQueryChange?: (value: LokiQuery, override?: boolean) => void; +interface LokiQueryFieldProps extends ExploreQueryFieldProps { + history: HistoryItem[]; } interface LokiQueryFieldState { @@ -98,14 +91,14 @@ export class LokiQueryField extends React.PureComponent node.type === 'code_block', getSyntax: node => 'promql', }), ]; - this.pluginsSearch = [RunnerPlugin({ handler: props.onPressEnter })]; + this.pluginsSearch = [RunnerPlugin({ handler: props.onExecuteQuery })]; this.state = { logLabelOptions: [], @@ -169,21 +162,25 @@ export class LokiQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange } = this.props; + const { initialQuery, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { const query = { ...initialQuery, expr: value, }; - onQueryChange(query, override); + onQueryChange(query); + + if (override && onExecuteQuery) { + onExecuteQuery(); + } } }; onClickHintFix = () => { - const { hint, onClickHintFix } = this.props; - if (onClickHintFix && hint && hint.fix) { - onClickHintFix(hint.fix.action); - } + // const { hint, onClickHintFix } = this.props; + // if (onClickHintFix && hint && hint.fix) { + // onClickHintFix(hint.fix.action); + // } }; onUpdateLanguage = () => { @@ -243,7 +240,8 @@ export class LokiQueryField extends React.PureComponent void; - onPressEnter?: () => void; - onQueryChange?: (value: PromQuery, override?: boolean) => void; +interface PromQueryFieldProps extends ExploreQueryFieldProps { + history: HistoryItem[]; } interface PromQueryFieldState { @@ -116,7 +110,7 @@ class PromQueryField extends React.PureComponent node.type === 'code_block', getSyntax: node => 'promql', @@ -174,21 +168,25 @@ class PromQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange } = this.props; + const { initialQuery, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { const query: PromQuery = { ...initialQuery, expr: value, }; - onQueryChange(query, override); + onQueryChange(query); + + if (override && onExecuteQuery) { + onExecuteQuery(); + } } }; onClickHintFix = () => { - const { hint, onClickHintFix } = this.props; - if (onClickHintFix && hint && hint.fix) { - onClickHintFix(hint.fix.action); - } + // const { hint, onClickHintFix } = this.props; + // if (onClickHintFix && hint && hint.fix) { + // onClickHintFix(hint.fix.action); + // } }; onUpdateLanguage = () => { @@ -264,7 +262,8 @@ class PromQueryField extends React.PureComponent Date: Fri, 1 Feb 2019 12:54:16 +0100 Subject: [PATCH 18/76] More types and some refactoring --- packages/grafana-ui/src/types/plugin.ts | 5 +++-- public/app/features/explore/QueryField.tsx | 2 -- public/app/features/explore/QueryRow.tsx | 14 ++++++-------- public/app/features/explore/state/actionTypes.ts | 6 +++--- public/app/features/explore/state/actions.ts | 10 ++++++++-- public/app/features/explore/state/reducers.ts | 2 +- .../datasource/loki/components/LokiQueryField.tsx | 8 ++++---- .../prometheus/components/PromQueryField.tsx | 8 ++++---- 8 files changed, 29 insertions(+), 26 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index 1be862e17f3..e951e91a223 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -1,6 +1,6 @@ import { ComponentClass } from 'react'; import { PanelProps, PanelOptionsProps } from './panel'; -import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint } from './datasource'; +import { DataQueryOptions, DataQuery, DataQueryResponse, QueryHint, QueryFixAction } from './datasource'; export interface DataSourceApi { /** @@ -42,7 +42,7 @@ export interface DataSourceApi { } export interface ExploreDataSourceApi extends DataSourceApi { - modifyQuery?(query: TQuery, action: any): TQuery; + modifyQuery?(query: TQuery, action: QueryFixAction): TQuery; getHighlighterExpression?(query: TQuery): string; languageProvider?: any; } @@ -62,6 +62,7 @@ export interface ExploreQueryFieldProps void; onQueryChange?: (value: TQuery) => void; + onExecuteHint?: (action: QueryFixAction) => void; } export interface PluginExports { diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 880bedd7905..a0e70e8066c 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -387,8 +387,6 @@ export class QueryField extends React.PureComponent {}; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 7de728edb99..bbc0bf0d101 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -20,7 +20,7 @@ import { // Types import { StoreState } from 'app/types'; -import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint } from '@grafana/ui'; +import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; @@ -78,10 +78,10 @@ export class QueryRow extends PureComponent { this.onChangeQuery(null, true); }; - onClickHintFix = action => { + onClickHintFix = (action: QueryFixAction) => { const { datasourceInstance, exploreId, index } = this.props; if (datasourceInstance && datasourceInstance.modifyQuery) { - const modifier = (queries: DataQuery, action: any) => datasourceInstance.modifyQuery(queries, action); + const modifier = (queries: DataQuery, action: QueryFixAction) => datasourceInstance.modifyQuery(queries, action); this.props.modifyQueries(exploreId, action, index, modifier); } }; @@ -116,14 +116,12 @@ export class QueryRow extends PureComponent { ) : ( DataQuery[]; + modifier: (queries: DataQuery[], modification: QueryFixAction) => DataQuery[]; }; } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 1a11b7fcac9..63432e9c516 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -30,6 +30,7 @@ import { DataQuery, DataSourceSelectItem, QueryHint, + QueryFixAction, } from '@grafana/ui/src/types'; import { ExploreId, @@ -54,6 +55,7 @@ import { ScanStopAction, UpdateDatasourceInstanceAction, QueriesImported, + ModifyQueriesAction, } from './actionTypes'; type ThunkResult = ThunkAction; @@ -385,12 +387,16 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T */ export function modifyQueries( exploreId: ExploreId, - modification: any, + modification: QueryFixAction, index: number, modifier: any ): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ModifyQueries, payload: { exploreId, modification, index, modifier } }); + const modifyQueryAction: ModifyQueriesAction = { + type: ActionTypes.ModifyQueries, + payload: { exploreId, modification, index, modifier }, + }; + dispatch(modifyQueryAction); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index eb67beee3b3..14c8d87bbd2 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -230,7 +230,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { case ActionTypes.ModifyQueries: { const { initialQueries, modifiedQueries, queryTransactions } = state; - const { modification, index, modifier } = action.payload as any; + const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { diff --git a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx index 76d2facc5b6..5046c353f17 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryField.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryField.tsx @@ -177,10 +177,10 @@ export class LokiQueryField extends React.PureComponent { - // const { hint, onClickHintFix } = this.props; - // if (onClickHintFix && hint && hint.fix) { - // onClickHintFix(hint.fix.action); - // } + const { hint, onExecuteHint } = this.props; + if (onExecuteHint && hint && hint.fix) { + onExecuteHint(hint.fix.action); + } }; onUpdateLanguage = () => { diff --git a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx index 4bdd9f17392..c86ea5c4072 100644 --- a/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromQueryField.tsx @@ -183,10 +183,10 @@ class PromQueryField extends React.PureComponent { - // const { hint, onClickHintFix } = this.props; - // if (onClickHintFix && hint && hint.fix) { - // onClickHintFix(hint.fix.action); - // } + const { hint, onExecuteHint } = this.props; + if (onExecuteHint && hint && hint.fix) { + onExecuteHint(hint.fix.action); + } }; onUpdateLanguage = () => { From 1f5bb767186b8b0c36594771d1602bffef2af68d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 07:47:10 +0100 Subject: [PATCH 19/76] Refactor of action, actionTypes and reducer --- public/app/features/explore/Explore.tsx | 17 +- public/app/features/explore/QueryRow.tsx | 23 +- public/app/features/explore/Wrapper.tsx | 14 +- .../app/features/explore/state/actionTypes.ts | 664 ++++++++++-------- public/app/features/explore/state/actions.ts | 296 +++----- .../features/explore/state/reducers.test.ts | 65 +- public/app/features/explore/state/reducers.ts | 313 +++++---- 7 files changed, 698 insertions(+), 694 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 06a6ae24cac..31ffdf4ab24 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -18,15 +18,7 @@ import TableContainer from './TableContainer'; import TimePicker, { parseTime } from './TimePicker'; // Actions -import { - changeSize, - changeTime, - initializeExplore, - modifyQueries, - scanStart, - scanStop, - setQueries, -} from './state/actions'; +import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; @@ -35,6 +27,7 @@ import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; import { Emitter } from 'app/core/utils/emitter'; import { ExploreToolbar } from './ExploreToolbar'; +import { scanStopAction } from './state/actionTypes'; interface ExploreProps { StartPage?: any; @@ -54,7 +47,7 @@ interface ExploreProps { scanning?: boolean; scanRange?: RawTimeRange; scanStart: typeof scanStart; - scanStop: typeof scanStop; + scanStopAction: typeof scanStopAction; setQueries: typeof setQueries; split: boolean; showingStartPage?: boolean; @@ -171,7 +164,7 @@ export class Explore extends React.PureComponent { }; onStopScanning = () => { - this.props.scanStop(this.props.exploreId); + this.props.scanStopAction({ exploreId: this.props.exploreId }); }; render() { @@ -281,7 +274,7 @@ const mapDispatchToProps = { initializeExplore, modifyQueries, scanStart, - scanStop, + scanStopAction, setQueries, }; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index bbc0bf0d101..5e2e8442e54 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -9,20 +9,14 @@ import QueryEditor from './QueryEditor'; import QueryTransactionStatus from './QueryTransactionStatus'; // Actions -import { - addQueryRow, - changeQuery, - highlightLogsExpression, - modifyQueries, - removeQueryRow, - runQueries, -} from './state/actions'; +import { changeQuery, modifyQueries, runQueries, addQueryRow } from './state/actions'; // Types import { StoreState } from 'app/types'; import { RawTimeRange, DataQuery, ExploreDataSourceApi, QueryHint, QueryFixAction } from '@grafana/ui'; import { QueryTransaction, HistoryItem, ExploreItemState, ExploreId } from 'app/types/explore'; import { Emitter } from 'app/core/utils/emitter'; +import { highlightLogsExpressionAction, removeQueryRowAction } from './state/actionTypes'; function getFirstHintFromTransactions(transactions: QueryTransaction[]): QueryHint { const transaction = transactions.find(qt => qt.hints && qt.hints.length > 0); @@ -38,7 +32,7 @@ interface QueryRowProps { className?: string; exploreId: ExploreId; datasourceInstance: ExploreDataSourceApi; - highlightLogsExpression: typeof highlightLogsExpression; + highlightLogsExpressionAction: typeof highlightLogsExpressionAction; history: HistoryItem[]; index: number; initialQuery: DataQuery; @@ -46,7 +40,7 @@ interface QueryRowProps { queryTransactions: QueryTransaction[]; exploreEvents: Emitter; range: RawTimeRange; - removeQueryRow: typeof removeQueryRow; + removeQueryRowAction: typeof removeQueryRowAction; runQueries: typeof runQueries; } @@ -88,14 +82,15 @@ export class QueryRow extends PureComponent { onClickRemoveButton = () => { const { exploreId, index } = this.props; - this.props.removeQueryRow(exploreId, index); + this.props.removeQueryRowAction({ exploreId, index }); }; updateLogsHighlights = _.debounce((value: DataQuery) => { const { datasourceInstance } = this.props; if (datasourceInstance.getHighlighterExpression) { + const { exploreId } = this.props; const expressions = [datasourceInstance.getHighlighterExpression(value)]; - this.props.highlightLogsExpression(this.props.exploreId, expressions); + this.props.highlightLogsExpressionAction({ exploreId, expressions }); } }, 500); @@ -168,9 +163,9 @@ function mapStateToProps(state: StoreState, { exploreId, index }) { const mapDispatchToProps = { addQueryRow, changeQuery, - highlightLogsExpression, + highlightLogsExpressionAction, modifyQueries, - removeQueryRow, + removeQueryRowAction, runQueries, }; diff --git a/public/app/features/explore/Wrapper.tsx b/public/app/features/explore/Wrapper.tsx index aca2e6d8cbd..f64b2704b71 100644 --- a/public/app/features/explore/Wrapper.tsx +++ b/public/app/features/explore/Wrapper.tsx @@ -7,16 +7,16 @@ import { StoreState } from 'app/types'; import { ExploreId, ExploreUrlState } from 'app/types/explore'; import { parseUrlState } from 'app/core/utils/explore'; -import { initializeExploreSplit, resetExplore } from './state/actions'; import ErrorBoundary from './ErrorBoundary'; import Explore from './Explore'; import { CustomScrollbar } from '@grafana/ui'; +import { initializeExploreSplitAction, resetExploreAction } from './state/actionTypes'; interface WrapperProps { - initializeExploreSplit: typeof initializeExploreSplit; + initializeExploreSplitAction: typeof initializeExploreSplitAction; split: boolean; updateLocation: typeof updateLocation; - resetExplore: typeof resetExplore; + resetExploreAction: typeof resetExploreAction; urlStates: { [key: string]: string }; } @@ -39,12 +39,12 @@ export class Wrapper extends Component { componentDidMount() { if (this.initialSplit) { - this.props.initializeExploreSplit(); + this.props.initializeExploreSplitAction(); } } componentWillUnmount() { - this.props.resetExplore(); + this.props.resetExploreAction(); } render() { @@ -77,9 +77,9 @@ const mapStateToProps = (state: StoreState) => { }; const mapDispatchToProps = { - initializeExploreSplit, + initializeExploreSplitAction, updateLocation, - resetExplore, + resetExploreAction, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(Wrapper)); diff --git a/public/app/features/explore/state/actionTypes.ts b/public/app/features/explore/state/actionTypes.ts index 53954f4dc2b..05ef661a8e5 100644 --- a/public/app/features/explore/state/actionTypes.ts +++ b/public/app/features/explore/state/actionTypes.ts @@ -1,6 +1,13 @@ // Types import { Emitter } from 'app/core/core'; -import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryFixAction } from '@grafana/ui/src/types'; +import { + RawTimeRange, + TimeRange, + DataQuery, + DataSourceSelectItem, + DataSourceApi, + QueryFixAction, +} from '@grafana/ui/src/types'; import { ExploreId, ExploreItemState, @@ -9,233 +16,26 @@ import { ResultType, QueryTransaction, } from 'app/types/explore'; +import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux/actionCreatorFactory'; +/** Higher order actions + * + */ export enum ActionTypes { - AddQueryRow = 'explore/ADD_QUERY_ROW', - ChangeDatasource = 'explore/CHANGE_DATASOURCE', - ChangeQuery = 'explore/CHANGE_QUERY', - ChangeSize = 'explore/CHANGE_SIZE', - ChangeTime = 'explore/CHANGE_TIME', - ClearQueries = 'explore/CLEAR_QUERIES', - HighlightLogsExpression = 'explore/HIGHLIGHT_LOGS_EXPRESSION', - InitializeExplore = 'explore/INITIALIZE_EXPLORE', InitializeExploreSplit = 'explore/INITIALIZE_EXPLORE_SPLIT', - LoadDatasourceFailure = 'explore/LOAD_DATASOURCE_FAILURE', - LoadDatasourceMissing = 'explore/LOAD_DATASOURCE_MISSING', - LoadDatasourcePending = 'explore/LOAD_DATASOURCE_PENDING', - LoadDatasourceSuccess = 'explore/LOAD_DATASOURCE_SUCCESS', - ModifyQueries = 'explore/MODIFY_QUERIES', - QueryTransactionFailure = 'explore/QUERY_TRANSACTION_FAILURE', - QueryTransactionStart = 'explore/QUERY_TRANSACTION_START', - QueryTransactionSuccess = 'explore/QUERY_TRANSACTION_SUCCESS', - RemoveQueryRow = 'explore/REMOVE_QUERY_ROW', - RunQueries = 'explore/RUN_QUERIES', - RunQueriesEmpty = 'explore/RUN_QUERIES_EMPTY', - ScanRange = 'explore/SCAN_RANGE', - ScanStart = 'explore/SCAN_START', - ScanStop = 'explore/SCAN_STOP', - SetQueries = 'explore/SET_QUERIES', SplitClose = 'explore/SPLIT_CLOSE', SplitOpen = 'explore/SPLIT_OPEN', - StateSave = 'explore/STATE_SAVE', - ToggleGraph = 'explore/TOGGLE_GRAPH', - ToggleLogs = 'explore/TOGGLE_LOGS', - ToggleTable = 'explore/TOGGLE_TABLE', - UpdateDatasourceInstance = 'explore/UPDATE_DATASOURCE_INSTANCE', ResetExplore = 'explore/RESET_EXPLORE', - QueriesImported = 'explore/QueriesImported', -} - -export interface AddQueryRowAction { - type: ActionTypes.AddQueryRow; - payload: { - exploreId: ExploreId; - index: number; - query: DataQuery; - }; -} - -export interface ChangeQueryAction { - type: ActionTypes.ChangeQuery; - payload: { - exploreId: ExploreId; - query: DataQuery; - index: number; - override: boolean; - }; -} - -export interface ChangeSizeAction { - type: ActionTypes.ChangeSize; - payload: { - exploreId: ExploreId; - width: number; - height: number; - }; -} - -export interface ChangeTimeAction { - type: ActionTypes.ChangeTime; - payload: { - exploreId: ExploreId; - range: TimeRange; - }; -} - -export interface ClearQueriesAction { - type: ActionTypes.ClearQueries; - payload: { - exploreId: ExploreId; - }; -} - -export interface HighlightLogsExpressionAction { - type: ActionTypes.HighlightLogsExpression; - payload: { - exploreId: ExploreId; - expressions: string[]; - }; -} - -export interface InitializeExploreAction { - type: ActionTypes.InitializeExplore; - payload: { - exploreId: ExploreId; - containerWidth: number; - eventBridge: Emitter; - exploreDatasources: DataSourceSelectItem[]; - queries: DataQuery[]; - range: RawTimeRange; - }; } export interface InitializeExploreSplitAction { type: ActionTypes.InitializeExploreSplit; -} - -export interface LoadDatasourceFailureAction { - type: ActionTypes.LoadDatasourceFailure; - payload: { - exploreId: ExploreId; - error: string; - }; -} - -export interface LoadDatasourcePendingAction { - type: ActionTypes.LoadDatasourcePending; - payload: { - exploreId: ExploreId; - requestedDatasourceName: string; - }; -} - -export interface LoadDatasourceMissingAction { - type: ActionTypes.LoadDatasourceMissing; - payload: { - exploreId: ExploreId; - }; -} - -export interface LoadDatasourceSuccessAction { - type: ActionTypes.LoadDatasourceSuccess; - payload: { - exploreId: ExploreId; - StartPage?: any; - datasourceInstance: any; - history: HistoryItem[]; - logsHighlighterExpressions?: any[]; - showingStartPage: boolean; - supportsGraph: boolean; - supportsLogs: boolean; - supportsTable: boolean; - }; -} - -export interface ModifyQueriesAction { - type: ActionTypes.ModifyQueries; - payload: { - exploreId: ExploreId; - modification: QueryFixAction; - index: number; - modifier: (queries: DataQuery[], modification: QueryFixAction) => DataQuery[]; - }; -} - -export interface QueryTransactionFailureAction { - type: ActionTypes.QueryTransactionFailure; - payload: { - exploreId: ExploreId; - queryTransactions: QueryTransaction[]; - }; -} - -export interface QueryTransactionStartAction { - type: ActionTypes.QueryTransactionStart; - payload: { - exploreId: ExploreId; - resultType: ResultType; - rowIndex: number; - transaction: QueryTransaction; - }; -} - -export interface QueryTransactionSuccessAction { - type: ActionTypes.QueryTransactionSuccess; - payload: { - exploreId: ExploreId; - history: HistoryItem[]; - queryTransactions: QueryTransaction[]; - }; -} - -export interface RemoveQueryRowAction { - type: ActionTypes.RemoveQueryRow; - payload: { - exploreId: ExploreId; - index: number; - }; -} - -export interface RunQueriesEmptyAction { - type: ActionTypes.RunQueriesEmpty; - payload: { - exploreId: ExploreId; - }; -} - -export interface ScanStartAction { - type: ActionTypes.ScanStart; - payload: { - exploreId: ExploreId; - scanner: RangeScanner; - }; -} - -export interface ScanRangeAction { - type: ActionTypes.ScanRange; - payload: { - exploreId: ExploreId; - range: RawTimeRange; - }; -} - -export interface ScanStopAction { - type: ActionTypes.ScanStop; - payload: { - exploreId: ExploreId; - }; -} - -export interface SetQueriesAction { - type: ActionTypes.SetQueries; - payload: { - exploreId: ExploreId; - queries: DataQuery[]; - }; + payload: {}; } export interface SplitCloseAction { type: ActionTypes.SplitClose; + payload: {}; } export interface SplitOpenAction { @@ -245,80 +45,384 @@ export interface SplitOpenAction { }; } -export interface StateSaveAction { - type: ActionTypes.StateSave; -} - -export interface ToggleTableAction { - type: ActionTypes.ToggleTable; - payload: { - exploreId: ExploreId; - }; -} - -export interface ToggleGraphAction { - type: ActionTypes.ToggleGraph; - payload: { - exploreId: ExploreId; - }; -} - -export interface ToggleLogsAction { - type: ActionTypes.ToggleLogs; - payload: { - exploreId: ExploreId; - }; -} - -export interface UpdateDatasourceInstanceAction { - type: ActionTypes.UpdateDatasourceInstance; - payload: { - exploreId: ExploreId; - datasourceInstance: DataSourceApi; - }; -} - export interface ResetExploreAction { type: ActionTypes.ResetExplore; payload: {}; } -export interface QueriesImported { - type: ActionTypes.QueriesImported; - payload: { - exploreId: ExploreId; - queries: DataQuery[]; - }; +/** Lower order actions + * + */ +export interface AddQueryRowPayload { + exploreId: ExploreId; + index: number; + query: DataQuery; } -export type Action = - | AddQueryRowAction - | ChangeQueryAction - | ChangeSizeAction - | ChangeTimeAction - | ClearQueriesAction - | HighlightLogsExpressionAction - | InitializeExploreAction +export interface ChangeQueryPayload { + exploreId: ExploreId; + query: DataQuery; + index: number; + override: boolean; +} + +export interface ChangeSizePayload { + exploreId: ExploreId; + width: number; + height: number; +} + +export interface ChangeTimePayload { + exploreId: ExploreId; + range: TimeRange; +} + +export interface ClearQueriesPayload { + exploreId: ExploreId; +} + +export interface HighlightLogsExpressionPayload { + exploreId: ExploreId; + expressions: string[]; +} + +export interface InitializeExplorePayload { + exploreId: ExploreId; + containerWidth: number; + eventBridge: Emitter; + exploreDatasources: DataSourceSelectItem[]; + queries: DataQuery[]; + range: RawTimeRange; +} + +export interface LoadDatasourceFailurePayload { + exploreId: ExploreId; + error: string; +} + +export interface LoadDatasourceMissingPayload { + exploreId: ExploreId; +} + +export interface LoadDatasourcePendingPayload { + exploreId: ExploreId; + requestedDatasourceName: string; +} + +export interface LoadDatasourceSuccessPayload { + exploreId: ExploreId; + StartPage?: any; + datasourceInstance: any; + history: HistoryItem[]; + logsHighlighterExpressions?: any[]; + showingStartPage: boolean; + supportsGraph: boolean; + supportsLogs: boolean; + supportsTable: boolean; +} + +export interface ModifyQueriesPayload { + exploreId: ExploreId; + modification: QueryFixAction; + index: number; + modifier: (query: DataQuery, modification: QueryFixAction) => DataQuery; +} + +export interface QueryTransactionFailurePayload { + exploreId: ExploreId; + queryTransactions: QueryTransaction[]; +} + +export interface QueryTransactionStartPayload { + exploreId: ExploreId; + resultType: ResultType; + rowIndex: number; + transaction: QueryTransaction; +} + +export interface QueryTransactionSuccessPayload { + exploreId: ExploreId; + history: HistoryItem[]; + queryTransactions: QueryTransaction[]; +} + +export interface RemoveQueryRowPayload { + exploreId: ExploreId; + index: number; +} + +export interface RunQueriesEmptyPayload { + exploreId: ExploreId; +} + +export interface ScanStartPayload { + exploreId: ExploreId; + scanner: RangeScanner; +} + +export interface ScanRangePayload { + exploreId: ExploreId; + range: RawTimeRange; +} + +export interface ScanStopPayload { + exploreId: ExploreId; +} + +export interface SetQueriesPayload { + exploreId: ExploreId; + queries: DataQuery[]; +} + +export interface SplitOpenPayload { + itemState: ExploreItemState; +} + +export interface ToggleTablePayload { + exploreId: ExploreId; +} + +export interface ToggleGraphPayload { + exploreId: ExploreId; +} + +export interface ToggleLogsPayload { + exploreId: ExploreId; +} + +export interface UpdateDatasourceInstancePayload { + exploreId: ExploreId; + datasourceInstance: DataSourceApi; +} + +export interface QueriesImportedPayload { + exploreId: ExploreId; + queries: DataQuery[]; +} + +/** + * Adds a query row after the row with the given index. + */ +export const addQueryRowAction = actionCreatorFactory('explore/ADD_QUERY_ROW').create(); + +/** + * Loads a new datasource identified by the given name. + */ +export const changeDatasourceAction = noPayloadActionCreatorFactory('explore/CHANGE_DATASOURCE').create(); + +/** + * Query change handler for the query row with the given index. + * If `override` is reset the query modifications and run the queries. Use this to set queries via a link. + */ +export const changeQueryAction = actionCreatorFactory('explore/CHANGE_QUERY').create(); + +/** + * Keep track of the Explore container size, in particular the width. + * The width will be used to calculate graph intervals (number of datapoints). + */ +export const changeSizeAction = actionCreatorFactory('explore/CHANGE_SIZE').create(); + +/** + * Change the time range of Explore. Usually called from the Timepicker or a graph interaction. + */ +export const changeTimeAction = actionCreatorFactory('explore/CHANGE_TIME').create(); + +/** + * Clear all queries and results. + */ +export const clearQueriesAction = actionCreatorFactory('explore/CLEAR_QUERIES').create(); + +/** + * Highlight expressions in the log results + */ +export const highlightLogsExpressionAction = actionCreatorFactory( + 'explore/HIGHLIGHT_LOGS_EXPRESSION' +).create(); + +/** + * Initialize Explore state with state from the URL and the React component. + * Call this only on components for with the Explore state has not been initialized. + */ +export const initializeExploreAction = actionCreatorFactory( + 'explore/INITIALIZE_EXPLORE' +).create(); + +/** + * Initialize the wrapper split state + */ +export const initializeExploreSplitAction = noPayloadActionCreatorFactory('explore/INITIALIZE_EXPLORE_SPLIT').create(); + +/** + * Display an error that happened during the selection of a datasource + */ +export const loadDatasourceFailureAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_FAILURE' +).create(); + +/** + * Display an error when no datasources have been configured + */ +export const loadDatasourceMissingAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_MISSING' +).create(); + +/** + * Start the async process of loading a datasource to display a loading indicator + */ +export const loadDatasourcePendingAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_PENDING' +).create(); + +/** + * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to + * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, + * e.g., Prometheus -> Loki queries. + */ +export const loadDatasourceSuccessAction = actionCreatorFactory( + 'explore/LOAD_DATASOURCE_SUCCESS' +).create(); + +/** + * Action to modify a query given a datasource-specific modifier action. + * @param exploreId Explore area + * @param modification Action object with a type, e.g., ADD_FILTER + * @param index Optional query row index. If omitted, the modification is applied to all query rows. + * @param modifier Function that executes the modification, typically `datasourceInstance.modifyQueries`. + */ +export const modifyQueriesAction = actionCreatorFactory('explore/MODIFY_QUERIES').create(); + +/** + * Mark a query transaction as failed with an error extracted from the query response. + * The transaction will be marked as `done`. + */ +export const queryTransactionFailureAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_FAILURE' +).create(); + +/** + * Start a query transaction for the given result type. + * @param exploreId Explore area + * @param transaction Query options and `done` status. + * @param resultType Associate the transaction with a result viewer, e.g., Graph + * @param rowIndex Index is used to associate latency for this transaction with a query row + */ +export const queryTransactionStartAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_START' +).create(); + +/** + * Complete a query transaction, mark the transaction as `done` and store query state in URL. + * If the transaction was started by a scanner, it keeps on scanning for more results. + * Side-effect: the query is stored in localStorage. + * @param exploreId Explore area + * @param transactionId ID + * @param result Response from `datasourceInstance.query()` + * @param latency Duration between request and response + * @param queries Queries from all query rows + * @param datasourceId Origin datasource instance, used to discard results if current datasource is different + */ +export const queryTransactionSuccessAction = actionCreatorFactory( + 'explore/QUERY_TRANSACTION_SUCCESS' +).create(); + +/** + * Remove query row of the given index, as well as associated query results. + */ +export const removeQueryRowAction = actionCreatorFactory('explore/REMOVE_QUERY_ROW').create(); +export const runQueriesAction = noPayloadActionCreatorFactory('explore/RUN_QUERIES').create(); +export const runQueriesEmptyAction = actionCreatorFactory('explore/RUN_QUERIES_EMPTY').create(); + +/** + * Start a scan for more results using the given scanner. + * @param exploreId Explore area + * @param scanner Function that a) returns a new time range and b) triggers a query run for the new range + */ +export const scanStartAction = actionCreatorFactory('explore/SCAN_START').create(); +export const scanRangeAction = actionCreatorFactory('explore/SCAN_RANGE').create(); + +/** + * Stop any scanning for more results. + */ +export const scanStopAction = actionCreatorFactory('explore/SCAN_STOP').create(); + +/** + * Reset queries to the given queries. Any modifications will be discarded. + * Use this action for clicks on query examples. Triggers a query run. + */ +export const setQueriesAction = actionCreatorFactory('explore/SET_QUERIES').create(); + +/** + * Close the split view and save URL state. + */ +export const splitCloseAction = noPayloadActionCreatorFactory('explore/SPLIT_CLOSE').create(); + +/** + * Open the split view and copy the left state to be the right state. + * The right state is automatically initialized. + * The copy keeps all query modifications but wipes the query results. + */ +export const splitOpenAction = actionCreatorFactory('explore/SPLIT_OPEN').create(); +export const stateSaveAction = noPayloadActionCreatorFactory('explore/STATE_SAVE').create(); + +/** + * Expand/collapse the table result viewer. When collapsed, table queries won't be run. + */ +export const toggleTableAction = actionCreatorFactory('explore/TOGGLE_TABLE').create(); + +/** + * Expand/collapse the graph result viewer. When collapsed, graph queries won't be run. + */ +export const toggleGraphAction = actionCreatorFactory('explore/TOGGLE_GRAPH').create(); + +/** + * Expand/collapse the logs result viewer. When collapsed, log queries won't be run. + */ +export const toggleLogsAction = actionCreatorFactory('explore/TOGGLE_LOGS').create(); + +/** + * Updates datasource instance before datasouce loading has started + */ +export const updateDatasourceInstanceAction = actionCreatorFactory( + 'explore/UPDATE_DATASOURCE_INSTANCE' +).create(); + +/** + * Resets state for explore. + */ +export const resetExploreAction = noPayloadActionCreatorFactory('explore/RESET_EXPLORE').create(); +export const queriesImportedAction = actionCreatorFactory('explore/QueriesImported').create(); + +export type HigherOrderAction = | InitializeExploreSplitAction - | LoadDatasourceFailureAction - | LoadDatasourceMissingAction - | LoadDatasourcePendingAction - | LoadDatasourceSuccessAction - | ModifyQueriesAction - | QueryTransactionFailureAction - | QueryTransactionStartAction - | QueryTransactionSuccessAction - | RemoveQueryRowAction - | RunQueriesEmptyAction - | ScanRangeAction - | ScanStartAction - | ScanStopAction - | SetQueriesAction | SplitCloseAction | SplitOpenAction - | ToggleGraphAction - | ToggleLogsAction - | ToggleTableAction - | UpdateDatasourceInstanceAction | ResetExploreAction - | QueriesImported; + | ActionOf; + +export type Action = + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf + | ActionOf; diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index 63432e9c516..f32575edda5 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -32,40 +32,49 @@ import { QueryHint, QueryFixAction, } from '@grafana/ui/src/types'; +import { ExploreId, ExploreUrlState, RangeScanner, ResultType, QueryOptions } from 'app/types/explore'; import { - ExploreId, - ExploreUrlState, - RangeScanner, - ResultType, - QueryOptions, - QueryTransaction, -} from 'app/types/explore'; - -import { - Action as ThunkableAction, - ActionTypes, - AddQueryRowAction, - ChangeSizeAction, - HighlightLogsExpressionAction, - LoadDatasourceFailureAction, - LoadDatasourceMissingAction, - LoadDatasourcePendingAction, - LoadDatasourceSuccessAction, - QueryTransactionStartAction, - ScanStopAction, - UpdateDatasourceInstanceAction, - QueriesImported, - ModifyQueriesAction, + Action, + updateDatasourceInstanceAction, + changeQueryAction, + changeSizeAction, + ChangeSizePayload, + changeTimeAction, + scanStopAction, + clearQueriesAction, + initializeExploreAction, + loadDatasourceMissingAction, + loadDatasourceFailureAction, + loadDatasourcePendingAction, + queriesImportedAction, + LoadDatasourceSuccessPayload, + loadDatasourceSuccessAction, + modifyQueriesAction, + queryTransactionFailureAction, + queryTransactionStartAction, + queryTransactionSuccessAction, + scanRangeAction, + runQueriesEmptyAction, + scanStartAction, + setQueriesAction, + splitCloseAction, + splitOpenAction, + toggleGraphAction, + toggleLogsAction, + toggleTableAction, + addQueryRowAction, + AddQueryRowPayload, } from './actionTypes'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; -type ThunkResult = ThunkAction; +type ThunkResult = ThunkAction; -/** - * Adds a query row after the row with the given index. - */ -export function addQueryRow(exploreId: ExploreId, index: number): AddQueryRowAction { +// /** +// * Adds a query row after the row with the given index. +// */ +export function addQueryRow(exploreId: ExploreId, index: number): ActionOf { const query = generateEmptyQuery(index + 1); - return { type: ActionTypes.AddQueryRow, payload: { exploreId, index, query } }; + return addQueryRowAction({ exploreId, index, query }); } /** @@ -79,7 +88,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); - dispatch(updateDatasourceInstance(exploreId, newDataSourceInstance)); + dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); dispatch(loadDatasource(exploreId, newDataSourceInstance)); }; } @@ -100,7 +109,7 @@ export function changeQuery( query = { ...generateEmptyQuery(index) }; } - dispatch({ type: ActionTypes.ChangeQuery, payload: { exploreId, query, index, override } }); + dispatch(changeQueryAction({ exploreId, query, index, override })); if (override) { dispatch(runQueries(exploreId)); } @@ -114,8 +123,8 @@ export function changeQuery( export function changeSize( exploreId: ExploreId, { height, width }: { height: number; width: number } -): ChangeSizeAction { - return { type: ActionTypes.ChangeSize, payload: { exploreId, height, width } }; +): ActionOf { + return changeSizeAction({ exploreId, height, width }); } /** @@ -123,7 +132,7 @@ export function changeSize( */ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.ChangeTime, payload: { exploreId, range } }); + dispatch(changeTimeAction({ exploreId, range })); dispatch(runQueries(exploreId)); }; } @@ -133,19 +142,12 @@ export function changeTime(exploreId: ExploreId, range: TimeRange): ThunkResult< */ export function clearQueries(exploreId: ExploreId): ThunkResult { return dispatch => { - dispatch(scanStop(exploreId)); - dispatch({ type: ActionTypes.ClearQueries, payload: { exploreId } }); + dispatch(scanStopAction({ exploreId })); + dispatch(clearQueriesAction({ exploreId })); dispatch(stateSave()); }; } -/** - * Highlight expressions in the log results - */ -export function highlightLogsExpression(exploreId: ExploreId, expressions: string[]): HighlightLogsExpressionAction { - return { type: ActionTypes.HighlightLogsExpression, payload: { exploreId, expressions } }; -} - /** * Initialize Explore state with state from the URL and the React component. * Call this only on components for with the Explore state has not been initialized. @@ -167,18 +169,16 @@ export function initializeExplore( meta: ds.meta, })); - dispatch({ - type: ActionTypes.InitializeExplore, - payload: { + dispatch( + initializeExploreAction({ exploreId, containerWidth, - datasourceName, eventBridge, exploreDatasources, queries, range, - }, - }); + }) + ); if (exploreDatasources.length >= 1) { let instance; @@ -195,75 +195,20 @@ export function initializeExplore( instance = await getDatasourceSrv().get(); } - dispatch(updateDatasourceInstance(exploreId, instance)); + dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: instance })); dispatch(loadDatasource(exploreId, instance)); } else { - dispatch(loadDatasourceMissing(exploreId)); + dispatch(loadDatasourceMissingAction({ exploreId })); } }; } -/** - * Initialize the wrapper split state - */ -export function initializeExploreSplit() { - return async dispatch => { - dispatch({ type: ActionTypes.InitializeExploreSplit }); - }; -} - -/** - * Display an error that happened during the selection of a datasource - */ -export const loadDatasourceFailure = (exploreId: ExploreId, error: string): LoadDatasourceFailureAction => ({ - type: ActionTypes.LoadDatasourceFailure, - payload: { - exploreId, - error, - }, -}); - -/** - * Display an error when no datasources have been configured - */ -export const loadDatasourceMissing = (exploreId: ExploreId): LoadDatasourceMissingAction => ({ - type: ActionTypes.LoadDatasourceMissing, - payload: { exploreId }, -}); - -/** - * Start the async process of loading a datasource to display a loading indicator - */ -export const loadDatasourcePending = ( - exploreId: ExploreId, - requestedDatasourceName: string -): LoadDatasourcePendingAction => ({ - type: ActionTypes.LoadDatasourcePending, - payload: { - exploreId, - requestedDatasourceName, - }, -}); - -export const queriesImported = (exploreId: ExploreId, queries: DataQuery[]): QueriesImported => { - return { - type: ActionTypes.QueriesImported, - payload: { - exploreId, - queries, - }, - }; -}; - /** * Datasource loading was successfully completed. The instance is stored in the state as well in case we need to * run datasource-specific code. Existing queries are imported to the new datasource if an importer exists, * e.g., Prometheus -> Loki queries. */ -export const loadDatasourceSuccess = ( - exploreId: ExploreId, - instance: any, -): LoadDatasourceSuccessAction => { +export const loadDatasourceSuccess = (exploreId: ExploreId, instance: any): ActionOf => { // Capabilities const supportsGraph = instance.meta.metrics; const supportsLogs = instance.meta.logs; @@ -276,37 +221,18 @@ export const loadDatasourceSuccess = ( // Save last-used datasource store.set(LAST_USED_DATASOURCE_KEY, instance.name); - return { - type: ActionTypes.LoadDatasourceSuccess, - payload: { - exploreId, - StartPage, - datasourceInstance: instance, - history, - showingStartPage: Boolean(StartPage), - supportsGraph, - supportsLogs, - supportsTable, - }, - }; + return loadDatasourceSuccessAction({ + exploreId, + StartPage, + datasourceInstance: instance, + history, + showingStartPage: Boolean(StartPage), + supportsGraph, + supportsLogs, + supportsTable, + }); }; -/** - * Updates datasource instance before datasouce loading has started - */ -export function updateDatasourceInstance( - exploreId: ExploreId, - instance: DataSourceApi -): UpdateDatasourceInstanceAction { - return { - type: ActionTypes.UpdateDatasourceInstance, - payload: { - exploreId, - datasourceInstance: instance, - }, - }; -} - export function importQueries( exploreId: ExploreId, queries: DataQuery[], @@ -332,7 +258,7 @@ export function importQueries( ...generateEmptyQuery(i), })); - dispatch(queriesImported(exploreId, nextQueries)); + dispatch(queriesImportedAction({ exploreId, queries: nextQueries })); }; } @@ -344,7 +270,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T const datasourceName = instance.name; // Keep ID to track selection - dispatch(loadDatasourcePending(exploreId, datasourceName)); + dispatch(loadDatasourcePendingAction({ exploreId, requestedDatasourceName: datasourceName })); let datasourceError = null; try { @@ -355,7 +281,7 @@ export function loadDatasource(exploreId: ExploreId, instance: DataSourceApi): T } if (datasourceError) { - dispatch(loadDatasourceFailure(exploreId, datasourceError)); + dispatch(loadDatasourceFailureAction({ exploreId, error: datasourceError })); return; } @@ -392,11 +318,7 @@ export function modifyQueries( modifier: any ): ThunkResult { return dispatch => { - const modifyQueryAction: ModifyQueriesAction = { - type: ActionTypes.ModifyQueries, - payload: { exploreId, modification, index, modifier }, - }; - dispatch(modifyQueryAction); + dispatch(modifyQueriesAction({ exploreId, modification, index, modifier })); if (!modification.preventSubmit) { dispatch(runQueries(exploreId)); } @@ -461,29 +383,10 @@ export function queryTransactionFailure( return qt; }); - dispatch({ - type: ActionTypes.QueryTransactionFailure, - payload: { exploreId, queryTransactions: nextQueryTransactions }, - }); + dispatch(queryTransactionFailureAction({ exploreId, queryTransactions: nextQueryTransactions })); }; } -/** - * Start a query transaction for the given result type. - * @param exploreId Explore area - * @param transaction Query options and `done` status. - * @param resultType Associate the transaction with a result viewer, e.g., Graph - * @param rowIndex Index is used to associate latency for this transaction with a query row - */ -export function queryTransactionStart( - exploreId: ExploreId, - transaction: QueryTransaction, - resultType: ResultType, - rowIndex: number -): QueryTransactionStartAction { - return { type: ActionTypes.QueryTransactionStart, payload: { exploreId, resultType, rowIndex, transaction } }; -} - /** * Complete a query transaction, mark the transaction as `done` and store query state in URL. * If the transaction was started by a scanner, it keeps on scanning for more results. @@ -540,14 +443,13 @@ export function queryTransactionSuccess( // Side-effect: Saving history in localstorage const nextHistory = updateHistory(history, datasourceId, queries); - dispatch({ - type: ActionTypes.QueryTransactionSuccess, - payload: { + dispatch( + queryTransactionSuccessAction({ exploreId, history: nextHistory, queryTransactions: nextQueryTransactions, - }, - }); + }) + ); // Keep scanning for results if this was the last scanning transaction if (scanning) { @@ -555,26 +457,16 @@ export function queryTransactionSuccess( const other = nextQueryTransactions.find(qt => qt.scanning && !qt.done); if (!other) { const range = scanner(); - dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + dispatch(scanRangeAction({ exploreId, range })); } } else { // We can stop scanning if we have a result - dispatch(scanStop(exploreId)); + dispatch(scanStopAction({ exploreId })); } } }; } -/** - * Remove query row of the given index, as well as associated query results. - */ -export function removeQueryRow(exploreId: ExploreId, index: number): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.RemoveQueryRow, payload: { exploreId, index } }); - dispatch(runQueries(exploreId)); - }; -} - /** * Main action to run queries and dispatches sub-actions based on which result viewers are active */ @@ -592,7 +484,7 @@ export function runQueries(exploreId: ExploreId) { } = getState().explore[exploreId]; if (!hasNonEmptyQuery(modifiedQueries)) { - dispatch({ type: ActionTypes.RunQueriesEmpty, payload: { exploreId } }); + dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; } @@ -673,7 +565,7 @@ function runQueriesForType( queryIntervals, scanning ); - dispatch(queryTransactionStart(exploreId, transaction, resultType, rowIndex)); + dispatch(queryTransactionStartAction({ exploreId, resultType, rowIndex, transaction })); try { const now = Date.now(); const res = await datasourceInstance.query(transaction.options); @@ -697,21 +589,14 @@ function runQueriesForType( export function scanStart(exploreId: ExploreId, scanner: RangeScanner): ThunkResult { return dispatch => { // Register the scanner - dispatch({ type: ActionTypes.ScanStart, payload: { exploreId, scanner } }); + dispatch(scanStartAction({ exploreId, scanner })); // Scanning must trigger query run, and return the new range const range = scanner(); // Set the new range to be displayed - dispatch({ type: ActionTypes.ScanRange, payload: { exploreId, range } }); + dispatch(scanRangeAction({ exploreId, range })); }; } -/** - * Stop any scanning for more results. - */ -export function scanStop(exploreId: ExploreId): ScanStopAction { - return { type: ActionTypes.ScanStop, payload: { exploreId } }; -} - /** * Reset queries to the given queries. Any modifications will be discarded. * Use this action for clicks on query examples. Triggers a query run. @@ -720,13 +605,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk return dispatch => { // Inject react keys into query objects const queries = rawQueries.map(q => ({ ...q, ...generateEmptyQuery() })); - dispatch({ - type: ActionTypes.SetQueries, - payload: { - exploreId, - queries, - }, - }); + dispatch(setQueriesAction({ exploreId, queries })); dispatch(runQueries(exploreId)); }; } @@ -736,7 +615,7 @@ export function setQueries(exploreId: ExploreId, rawQueries: DataQuery[]): Thunk */ export function splitClose(): ThunkResult { return dispatch => { - dispatch({ type: ActionTypes.SplitClose }); + dispatch(splitCloseAction()); dispatch(stateSave()); }; } @@ -755,7 +634,7 @@ export function splitOpen(): ThunkResult { queryTransactions: [], initialQueries: leftState.modifiedQueries.slice(), }; - dispatch({ type: ActionTypes.SplitOpen, payload: { itemState } }); + dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); }; } @@ -791,7 +670,7 @@ export function stateSave() { */ export function toggleGraph(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleGraph, payload: { exploreId } }); + dispatch(toggleGraphAction({ exploreId })); if (getState().explore[exploreId].showingGraph) { dispatch(runQueries(exploreId)); } @@ -803,7 +682,7 @@ export function toggleGraph(exploreId: ExploreId): ThunkResult { */ export function toggleLogs(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleLogs, payload: { exploreId } }); + dispatch(toggleLogsAction({ exploreId })); if (getState().explore[exploreId].showingLogs) { dispatch(runQueries(exploreId)); } @@ -815,18 +694,9 @@ export function toggleLogs(exploreId: ExploreId): ThunkResult { */ export function toggleTable(exploreId: ExploreId): ThunkResult { return (dispatch, getState) => { - dispatch({ type: ActionTypes.ToggleTable, payload: { exploreId } }); + dispatch(toggleTableAction({ exploreId })); if (getState().explore[exploreId].showingTable) { dispatch(runQueries(exploreId)); } }; } - -/** - * Resets state for explore. - */ -export function resetExplore(): ThunkResult { - return dispatch => { - dispatch({ type: ActionTypes.ResetExplore, payload: {} }); - }; -} diff --git a/public/app/features/explore/state/reducers.test.ts b/public/app/features/explore/state/reducers.test.ts index 8227a947c5b..44079313c04 100644 --- a/public/app/features/explore/state/reducers.test.ts +++ b/public/app/features/explore/state/reducers.test.ts @@ -1,42 +1,47 @@ -import { Action, ActionTypes } from './actionTypes'; import { itemReducer, makeExploreItemState } from './reducers'; -import { ExploreId } from 'app/types/explore'; +import { ExploreId, ExploreItemState } from 'app/types/explore'; +import { reducerTester } from 'test/core/redux/reducerTester'; +import { scanStartAction, scanStopAction } from './actionTypes'; +import { Reducer } from 'redux'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; describe('Explore item reducer', () => { describe('scanning', () => { test('should start scanning', () => { - let state = makeExploreItemState(); - const action: Action = { - type: ActionTypes.ScanStart, - payload: { - exploreId: ExploreId.left, - scanner: jest.fn(), - }, + const scanner = jest.fn(); + const initalState = { + ...makeExploreItemState(), + scanning: false, + scanner: undefined, }; - state = itemReducer(state, action); - expect(state.scanning).toBeTruthy(); - expect(state.scanner).toBe(action.payload.scanner); + + reducerTester() + .givenReducer(itemReducer as Reducer>, initalState) + .whenActionIsDispatched(scanStartAction({ exploreId: ExploreId.left, scanner })) + .thenStateShouldEqual({ + ...makeExploreItemState(), + scanning: true, + scanner, + }); }); test('should stop scanning', () => { - let state = makeExploreItemState(); - const start: Action = { - type: ActionTypes.ScanStart, - payload: { - exploreId: ExploreId.left, - scanner: jest.fn(), - }, + const scanner = jest.fn(); + const initalState = { + ...makeExploreItemState(), + scanning: true, + scanner, + scanRange: {}, }; - state = itemReducer(state, start); - expect(state.scanning).toBeTruthy(); - const action: Action = { - type: ActionTypes.ScanStop, - payload: { - exploreId: ExploreId.left, - }, - }; - state = itemReducer(state, action); - expect(state.scanning).toBeFalsy(); - expect(state.scanner).toBeUndefined(); + + reducerTester() + .givenReducer(itemReducer as Reducer>, initalState) + .whenActionIsDispatched(scanStopAction({ exploreId: ExploreId.left })) + .thenStateShouldEqual({ + ...makeExploreItemState(), + scanning: false, + scanner: undefined, + scanRange: undefined, + }); }); }); }); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 14c8d87bbd2..fc9be0c28b8 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -7,7 +7,36 @@ import { import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from '@grafana/ui/src/types'; -import { Action, ActionTypes } from './actionTypes'; +import { HigherOrderAction, ActionTypes } from './actionTypes'; +import { reducerFactory } from 'app/core/redux'; +import { + addQueryRowAction, + changeQueryAction, + changeSizeAction, + changeTimeAction, + clearQueriesAction, + highlightLogsExpressionAction, + initializeExploreAction, + updateDatasourceInstanceAction, + loadDatasourceFailureAction, + loadDatasourceMissingAction, + loadDatasourcePendingAction, + loadDatasourceSuccessAction, + modifyQueriesAction, + queryTransactionFailureAction, + queryTransactionStartAction, + queryTransactionSuccessAction, + removeQueryRowAction, + runQueriesEmptyAction, + scanRangeAction, + scanStartAction, + scanStopAction, + setQueriesAction, + toggleGraphAction, + toggleLogsAction, + toggleTableAction, + queriesImportedAction, +} from './actionTypes'; export const DEFAULT_RANGE = { from: 'now-6h', @@ -58,9 +87,10 @@ export const initialExploreState: ExploreState = { /** * Reducer for an Explore area, to be used by the global Explore reducer. */ -export const itemReducer = (state, action: Action): ExploreItemState => { - switch (action.type) { - case ActionTypes.AddQueryRow: { +export const itemReducer = reducerFactory({} as ExploreItemState) + .addMapper({ + filter: addQueryRowAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, modifiedQueries, queryTransactions } = state; const { index, query } = action.payload; @@ -77,10 +107,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { - return { - ...qt, - rowIndex: qt.rowIndex + 1, - }; + return { ...qt, rowIndex: qt.rowIndex + 1 }; } return qt; }); @@ -92,9 +119,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.ChangeQuery: { + }, + }) + .addMapper({ + filter: changeQueryAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, queryTransactions } = state; let { modifiedQueries } = state; const { query, index, override } = action.payload; @@ -102,17 +131,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Fast path: only change modifiedQueries to not trigger an update modifiedQueries[index] = query; if (!override) { - return { - ...state, - modifiedQueries, - }; + return { ...state, modifiedQueries }; } // Override path: queries are completely reset - const nextQuery: DataQuery = { - ...query, - ...generateEmptyQuery(index), - }; + const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; const nextQueries = [...initialQueries]; nextQueries[index] = nextQuery; modifiedQueries = [...nextQueries]; @@ -126,9 +149,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.ChangeSize: { + }, + }) + .addMapper({ + filter: changeSizeAction, + mapper: (state, action): ExploreItemState => { const { range, datasourceInstance } = state; let interval = '1s'; if (datasourceInstance && datasourceInstance.interval) { @@ -137,16 +162,17 @@ export const itemReducer = (state, action: Action): ExploreItemState => { const containerWidth = action.payload.width; const queryIntervals = getIntervals(range, interval, containerWidth); return { ...state, containerWidth, queryIntervals }; - } - - case ActionTypes.ChangeTime: { - return { - ...state, - range: action.payload.range, - }; - } - - case ActionTypes.ClearQueries: { + }, + }) + .addMapper({ + filter: changeTimeAction, + mapper: (state, action): ExploreItemState => { + return { ...state, range: action.payload.range }; + }, + }) + .addMapper({ + filter: clearQueriesAction, + mapper: (state): ExploreItemState => { const queries = ensureQueries(); return { ...state, @@ -155,14 +181,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; - } - - case ActionTypes.HighlightLogsExpression: { + }, + }) + .addMapper({ + filter: highlightLogsExpressionAction, + mapper: (state, action): ExploreItemState => { const { expressions } = action.payload; return { ...state, logsHighlighterExpressions: expressions }; - } - - case ActionTypes.InitializeExplore: { + }, + }) + .addMapper({ + filter: initializeExploreAction, + mapper: (state, action): ExploreItemState => { const { containerWidth, eventBridge, exploreDatasources, queries, range } = action.payload; return { ...state, @@ -174,30 +204,37 @@ export const itemReducer = (state, action: Action): ExploreItemState => { initialized: true, modifiedQueries: queries.slice(), }; - } - - case ActionTypes.UpdateDatasourceInstance: { + }, + }) + .addMapper({ + filter: updateDatasourceInstanceAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { - ...state, - datasourceInstance, - datasourceName: datasourceInstance.name, - }; - } - - case ActionTypes.LoadDatasourceFailure: { + return { ...state, datasourceInstance }; + /*datasourceName: datasourceInstance.name removed after refactor, datasourceName does not exists on ExploreItemState */ + }, + }) + .addMapper({ + filter: loadDatasourceFailureAction, + mapper: (state, action): ExploreItemState => { return { ...state, datasourceError: action.payload.error, datasourceLoading: false }; - } - - case ActionTypes.LoadDatasourceMissing: { + }, + }) + .addMapper({ + filter: loadDatasourceMissingAction, + mapper: (state): ExploreItemState => { return { ...state, datasourceMissing: true, datasourceLoading: false }; - } - - case ActionTypes.LoadDatasourcePending: { + }, + }) + .addMapper({ + filter: loadDatasourcePendingAction, + mapper: (state, action): ExploreItemState => { return { ...state, datasourceLoading: true, requestedDatasourceName: action.payload.requestedDatasourceName }; - } - - case ActionTypes.LoadDatasourceSuccess: { + }, + }) + .addMapper({ + filter: loadDatasourceSuccessAction, + mapper: (state, action): ExploreItemState => { const { containerWidth, range } = state; const { StartPage, @@ -226,9 +263,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { logsHighlighterExpressions: undefined, queryTransactions: [], }; - } - - case ActionTypes.ModifyQueries: { + }, + }) + .addMapper({ + filter: modifyQueriesAction, + mapper: (state, action): ExploreItemState => { const { initialQueries, modifiedQueries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; @@ -246,12 +285,7 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueries = initialQueries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? - return i === index - ? { - ...modifier(modifiedQueries[i], modification), - ...generateEmptyQuery(i), - } - : query; + return i === index ? { ...modifier(modifiedQueries[i], modification), ...generateEmptyQuery(i) } : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -270,18 +304,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.QueryTransactionFailure: { + }, + }) + .addMapper({ + filter: queryTransactionFailureAction, + mapper: (state, action): ExploreItemState => { const { queryTransactions } = action.payload; - return { - ...state, - queryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.QueryTransactionStart: { + return { ...state, queryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: queryTransactionStartAction, + mapper: (state, action): ExploreItemState => { const { queryTransactions } = state; const { resultType, rowIndex, transaction } = action.payload; // Discarding existing transactions of same type @@ -292,14 +326,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => { // Append new transaction const nextQueryTransactions: QueryTransaction[] = [...remainingTransactions, transaction]; - return { - ...state, - queryTransactions: nextQueryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.QueryTransactionSuccess: { + return { ...state, queryTransactions: nextQueryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: queryTransactionSuccessAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance, queryIntervals } = state; const { history, queryTransactions } = action.payload; const results = calculateResultsFromQueryTransactions( @@ -308,16 +340,12 @@ export const itemReducer = (state, action: Action): ExploreItemState => { queryIntervals.intervalMs ); - return { - ...state, - ...results, - history, - queryTransactions, - showingStartPage: false, - }; - } - - case ActionTypes.RemoveQueryRow: { + return { ...state, ...results, history, queryTransactions, showingStartPage: false }; + }, + }) + .addMapper({ + filter: removeQueryRowAction, + mapper: (state, action): ExploreItemState => { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; let { modifiedQueries } = state; const { index } = action.payload; @@ -346,21 +374,29 @@ export const itemReducer = (state, action: Action): ExploreItemState => { modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; - } - - case ActionTypes.RunQueriesEmpty: { + }, + }) + .addMapper({ + filter: runQueriesEmptyAction, + mapper: (state): ExploreItemState => { return { ...state, queryTransactions: [] }; - } - - case ActionTypes.ScanRange: { + }, + }) + .addMapper({ + filter: scanRangeAction, + mapper: (state, action): ExploreItemState => { return { ...state, scanRange: action.payload.range }; - } - - case ActionTypes.ScanStart: { + }, + }) + .addMapper({ + filter: scanStartAction, + mapper: (state, action): ExploreItemState => { return { ...state, scanning: true, scanner: action.payload.scanner }; - } - - case ActionTypes.ScanStop: { + }, + }) + .addMapper({ + filter: scanStopAction, + mapper: (state): ExploreItemState => { const { queryTransactions } = state; const nextQueryTransactions = queryTransactions.filter(qt => qt.scanning && !qt.done); return { @@ -370,14 +406,18 @@ export const itemReducer = (state, action: Action): ExploreItemState => { scanRange: undefined, scanner: undefined, }; - } - - case ActionTypes.SetQueries: { + }, + }) + .addMapper({ + filter: setQueriesAction, + mapper: (state, action): ExploreItemState => { const { queries } = action.payload; return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; - } - - case ActionTypes.ToggleGraph: { + }, + }) + .addMapper({ + filter: toggleGraphAction, + mapper: (state): ExploreItemState => { const showingGraph = !state.showingGraph; let nextQueryTransactions = state.queryTransactions; if (!showingGraph) { @@ -385,9 +425,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Graph'); } return { ...state, queryTransactions: nextQueryTransactions, showingGraph }; - } - - case ActionTypes.ToggleLogs: { + }, + }) + .addMapper({ + filter: toggleLogsAction, + mapper: (state): ExploreItemState => { const showingLogs = !state.showingLogs; let nextQueryTransactions = state.queryTransactions; if (!showingLogs) { @@ -395,9 +437,11 @@ export const itemReducer = (state, action: Action): ExploreItemState => { nextQueryTransactions = state.queryTransactions.filter(qt => qt.resultType !== 'Logs'); } return { ...state, queryTransactions: nextQueryTransactions, showingLogs }; - } - - case ActionTypes.ToggleTable: { + }, + }) + .addMapper({ + filter: toggleTableAction, + mapper: (state): ExploreItemState => { const showingTable = !state.showingTable; if (showingTable) { return { ...state, showingTable, queryTransactions: state.queryTransactions }; @@ -412,25 +456,21 @@ export const itemReducer = (state, action: Action): ExploreItemState => { ); return { ...state, ...results, queryTransactions: nextQueryTransactions, showingTable }; - } - - case ActionTypes.QueriesImported: { - return { - ...state, - initialQueries: action.payload.queries, - modifiedQueries: action.payload.queries.slice(), - }; - } - } - - return state; -}; + }, + }) + .addMapper({ + filter: queriesImportedAction, + mapper: (state, action): ExploreItemState => { + return { ...state, initialQueries: action.payload.queries, modifiedQueries: action.payload.queries.slice() }; + }, + }) + .create(); /** * Global Explore reducer that handles multiple Explore areas (left and right). * Actions that have an `exploreId` get routed to the ExploreItemReducer. */ -export const exploreReducer = (state = initialExploreState, action: Action): ExploreState => { +export const exploreReducer = (state = initialExploreState, action: HigherOrderAction): ExploreState => { switch (action.type) { case ActionTypes.SplitClose: { return { ...state, split: false }; @@ -453,10 +493,7 @@ export const exploreReducer = (state = initialExploreState, action: Action): Exp const { exploreId } = action.payload as any; if (exploreId !== undefined) { const exploreItemState = state[exploreId]; - return { - ...state, - [exploreId]: itemReducer(exploreItemState, action), - }; + return { ...state, [exploreId]: itemReducer(exploreItemState, action) }; } } From d9578bc48505c890a75c9e6c7ef996fe0886531d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 08:17:18 +0100 Subject: [PATCH 20/76] Merge with master --- .../datasource/loki/components/LokiQueryEditor.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index 634a642c65e..e9912522f16 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -33,7 +33,7 @@ export class LokiQueryEditor extends PureComponent { query: { ...this.state.query, expr: query.expr, - } + }, }); }; @@ -61,12 +61,18 @@ export class LokiQueryEditor extends PureComponent { datasource={datasource} initialQuery={query} onQueryChange={this.onFieldChange} - onPressEnter={this.onRunQuery} + onExecuteQuery={this.onRunQuery} + history={[]} />
Format as
-
From f5084045f216c79702ad7124ae72a7144d014881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 09:32:39 +0100 Subject: [PATCH 21/76] Fix save provisioned dashboard modal --- public/app/features/dashboard/components/SaveModals/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/components/SaveModals/index.ts b/public/app/features/dashboard/components/SaveModals/index.ts index afab0796d28..6f55cc2ce06 100644 --- a/public/app/features/dashboard/components/SaveModals/index.ts +++ b/public/app/features/dashboard/components/SaveModals/index.ts @@ -1,2 +1,3 @@ export { SaveDashboardAsModalCtrl } from './SaveDashboardAsModalCtrl'; export { SaveDashboardModalCtrl } from './SaveDashboardModalCtrl'; +export { SaveProvisionedDashboardModalCtrl } from './SaveProvisionedDashboardModalCtrl'; From fdd5ac1895e4ee7af5c9a060ba2c8fc8b4ef830d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 09:55:23 +0100 Subject: [PATCH 22/76] devenv: switching back using loki master plus various fixes --- devenv/docker/blocks/loki/docker-compose.yaml | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/devenv/docker/blocks/loki/docker-compose.yaml b/devenv/docker/blocks/loki/docker-compose.yaml index c2fee15b0bb..0ac5d439354 100644 --- a/devenv/docker/blocks/loki/docker-compose.yaml +++ b/devenv/docker/blocks/loki/docker-compose.yaml @@ -1,24 +1,14 @@ -version: "3" - -networks: loki: - -services: - loki: - image: grafana/loki:master-3e6a75e + image: grafana/loki:master ports: - "3100:3100" command: -config.file=/etc/loki/local-config.yaml - networks: - - loki promtail: - image: grafana/promtail:master-3e6a75e + image: grafana/promtail:master volumes: - ./docker/blocks/loki/config.yaml:/etc/promtail/docker-config.yaml - /var/log:/var/log - ../data/log:/var/log/grafana command: -config.file=/etc/promtail/docker-config.yaml - networks: - - loki From c61e90543411fee4971f9f67918ef09de32065ac Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 10:22:45 +0100 Subject: [PATCH 23/76] fixing logging action --- .../ValueMappingsEditor/ValueMappingsEditor.story.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx index d6c8cec8c1e..85504f6cd09 100644 --- a/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx +++ b/packages/grafana-ui/src/components/ValueMappingsEditor/ValueMappingsEditor.story.tsx @@ -6,12 +6,5 @@ import { ValueMappingsEditor } from './ValueMappingsEditor'; const ValueMappingsEditorStories = storiesOf('UI/ValueMappingsEditor', module); ValueMappingsEditorStories.add('default', () => { - return ( - { - action('Mapping changed'); - }} - /> - ); + return ; }); From 6b98b05976fb837433370ff45d214b6889e1bc14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 11:07:32 +0100 Subject: [PATCH 24/76] Removed modifiedQueries from state --- public/app/features/explore/QueryEditor.tsx | 11 ++---- public/app/features/explore/QueryRows.tsx | 9 +++-- public/app/features/explore/state/actions.ts | 18 ++++----- public/app/features/explore/state/reducers.ts | 39 ++++--------------- public/app/types/explore.ts | 8 +--- 5 files changed, 26 insertions(+), 59 deletions(-) diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 083cd8a2e17..1d329f1c56e 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -14,7 +14,7 @@ interface QueryEditorProps { datasource: any; error?: string | JSX.Element; onExecuteQuery?: () => void; - onQueryChange?: (value: DataQuery, override?: boolean) => void; + onQueryChange?: (value: DataQuery) => void; initialQuery: DataQuery; exploreEvents: Emitter; range: RawTimeRange; @@ -40,20 +40,17 @@ export default class QueryEditor extends PureComponent { datasource, target, refresh: () => { - this.props.onQueryChange(target, false); + this.props.onQueryChange(target); this.props.onExecuteQuery(); }, events: exploreEvents, - panel: { - datasource, - targets: [target], - }, + panel: { datasource, targets: [target] }, dashboard: {}, }, }; this.component = loader.load(this.element, scopeProps, template); - this.props.onQueryChange(target, false); + this.props.onQueryChange(target); } componentWillUnmount() { diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index f8bb6e5ce6b..d65c1283bd6 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -21,10 +21,11 @@ export default class QueryRows extends PureComponent { const { className = '', exploreEvents, exploreId, initialQueries } = this.props; return (
- {initialQueries.map((query, index) => ( - // TODO instead of relying on initialQueries, move to react key list in redux - - ))} + {initialQueries.map((query, index) => { + // using query.key will introduce infinite loop because QueryEditor#53 + const key = query.datasource ? `${query.datasource}-${index}` : query.key; + return ; + })}
); } diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index f32575edda5..8530e7678ad 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -84,9 +84,9 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun return async (dispatch, getState) => { const newDataSourceInstance = await getDatasourceSrv().get(datasource); const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; - const modifiedQueries = getState().explore[exploreId].modifiedQueries; + const queries = getState().explore[exploreId].initialQueries; - await dispatch(importQueries(exploreId, modifiedQueries, currentDataSourceInstance, newDataSourceInstance)); + await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); dispatch(updateDatasourceInstanceAction({ exploreId, datasourceInstance: newDataSourceInstance })); dispatch(loadDatasource(exploreId, newDataSourceInstance)); @@ -254,7 +254,7 @@ export function importQueries( } const nextQueries = importedQueries.map((q, i) => ({ - ...importedQueries[i], + ...q, ...generateEmptyQuery(i), })); @@ -474,7 +474,7 @@ export function runQueries(exploreId: ExploreId) { return (dispatch, getState) => { const { datasourceInstance, - modifiedQueries, + initialQueries, showingLogs, showingGraph, showingTable, @@ -483,7 +483,7 @@ export function runQueries(exploreId: ExploreId) { supportsTable, } = getState().explore[exploreId]; - if (!hasNonEmptyQuery(modifiedQueries)) { + if (!hasNonEmptyQuery(initialQueries)) { dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; @@ -547,7 +547,7 @@ function runQueriesForType( const { datasourceInstance, eventBridge, - modifiedQueries: queries, + initialQueries: queries, queryIntervals, range, scanning, @@ -632,7 +632,7 @@ export function splitOpen(): ThunkResult { const itemState = { ...leftState, queryTransactions: [], - initialQueries: leftState.modifiedQueries.slice(), + initialQueries: leftState.initialQueries.slice(), }; dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); @@ -649,14 +649,14 @@ export function stateSave() { const urlStates: { [index: string]: string } = {}; const leftUrlState: ExploreUrlState = { datasource: left.datasourceInstance.name, - queries: left.modifiedQueries.map(clearQueryKeys), + queries: left.initialQueries.map(clearQueryKeys), range: left.range, }; urlStates.left = serializeStateToUrlParam(leftUrlState, true); if (split) { const rightUrlState: ExploreUrlState = { datasource: right.datasourceInstance.name, - queries: right.modifiedQueries.map(clearQueryKeys), + queries: right.initialQueries.map(clearQueryKeys), range: right.range, }; urlStates.right = serializeStateToUrlParam(rightUrlState, true); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index fc9be0c28b8..9343cf0ec57 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -61,7 +61,6 @@ export const makeExploreItemState = (): ExploreItemState => ({ history: [], initialQueries: [], initialized: false, - modifiedQueries: [], queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, range: DEFAULT_RANGE, @@ -91,16 +90,9 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: addQueryRowAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, modifiedQueries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; const { index, query } = action.payload; - // Add new query row after given index, keep modifications of existing rows - const nextModifiedQueries = [ - ...modifiedQueries.slice(0, index + 1), - { ...query }, - ...initialQueries.slice(index + 1), - ]; - // Add to initialQueries, which will cause a new row to be rendered const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; @@ -116,7 +108,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...state, initialQueries: nextQueries, logsHighlighterExpressions: undefined, - modifiedQueries: nextModifiedQueries, queryTransactions: nextQueryTransactions, }; }, @@ -125,20 +116,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: changeQueryAction, mapper: (state, action): ExploreItemState => { const { initialQueries, queryTransactions } = state; - let { modifiedQueries } = state; - const { query, index, override } = action.payload; - - // Fast path: only change modifiedQueries to not trigger an update - modifiedQueries[index] = query; - if (!override) { - return { ...state, modifiedQueries }; - } + const { query, index } = action.payload; // Override path: queries are completely reset const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; const nextQueries = [...initialQueries]; nextQueries[index] = nextQuery; - modifiedQueries = [...nextQueries]; // Discard ongoing transaction related to row query const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); @@ -146,7 +129,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -177,7 +159,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: queries.slice(), - modifiedQueries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), }; @@ -202,7 +183,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta range, initialQueries: queries, initialized: true, - modifiedQueries: queries.slice(), }; }, }) @@ -268,14 +248,14 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: modifyQueriesAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, modifiedQueries, queryTransactions } = state; + const { initialQueries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries nextQueries = initialQueries.map((query, i) => ({ - ...modifier(modifiedQueries[i], modification), + ...modifier({ ...query }, modification), ...generateEmptyQuery(i), })); // Discard all ongoing transactions @@ -285,7 +265,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta nextQueries = initialQueries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? - return i === index ? { ...modifier(modifiedQueries[i], modification), ...generateEmptyQuery(i) } : query; + return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query; }); nextQueryTransactions = queryTransactions // Consume the hint corresponding to the action @@ -301,7 +281,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -347,11 +326,8 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; - let { modifiedQueries } = state; const { index } = action.payload; - modifiedQueries = [...modifiedQueries.slice(0, index), ...modifiedQueries.slice(index + 1)]; - if (initialQueries.length <= 1) { return state; } @@ -371,7 +347,6 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...results, initialQueries: nextQueries, logsHighlighterExpressions: undefined, - modifiedQueries: nextQueries.slice(), queryTransactions: nextQueryTransactions, }; }, @@ -412,7 +387,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: setQueriesAction, mapper: (state, action): ExploreItemState => { const { queries } = action.payload; - return { ...state, initialQueries: queries.slice(), modifiedQueries: queries.slice() }; + return { ...state, initialQueries: queries.slice() }; }, }) .addMapper({ @@ -461,7 +436,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: queriesImportedAction, mapper: (state, action): ExploreItemState => { - return { ...state, initialQueries: action.payload.queries, modifiedQueries: action.payload.queries.slice() }; + return { ...state, initialQueries: action.payload.queries }; }, }) .create(); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 34b7ff08c99..92145dc2324 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -145,7 +145,7 @@ export interface ExploreItemState { history: HistoryItem[]; /** * Initial queries for this Explore, e.g., set via URL. Each query will be - * converted to a query row. Query edits should be tracked in `modifiedQueries` though. + * converted to a query row. */ initialQueries: DataQuery[]; /** @@ -162,12 +162,6 @@ export interface ExploreItemState { * Log query result to be displayed in the logs result viewer. */ logsResult?: LogsModel; - /** - * Copy of `initialQueries` that tracks user edits. - * Don't connect this property to a react component as it is updated on every query change. - * Used when running queries. Needs to be reset to `initialQueries` when those are reset as well. - */ - modifiedQueries: DataQuery[]; /** * Query intervals for graph queries to determine how many datapoints to return. * Needs to be updated when `datasourceInstance` or `containerWidth` is changed. From eb8dfefb231559acce4291a9d288d8d44fc02b50 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 11:16:11 +0100 Subject: [PATCH 25/76] changelog: add notes about closing #14231 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7164f5d99a9..1a19957b3ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Minor * **Pushover**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) +* **Cloudwatch**: Add AWS/Neptune metrics [#14231](https://github.com/grafana/grafana/issues/14231), thx [@tcpatterson](https://github.com/tcpatterson) # 6.0.0-beta1 (2019-01-30) From 5e2b9e40a2f6a858bef3ba9ccabc7fff6d96c47d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 11:25:07 +0100 Subject: [PATCH 26/76] Added more typings --- packages/grafana-ui/src/types/plugin.ts | 10 ++++++---- public/app/features/explore/Explore.tsx | 6 +++--- .../datasource/loki/components/LokiStartPage.tsx | 7 ++----- .../datasource/prometheus/components/PromStart.tsx | 7 ++----- public/app/types/explore.ts | 13 +++++++++++-- 5 files changed, 24 insertions(+), 19 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index e951e91a223..e674c9fbc32 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -65,15 +65,19 @@ export interface ExploreQueryFieldProps void; } +export interface ExploreStartPageProps { + onClickExample: (query: DataQuery) => void; +} + export interface PluginExports { Datasource?: DataSourceApi; QueryCtrl?: any; - QueryEditor?: ComponentClass>; + QueryEditor?: ComponentClass>; ConfigCtrl?: any; AnnotationsQueryCtrl?: any; VariableQueryEditor?: any; ExploreQueryField?: ComponentClass>; - ExploreStartPage?: any; + ExploreStartPage?: ComponentClass; // Panel plugin PanelCtrl?: any; @@ -131,5 +135,3 @@ export interface PluginMetaInfo { updated: string; version: string; } - - diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 31ffdf4ab24..36c1f7f5ad7 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -1,5 +1,5 @@ // Libraries -import React from 'react'; +import React, { ComponentClass } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import _ from 'lodash'; @@ -21,7 +21,7 @@ import TimePicker, { parseTime } from './TimePicker'; import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types -import { RawTimeRange, TimeRange, DataQuery } from '@grafana/ui'; +import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE } from 'app/core/utils/explore'; @@ -30,7 +30,7 @@ import { ExploreToolbar } from './ExploreToolbar'; import { scanStopAction } from './state/actionTypes'; interface ExploreProps { - StartPage?: any; + StartPage?: ComponentClass; changeSize: typeof changeSize; changeTime: typeof changeTime; datasourceError: string; diff --git a/public/app/plugins/datasource/loki/components/LokiStartPage.tsx b/public/app/plugins/datasource/loki/components/LokiStartPage.tsx index da20661fe1b..62063a790ec 100644 --- a/public/app/plugins/datasource/loki/components/LokiStartPage.tsx +++ b/public/app/plugins/datasource/loki/components/LokiStartPage.tsx @@ -1,11 +1,8 @@ import React, { PureComponent } from 'react'; import LokiCheatSheet from './LokiCheatSheet'; +import { ExploreStartPageProps } from '@grafana/ui'; -interface Props { - onClickExample: () => void; -} - -export default class LokiStartPage extends PureComponent { +export default class LokiStartPage extends PureComponent { render() { return (
diff --git a/public/app/plugins/datasource/prometheus/components/PromStart.tsx b/public/app/plugins/datasource/prometheus/components/PromStart.tsx index 9acfc534853..de545e826e3 100644 --- a/public/app/plugins/datasource/prometheus/components/PromStart.tsx +++ b/public/app/plugins/datasource/prometheus/components/PromStart.tsx @@ -1,11 +1,8 @@ import React, { PureComponent } from 'react'; import PromCheatSheet from './PromCheatSheet'; +import { ExploreStartPageProps } from '@grafana/ui'; -interface Props { - onClickExample: () => void; -} - -export default class PromStart extends PureComponent { +export default class PromStart extends PureComponent { render() { return (
diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 92145dc2324..4e099480cf0 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -1,5 +1,14 @@ +import { ComponentClass } from 'react'; import { Value } from 'slate'; -import { RawTimeRange, TimeRange, DataQuery, DataSourceSelectItem, DataSourceApi, QueryHint } from '@grafana/ui'; +import { + RawTimeRange, + TimeRange, + DataQuery, + DataSourceSelectItem, + DataSourceApi, + QueryHint, + ExploreStartPageProps, +} from '@grafana/ui'; import { Emitter } from 'app/core/core'; import { LogsModel } from 'app/core/logs_model'; @@ -102,7 +111,7 @@ export interface ExploreItemState { /** * React component to be shown when no queries have been run yet, e.g., for a query language cheat sheet. */ - StartPage?: any; + StartPage?: ComponentClass; /** * Width used for calculating the graph interval (can't have more datapoints than pixels) */ From efa48390b71d6d6397bc518cdda9ffb270ea8544 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 12:09:06 +0100 Subject: [PATCH 27/76] Reverted redux-logger --- public/app/store/configureStore.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 570a387cd74..dc9a478adf3 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; -import { createLogger } from 'redux-logger'; +// import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; @@ -39,7 +39,7 @@ export function configureStore() { if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware - setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())))); + setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } else { setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } From d433ca7d40d0dfc4a154163334efbe6efeea7cc6 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 13:10:32 +0100 Subject: [PATCH 28/76] fix util for splitting host and port Now you can provide both a default host and a default port --- pkg/services/sqlstore/sqlstore.go | 5 +- pkg/tsdb/mssql/mssql.go | 5 +- pkg/util/ip.go | 25 --------- pkg/util/ip_address.go | 47 +++++++++++++---- pkg/util/ip_address_test.go | 84 ++++++++++++++++++++++++++++++- pkg/util/ip_test.go | 43 ---------------- 6 files changed, 121 insertions(+), 88 deletions(-) delete mode 100644 pkg/util/ip.go delete mode 100644 pkg/util/ip_test.go diff --git a/pkg/services/sqlstore/sqlstore.go b/pkg/services/sqlstore/sqlstore.go index fb0f0938573..6debaca89a1 100644 --- a/pkg/services/sqlstore/sqlstore.go +++ b/pkg/services/sqlstore/sqlstore.go @@ -242,10 +242,7 @@ func (ss *SqlStore) buildConnectionString() (string, error) { cnnstr += ss.buildExtraConnectionString('&') case migrator.POSTGRES: - host, port, err := util.SplitIPPort(ss.dbCfg.Host, "5432") - if err != nil { - return "", err - } + host, port := util.SplitHostPortDefault(ss.dbCfg.Host, "127.0.0.1", "5432") if ss.dbCfg.Pwd == "" { ss.dbCfg.Pwd = "''" } diff --git a/pkg/tsdb/mssql/mssql.go b/pkg/tsdb/mssql/mssql.go index bd4510f6cf3..12f2b6c03c9 100644 --- a/pkg/tsdb/mssql/mssql.go +++ b/pkg/tsdb/mssql/mssql.go @@ -49,10 +49,7 @@ func generateConnectionString(datasource *models.DataSource) (string, error) { } } - server, port, err := util.SplitIPPort(datasource.Url, "1433") - if err != nil { - return "", err - } + server, port := util.SplitHostPortDefault(datasource.Url, "localhost", "1433") encrypt := datasource.JsonData.Get("encrypt").MustString("false") connStr := fmt.Sprintf("server=%s;port=%s;database=%s;user id=%s;password=%s;", diff --git a/pkg/util/ip.go b/pkg/util/ip.go deleted file mode 100644 index d3809318191..00000000000 --- a/pkg/util/ip.go +++ /dev/null @@ -1,25 +0,0 @@ -package util - -import ( - "net" -) - -// SplitIPPort splits the ip string and port. -func SplitIPPort(ipStr string, portDefault string) (ip string, port string, err error) { - ipAddr := net.ParseIP(ipStr) - - if ipAddr == nil { - // Port was included - ip, port, err = net.SplitHostPort(ipStr) - - if err != nil { - return "", "", err - } - } else { - // No port was included - ip = ipAddr.String() - port = portDefault - } - - return ip, port, nil -} diff --git a/pkg/util/ip_address.go b/pkg/util/ip_address.go index d8d95ef3acd..b5ffb361e0b 100644 --- a/pkg/util/ip_address.go +++ b/pkg/util/ip_address.go @@ -7,23 +7,48 @@ import ( // ParseIPAddress parses an IP address and removes port and/or IPV6 format func ParseIPAddress(input string) string { - s := input - lastIndex := strings.LastIndex(input, ":") + host, _ := SplitHostPort(input) - if lastIndex != -1 { - if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" { - s = input[:lastIndex] - } + ip := net.ParseIP(host) + + if ip == nil { + return host } - s = strings.Replace(s, "[", "", -1) - s = strings.Replace(s, "]", "", -1) - - ip := net.ParseIP(s) - if ip.IsLoopback() { return "127.0.0.1" } return ip.String() } + +// SplitHostPortDefault splits ip address/hostname string by host and port. Defaults used if no match found +func SplitHostPortDefault(input, defaultHost, defaultPort string) (host string, port string) { + port = defaultPort + s := input + lastIndex := strings.LastIndex(input, ":") + + if lastIndex != -1 { + if lastIndex > 0 && input[lastIndex-1:lastIndex] != ":" { + s = input[:lastIndex] + port = input[lastIndex+1:] + } else if lastIndex == 0 { + s = defaultHost + port = input[lastIndex+1:] + } + } else { + port = defaultPort + } + + s = strings.Replace(s, "[", "", -1) + s = strings.Replace(s, "]", "", -1) + port = strings.Replace(port, "[", "", -1) + port = strings.Replace(port, "]", "", -1) + + return s, port +} + +// SplitHostPort splits ip address/hostname string by host and port +func SplitHostPort(input string) (host string, port string) { + return SplitHostPortDefault(input, "", "") +} diff --git a/pkg/util/ip_address_test.go b/pkg/util/ip_address_test.go index fd3e3ea8587..b926de1a36b 100644 --- a/pkg/util/ip_address_test.go +++ b/pkg/util/ip_address_test.go @@ -9,8 +9,90 @@ import ( func TestParseIPAddress(t *testing.T) { Convey("Test parse ip address", t, func() { So(ParseIPAddress("192.168.0.140:456"), ShouldEqual, "192.168.0.140") + So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140") So(ParseIPAddress("[::1:456]"), ShouldEqual, "127.0.0.1") So(ParseIPAddress("[::1]"), ShouldEqual, "127.0.0.1") - So(ParseIPAddress("192.168.0.140"), ShouldEqual, "192.168.0.140") + So(ParseIPAddress("::1"), ShouldEqual, "127.0.0.1") + So(ParseIPAddress("::1:123"), ShouldEqual, "127.0.0.1") + }) +} + +func TestSplitHostPortDefault(t *testing.T) { + Convey("Test split ip address to host and port", t, func() { + host, port := SplitHostPortDefault("192.168.0.140:456", "", "") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("192.168.0.140", "", "123") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("[::1:456]", "", "") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("[::1]", "", "123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("::1:123", "", "") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("::1", "", "123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault(":456", "1.2.3.4", "") + So(host, ShouldEqual, "1.2.3.4") + So(port, ShouldEqual, "456") + + host, port = SplitHostPortDefault("xyz.rds.amazonaws.com", "", "123") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") + + host, port = SplitHostPortDefault("xyz.rds.amazonaws.com:123", "", "") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") + }) +} + +func TestSplitHostPort(t *testing.T) { + Convey("Test split ip address to host and port", t, func() { + host, port := SplitHostPort("192.168.0.140:456") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("192.168.0.140") + So(host, ShouldEqual, "192.168.0.140") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("[::1:456]") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("[::1]") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("::1:123") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "123") + + host, port = SplitHostPort("::1") + So(host, ShouldEqual, "::1") + So(port, ShouldEqual, "") + + host, port = SplitHostPort(":456") + So(host, ShouldEqual, "") + So(port, ShouldEqual, "456") + + host, port = SplitHostPort("xyz.rds.amazonaws.com") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "") + + host, port = SplitHostPort("xyz.rds.amazonaws.com:123") + So(host, ShouldEqual, "xyz.rds.amazonaws.com") + So(port, ShouldEqual, "123") }) } diff --git a/pkg/util/ip_test.go b/pkg/util/ip_test.go deleted file mode 100644 index 3a62a080e26..00000000000 --- a/pkg/util/ip_test.go +++ /dev/null @@ -1,43 +0,0 @@ -package util - -import ( - "testing" - - . "github.com/smartystreets/goconvey/convey" -) - -func TestSplitIPPort(t *testing.T) { - - Convey("When parsing an IPv4 without explicit port", t, func() { - ip, port, err := SplitIPPort("1.2.3.4", "5678") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "1.2.3.4") - So(port, ShouldEqual, "5678") - }) - - Convey("When parsing an IPv6 without explicit port", t, func() { - ip, port, err := SplitIPPort("::1", "5678") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "::1") - So(port, ShouldEqual, "5678") - }) - - Convey("When parsing an IPv4 with explicit port", t, func() { - ip, port, err := SplitIPPort("1.2.3.4:56", "78") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "1.2.3.4") - So(port, ShouldEqual, "56") - }) - - Convey("When parsing an IPv6 with explicit port", t, func() { - ip, port, err := SplitIPPort("[::1]:56", "78") - - So(err, ShouldEqual, nil) - So(ip, ShouldEqual, "::1") - So(port, ShouldEqual, "56") - }) - -} From 96aef3bab878644a16091d4522302361ebea99f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 13:41:29 +0100 Subject: [PATCH 29/76] Replaced intialQueris with queryKeys --- public/app/core/utils/explore.ts | 11 ++++++++- public/app/features/explore/Explore.tsx | 10 ++++---- public/app/features/explore/QueryRows.tsx | 9 +++---- public/app/features/explore/state/reducers.ts | 24 +++++++++++++++---- public/app/types/explore.ts | 5 ++++ 5 files changed, 43 insertions(+), 16 deletions(-) diff --git a/public/app/core/utils/explore.ts b/public/app/core/utils/explore.ts index 7a9f54a0cae..efa54b7bc23 100644 --- a/public/app/core/utils/explore.ts +++ b/public/app/core/utils/explore.ts @@ -11,7 +11,7 @@ import { colors } from '@grafana/ui'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; // Types -import { RawTimeRange, IntervalValues, DataQuery } from '@grafana/ui/src/types'; +import { RawTimeRange, IntervalValues, DataQuery, DataSourceApi } from '@grafana/ui/src/types'; import TimeSeries from 'app/core/time_series2'; import { ExploreUrlState, @@ -304,3 +304,12 @@ export function clearHistory(datasourceId: string) { const historyKey = `grafana.explore.history.${datasourceId}`; store.delete(historyKey); } + +export const getQueryKeys = (queries: DataQuery[], datasourceInstance: DataSourceApi): string[] => { + const queryKeys = queries.reduce((newQueryKeys, query, index) => { + const primaryKey = datasourceInstance && datasourceInstance.name ? datasourceInstance.name : query.key; + return newQueryKeys.concat(`${primaryKey}-${index}`); + }, []); + + return queryKeys; +}; diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 36c1f7f5ad7..2012a52c338 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -38,7 +38,6 @@ interface ExploreProps { datasourceLoading: boolean | null; datasourceMissing: boolean; exploreId: ExploreId; - initialQueries: DataQuery[]; initializeExplore: typeof initializeExplore; initialized: boolean; modifyQueries: typeof modifyQueries; @@ -55,6 +54,7 @@ interface ExploreProps { supportsLogs: boolean | null; supportsTable: boolean | null; urlState: ExploreUrlState; + queryKeys: string[]; } /** @@ -175,12 +175,12 @@ export class Explore extends React.PureComponent { datasourceLoading, datasourceMissing, exploreId, - initialQueries, showingStartPage, split, supportsGraph, supportsLogs, supportsTable, + queryKeys, } = this.props; const exploreClass = split ? 'explore explore-split' : 'explore'; @@ -201,7 +201,7 @@ export class Explore extends React.PureComponent { {datasourceInstance && !datasourceError && (
- + {({ width }) => (
@@ -243,13 +243,13 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceInstance, datasourceLoading, datasourceMissing, - initialQueries, initialized, range, showingStartPage, supportsGraph, supportsLogs, supportsTable, + queryKeys, } = item; return { StartPage, @@ -257,7 +257,6 @@ function mapStateToProps(state: StoreState, { exploreId }) { datasourceInstance, datasourceLoading, datasourceMissing, - initialQueries, initialized, range, showingStartPage, @@ -265,6 +264,7 @@ function mapStateToProps(state: StoreState, { exploreId }) { supportsGraph, supportsLogs, supportsTable, + queryKeys, }; } diff --git a/public/app/features/explore/QueryRows.tsx b/public/app/features/explore/QueryRows.tsx index d65c1283bd6..4b5a16ef781 100644 --- a/public/app/features/explore/QueryRows.tsx +++ b/public/app/features/explore/QueryRows.tsx @@ -6,24 +6,21 @@ import QueryRow from './QueryRow'; // Types import { Emitter } from 'app/core/utils/emitter'; -import { DataQuery } from '@grafana/ui/src/types'; import { ExploreId } from 'app/types/explore'; interface QueryRowsProps { className?: string; exploreEvents: Emitter; exploreId: ExploreId; - initialQueries: DataQuery[]; + queryKeys: string[]; } export default class QueryRows extends PureComponent { render() { - const { className = '', exploreEvents, exploreId, initialQueries } = this.props; + const { className = '', exploreEvents, exploreId, queryKeys } = this.props; return (
- {initialQueries.map((query, index) => { - // using query.key will introduce infinite loop because QueryEditor#53 - const key = query.datasource ? `${query.datasource}-${index}` : query.key; + {queryKeys.map((key, index) => { return ; })}
diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 9343cf0ec57..f7eca489b6e 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -3,6 +3,7 @@ import { generateEmptyQuery, getIntervals, ensureQueries, + getQueryKeys, } from 'app/core/utils/explore'; import { ExploreItemState, ExploreState, QueryTransaction } from 'app/types/explore'; import { DataQuery } from '@grafana/ui/src/types'; @@ -72,6 +73,7 @@ export const makeExploreItemState = (): ExploreItemState => ({ supportsGraph: null, supportsLogs: null, supportsTable: null, + queryKeys: [], }); /** @@ -109,6 +111,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -130,6 +133,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta ...state, initialQueries: nextQueries, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -161,6 +165,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), + queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, }) @@ -183,6 +188,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta range, initialQueries: queries, initialized: true, + queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, }) @@ -190,8 +196,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: updateDatasourceInstanceAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { ...state, datasourceInstance }; - /*datasourceName: datasourceInstance.name removed after refactor, datasourceName does not exists on ExploreItemState */ + return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.initialQueries, datasourceInstance) }; }, }) .addMapper({ @@ -281,6 +286,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, initialQueries: nextQueries, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), queryTransactions: nextQueryTransactions, }; }, @@ -348,6 +354,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, + queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; }, }) @@ -387,7 +394,11 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: setQueriesAction, mapper: (state, action): ExploreItemState => { const { queries } = action.payload; - return { ...state, initialQueries: queries.slice() }; + return { + ...state, + initialQueries: queries.slice(), + queryKeys: getQueryKeys(queries, state.datasourceInstance), + }; }, }) .addMapper({ @@ -436,7 +447,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: queriesImportedAction, mapper: (state, action): ExploreItemState => { - return { ...state, initialQueries: action.payload.queries }; + const { queries } = action.payload; + return { + ...state, + initialQueries: queries, + queryKeys: getQueryKeys(queries, state.datasourceInstance), + }; }, }) .create(); diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 4e099480cf0..8faf0d2ed09 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -232,6 +232,11 @@ export interface ExploreItemState { * Table model that combines all query table results into a single table. */ tableResult?: TableModel; + + /** + * React keys for rendering of QueryRows + */ + queryKeys: string[]; } export interface ExploreUrlState { From 34dd1a22ab51e145211bef9a8414e55baee164c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 14:16:15 +0100 Subject: [PATCH 30/76] Fixed bug with removing a QueryRow thats not part of nextQueries --- public/app/features/explore/state/reducers.ts | 7 ++++--- public/app/store/configureStore.ts | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index f7eca489b6e..86c263e39e9 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -331,7 +331,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions } = state; + const { datasourceInstance, initialQueries, queryIntervals, queryTransactions, queryKeys } = state; const { index } = action.payload; if (initialQueries.length <= 1) { @@ -339,9 +339,10 @@ export const itemReducer = reducerFactory({} as ExploreItemSta } const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)]; // Discard transactions related to row query - const nextQueryTransactions = queryTransactions.filter(qt => qt.rowIndex !== index); + const nextQueryTransactions = queryTransactions.filter(qt => nextQueries.some(nq => nq.key === qt.query.key)); const results = calculateResultsFromQueryTransactions( nextQueryTransactions, datasourceInstance, @@ -354,7 +355,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta initialQueries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, - queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), + queryKeys: nextQueryKeys, }; }, }) diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index dc9a478adf3..570a387cd74 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,6 +1,6 @@ import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; import thunk from 'redux-thunk'; -// import { createLogger } from 'redux-logger'; +import { createLogger } from 'redux-logger'; import sharedReducers from 'app/core/reducers'; import alertingReducers from 'app/features/alerting/state/reducers'; import teamsReducers from 'app/features/teams/state/reducers'; @@ -39,7 +39,7 @@ export function configureStore() { if (process.env.NODE_ENV !== 'production') { // DEV builds we had the logger middleware - setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); + setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger())))); } else { setStore(createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk)))); } From bc21c9520fafee7c6a0380389d695526aa921fd8 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 15:07:11 +0100 Subject: [PATCH 31/76] fix: Data source picker in panel queries options should overlap content below, including ace scrollbar #15122 --- public/sass/components/_toolbar.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_toolbar.scss b/public/sass/components/_toolbar.scss index 14db85f7e65..36be8a18739 100644 --- a/public/sass/components/_toolbar.scss +++ b/public/sass/components/_toolbar.scss @@ -4,7 +4,6 @@ align-items: center; padding: 3px 20px 3px 20px; position: relative; - z-index: 1; flex: 0 0 auto; background: $toolbar-bg; border-radius: 3px; From f74ebdade663d727d153fb0b15a76c9cb0de6693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Mon, 4 Feb 2019 15:11:19 +0100 Subject: [PATCH 32/76] Missed to save --- public/app/features/explore/state/actions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index a41c0701994..a0357315484 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -710,7 +710,7 @@ const togglePanelActionCreator = ( ) => (exploreId: ExploreId) => { return (dispatch, getState) => { let shouldRunQueries; - dispatch(actionCreator); + dispatch(actionCreator({ exploreId })); dispatch(stateSave()); switch (actionCreator.type) { From 648bec1807b9685693b8664ed730d5f5b9975434 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 15:19:23 +0100 Subject: [PATCH 33/76] fix: Set ace editor min height to avoid problem with scrollbar overlapping ace content #15122 --- public/sass/components/_code_editor.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_code_editor.scss b/public/sass/components/_code_editor.scss index 4f50495789d..a9c7ebf2e75 100644 --- a/public/sass/components/_code_editor.scss +++ b/public/sass/components/_code_editor.scss @@ -7,7 +7,7 @@ &.ace_editor { @include font-family-monospace(); font-size: 1rem; - min-height: 2.6rem; + min-height: 3.6rem; // Include space for horizontal scrollbar @include border-radius($input-border-radius-sm); border: $input-btn-border-width solid $input-border-color; From b9c58d88dc381dc096a66bc9eadcc6489cf70588 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 16:48:27 +0100 Subject: [PATCH 34/76] basic layout --- .../AddPanelWidget/AddPanelWidget.tsx | 29 ++++++++++--------- .../AddPanelWidget/_AddPanelWidget.scss | 22 +++++++++++++- public/sass/base/_icons.scss | 2 +- 3 files changed, 38 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 8c1ab93cec1..7a9767666c0 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -125,13 +125,20 @@ export class AddPanelWidget extends React.Component { dashboard.removePanel(this.props.panel); }; + renderOptionLink = (icon, text, onClick) => { + return ( + + ); + }; + render() { - let addCopyButton; - - if (this.state.copiedPanelPlugins.length === 1) { - addCopyButton = this.copyButton(this.state.copiedPanelPlugins[0]); - } - return (
@@ -142,13 +149,9 @@ export class AddPanelWidget extends React.Component {
- - {addCopyButton} - + {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', this.onCreateNewPanel)} + {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)}
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index 5a1cbee4b44..587daa2703f 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -26,6 +26,26 @@ } } +.add-panel-widget__link { + display: block; + margin: 0 8px; + width: 130px; + text-align: center; + padding: 8px 0; +} + +.add-panel-widget__icon { + margin-bottom: 8px; + + .gicon { + color: white; + height: 44px; + width: 53px; + position: relative; + left: 5px; + } +} + .add-panel-widget__close { margin-left: auto; background-color: transparent; @@ -39,7 +59,7 @@ justify-content: center; align-items: center; height: 100%; - flex-direction: column; + flex-direction: row; .btn { margin-bottom: 10px; diff --git a/public/sass/base/_icons.scss b/public/sass/base/_icons.scss index a60259ac0f2..a2649b31fcd 100644 --- a/public/sass/base/_icons.scss +++ b/public/sass/base/_icons.scss @@ -212,7 +212,7 @@ padding-right: 5px; } -.panel-editor-tabs { +.panel-editor-tabs, .add-panel-widget__icon { .gicon-advanced-active { background-image: url('../img/icons_#{$theme-name}_theme/icon_advanced_active.svg'); } From f6b46f7a34c822508605ffca9da8c564c26fb209 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 17:18:46 +0100 Subject: [PATCH 35/76] prepping go to visualization --- .../components/AddPanelWidget/AddPanelWidget.tsx | 9 +++++---- .../features/dashboard/panel_editor/VisualizationTab.tsx | 1 + 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 7a9767666c0..b3d5e6167c4 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -62,20 +62,21 @@ export class AddPanelWidget extends React.Component { ); } - moveToEdit(panel) { + moveToEdit(panel, tab) { reduxStore.dispatch( updateLocation({ query: { panelId: panel.id, edit: true, fullscreen: true, + tab: tab, }, partial: true, }) ); } - onCreateNewPanel = () => { + onCreateNewPanel = (tab = 'queries') => { const dashboard = this.props.dashboard; const { gridPos } = this.props.panel; @@ -88,7 +89,7 @@ export class AddPanelWidget extends React.Component { dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); - this.moveToEdit(newPanel); + this.moveToEdit(newPanel, tab); }; onPasteCopiedPanel = panelPluginInfo => { @@ -150,7 +151,7 @@ export class AddPanelWidget extends React.Component {
{this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', () => this.onCreateNewPanel('visualization'))} {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)}
diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 35b9b71112a..fdf978acdf9 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; +//TODO: See PanelEdit // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; From 9ab5eeb7f30cdbcd1b950ecbaeed8938679650b5 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 4 Feb 2019 17:26:20 +0100 Subject: [PATCH 36/76] fix: Explore: Query wrapping on long queries #15222 --- .../src/components/Select/SelectOptionGroup.tsx | 2 +- .../app/features/dashboard/panel_editor/QueriesTab.tsx | 2 +- public/app/features/explore/QueryRow.tsx | 4 ++-- .../prometheus/components/PromQueryField.tsx | 6 +++--- public/sass/components/_gf-form.scss | 4 ++++ public/sass/utils/_utils.scss | 10 +++++++++- 6 files changed, 20 insertions(+), 8 deletions(-) diff --git a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx index efc5e4516fc..9a787a84819 100644 --- a/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx +++ b/packages/grafana-ui/src/components/Select/SelectOptionGroup.tsx @@ -49,7 +49,7 @@ export default class SelectOptionGroup extends PureComponent
- {label} + {label} {' '}
{expanded && children} diff --git a/public/app/features/dashboard/panel_editor/QueriesTab.tsx b/public/app/features/dashboard/panel_editor/QueriesTab.tsx index 491f255d761..d46ff020906 100644 --- a/public/app/features/dashboard/panel_editor/QueriesTab.tsx +++ b/public/app/features/dashboard/panel_editor/QueriesTab.tsx @@ -133,7 +133,7 @@ export class QueriesTab extends PureComponent { return ( <> -
+
{!isAddingMixed && (
-
+
Date: Mon, 4 Feb 2019 17:37:07 +0100 Subject: [PATCH 37/76] now /api/login/ping returns Response --- pkg/api/api.go | 4 ++-- pkg/api/login.go | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/pkg/api/api.go b/pkg/api/api.go index 07cb712f794..980706d8355 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -108,8 +108,8 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/api/snapshots-delete/:deleteKey", Wrap(DeleteDashboardSnapshotByDeleteKey)) r.Delete("/api/snapshots/:key", reqEditorRole, Wrap(DeleteDashboardSnapshot)) - // api renew session based on remember cookie - r.Get("/api/login/ping", quota("session"), hs.LoginAPIPing) + // api renew session based on cookie + r.Get("/api/login/ping", quota("session"), Wrap(hs.LoginAPIPing)) // authed api r.Group("/api", func(apiRoute routing.RouteRegister) { diff --git a/pkg/api/login.go b/pkg/api/login.go index 3f2d82a6c0f..50c62e0835a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -78,13 +78,12 @@ func tryOAuthAutoLogin(c *m.ReqContext) bool { return false } -func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) { - if c.IsSignedIn || (c.AllowAnonymous && c.IsAnonymous) { - c.JsonOK("Logged in") - return +func (hs *HTTPServer) LoginAPIPing(c *m.ReqContext) Response { + if c.IsSignedIn || c.IsAnonymous { + return JSON(200, "Logged in") } - c.JsonApiErr(401, "Unauthorized", nil) + return Error(401, "Unauthorized", nil) } func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response { From e4dad78045bc70a17e8274310c67d7b82511660c Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 4 Feb 2019 21:26:49 +0100 Subject: [PATCH 38/76] added flags to vizpicker from query param --- .../AddPanelWidget/AddPanelWidget.tsx | 48 ++++++++++++++----- .../panel_editor/VisualizationTab.tsx | 4 +- 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index b3d5e6167c4..21c4451d9b9 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -17,6 +17,17 @@ export interface State { copiedPanelPlugins: any[]; } +type Location = { + query: { + panelId: number; + edit: boolean; + fullscreen: boolean; + tab?: string; + isVizPickerOpen?: boolean; + }; + partial: boolean; +}; + export class AddPanelWidget extends React.Component { constructor(props) { super(props); @@ -62,18 +73,8 @@ export class AddPanelWidget extends React.Component { ); } - moveToEdit(panel, tab) { - reduxStore.dispatch( - updateLocation({ - query: { - panelId: panel.id, - edit: true, - fullscreen: true, - tab: tab, - }, - partial: true, - }) - ); + moveToEdit(location) { + reduxStore.dispatch(updateLocation(location)); } onCreateNewPanel = (tab = 'queries') => { @@ -89,7 +90,28 @@ export class AddPanelWidget extends React.Component { dashboard.addPanel(newPanel); dashboard.removePanel(this.props.panel); - this.moveToEdit(newPanel, tab); + let location: Location = { + query: { + panelId: newPanel.id, + edit: true, + fullscreen: true, + }, + partial: true, + }; + + if (tab === 'visualization') { + location = { + ...location, + query: { + ...location.query, + tab: 'visualization', + isVizPickerOpen: true, + }, + }; + this.moveToEdit(location); + } else { + this.moveToEdit(location); + } }; onPasteCopiedPanel = panelPluginInfo => { diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index fdf978acdf9..1ca290d4051 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,7 +3,7 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; -//TODO: See PanelEdit +import { store } from 'app/store/store'; // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; @@ -39,7 +39,7 @@ export class VisualizationTab extends PureComponent { super(props); this.state = { - isVizPickerOpen: false, + isVizPickerOpen: store.getState().location.query.isVizPickerOpen === true, searchQuery: '', scrollTop: 0, }; From a7a964ec19cec8f0848d14eee6109d3656f61701 Mon Sep 17 00:00:00 2001 From: SamuelToh Date: Sun, 27 Jan 2019 21:49:22 +1000 Subject: [PATCH 39/76] Added PATCH verb end point for annotation op Added new PATCH verb annotation endpoint Removed unwanted fmt Added test cases for PATCH verb annotation endpoint Fixed formatting issue Check arr len before proceeding Updated doc to include PATCH verb annotation endpt --- docs/sources/http_api/annotations.md | 27 +++++++++++- pkg/api/annotations.go | 59 ++++++++++++++++++++++++++ pkg/api/annotations_test.go | 62 ++++++++++++++++++++++++++++ pkg/api/api.go | 1 + pkg/api/dtos/annotations.go | 8 ++++ 5 files changed, 156 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index 6633714d77b..dee4ede0777 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -160,15 +160,18 @@ Content-Type: application/json } ``` -## Update Annotation +## Replace Annotation `PUT /api/annotations/:id` +Replaces the annotation that matches the specified id. + **Example Request**: ```json PUT /api/annotations/1141 HTTP/1.1 Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json { @@ -180,6 +183,28 @@ Content-Type: application/json } ``` +## Update Annotation + +`PATCH /api/annotations/:id` + +Updates one or more properties of an annotation that matches the specified id. + +**Example Request**: + +```json +PATCH /api/annotations/1145 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +Content-Type: application/json + +{ + "time":1507037197000, + "timeEnd":1507180807095, + "text":"New Annotation Description", + "tags":["tag6","tag7","tag8"] +} +``` + ## Delete Annotation By Id `DELETE /api/annotations/:id` diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 242b5531f51..da9b55a1c16 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { return Success("Annotation updated") } +func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response { + annotationID := c.ParamsInt64(":annotationId") + + repo := annotations.GetRepository() + + if resp := canSave(c, repo, annotationID); resp != nil { + return resp + } + + items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId}) + + if err != nil || len(items) == 0 { + return Error(500, "Could not find annotation to update", err) + } + + existing := annotations.Item{ + OrgId: c.OrgId, + UserId: c.UserId, + Id: annotationID, + Epoch: items[0].Time, + Text: items[0].Text, + Tags: items[0].Tags, + RegionId: items[0].RegionId, + } + + if cmd.Tags != nil { + existing.Tags = cmd.Tags + } + + if cmd.Text != "" && cmd.Text != existing.Text { + existing.Text = cmd.Text + } + + if cmd.Time > 0 && cmd.Time != existing.Epoch { + existing.Epoch = cmd.Time + } + + if err := repo.Update(&existing); err != nil { + return Error(500, "Failed to update annotation", err) + } + + // Update region end time if provided + if existing.RegionId != 0 && cmd.TimeEnd > 0 { + itemRight := existing + itemRight.RegionId = existing.Id + itemRight.Epoch = cmd.TimeEnd + + // We don't know id of region right event, so set it to 0 and find then using query like + // ... WHERE region_id = AND id != ... + itemRight.Id = 0 + + if err := repo.Update(&itemRight); err != nil { + return Error(500, "Failed to update annotation for region end time", err) + } + } + + return Success("Annotation patched") +} + func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response { repo := annotations.GetRepository() diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 08f3018c694..ebdd867a031 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { IsRegion: false, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 1000, + Text: "annotation text", + Tags: []string{"tag1", "tag2"}, + } + Convey("When user is an Org Viewer", func() { role := m.ROLE_VIEWER Convey("Should not be allowed to save an annotation", func() { @@ -40,6 +46,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) { Id: 1, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 8000, + Text: "annotation text 50", + Tags: []string{"foo", "bar"}, + Id: 1, + } + deleteCmd := dtos.DeleteAnnotationsCmd{ DashboardId: 1, PanelId: 1, @@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -189,6 +222,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) }) + + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) @@ -264,6 +303,29 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m. }) } +func patchAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = role + + return PatchAnnotation(c, cmd) + }) + + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + + sc.m.Patch(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() diff --git a/pkg/api/api.go b/pkg/api/api.go index 980706d8355..0685ef3814d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -354,6 +354,7 @@ func (hs *HTTPServer) registerRoutes() { annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation)) annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID)) annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation)) + annotationsRoute.Patch("/:annotationId", bind(dtos.PatchAnnotationsCmd{}), Wrap(PatchAnnotation)) annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion)) annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation)) }) diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index c917b0d9feb..b64329e56d1 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct { TimeEnd int64 `json:"timeEnd"` } +type PatchAnnotationsCmd struct { + Id int64 `json:"id"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + TimeEnd int64 `json:"timeEnd"` +} + type DeleteAnnotationsCmd struct { AlertId int64 `json:"alertId"` DashboardId int64 `json:"dashboardId"` From 2c255fd85a8646303e6b97455bc2869e25420609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 06:19:40 +0100 Subject: [PATCH 40/76] Renamed initialQueries to queries --- packages/grafana-ui/src/types/plugin.ts | 2 +- public/app/features/explore/QueryRow.tsx | 14 +++--- public/app/features/explore/state/actions.ts | 27 ++++------- public/app/features/explore/state/reducers.ts | 47 ++++++++++--------- .../loki/components/LokiQueryEditor.tsx | 2 +- .../loki/components/LokiQueryField.tsx | 13 ++--- .../prometheus/components/PromQueryField.tsx | 13 ++--- public/app/types/explore.ts | 4 +- 8 files changed, 54 insertions(+), 68 deletions(-) diff --git a/packages/grafana-ui/src/types/plugin.ts b/packages/grafana-ui/src/types/plugin.ts index e674c9fbc32..c8f156c08dc 100644 --- a/packages/grafana-ui/src/types/plugin.ts +++ b/packages/grafana-ui/src/types/plugin.ts @@ -56,7 +56,7 @@ export interface QueryEditorProps { datasource: DSType; - initialQuery: TQuery; + query: TQuery; error?: string | JSX.Element; hint?: QueryHint; history: any[]; diff --git a/public/app/features/explore/QueryRow.tsx b/public/app/features/explore/QueryRow.tsx index 5e2e8442e54..bcb980e49e4 100644 --- a/public/app/features/explore/QueryRow.tsx +++ b/public/app/features/explore/QueryRow.tsx @@ -35,7 +35,7 @@ interface QueryRowProps { highlightLogsExpressionAction: typeof highlightLogsExpressionAction; history: HistoryItem[]; index: number; - initialQuery: DataQuery; + query: DataQuery; modifyQueries: typeof modifyQueries; queryTransactions: QueryTransaction[]; exploreEvents: Emitter; @@ -95,7 +95,7 @@ export class QueryRow extends PureComponent { }, 500); render() { - const { datasourceInstance, history, index, initialQuery, queryTransactions, exploreEvents, range } = this.props; + const { datasourceInstance, history, index, query, queryTransactions, exploreEvents, range } = this.props; const transactions = queryTransactions.filter(t => t.rowIndex === index); const transactionWithError = transactions.find(t => t.error !== undefined); const hint = getFirstHintFromTransactions(transactions); @@ -110,7 +110,7 @@ export class QueryRow extends PureComponent { {QueryField ? ( { error={queryError} onQueryChange={this.onChangeQuery} onExecuteQuery={this.onExecuteQuery} - initialQuery={initialQuery} + initialQuery={query} exploreEvents={exploreEvents} range={range} /> @@ -155,9 +155,9 @@ export class QueryRow extends PureComponent { function mapStateToProps(state: StoreState, { exploreId, index }) { const explore = state.explore; const item: ExploreItemState = explore[exploreId]; - const { datasourceInstance, history, initialQueries, queryTransactions, range } = item; - const initialQuery = initialQueries[index]; - return { datasourceInstance, history, initialQuery, queryTransactions, range }; + const { datasourceInstance, history, queries, queryTransactions, range } = item; + const query = queries[index]; + return { datasourceInstance, history, query, queryTransactions, range }; } const mapDispatchToProps = { diff --git a/public/app/features/explore/state/actions.ts b/public/app/features/explore/state/actions.ts index a0357315484..f6fa5c05d63 100644 --- a/public/app/features/explore/state/actions.ts +++ b/public/app/features/explore/state/actions.ts @@ -87,7 +87,7 @@ export function changeDatasource(exploreId: ExploreId, datasource: string): Thun return async (dispatch, getState) => { const newDataSourceInstance = await getDatasourceSrv().get(datasource); const currentDataSourceInstance = getState().explore[exploreId].datasourceInstance; - const queries = getState().explore[exploreId].initialQueries; + const queries = getState().explore[exploreId].queries; await dispatch(importQueries(exploreId, queries, currentDataSourceInstance, newDataSourceInstance)); @@ -494,7 +494,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { return (dispatch, getState) => { const { datasourceInstance, - initialQueries, + queries, showingLogs, showingGraph, showingTable, @@ -503,7 +503,7 @@ export function runQueries(exploreId: ExploreId, ignoreUIState = false) { supportsTable, } = getState().explore[exploreId]; - if (!hasNonEmptyQuery(initialQueries)) { + if (!hasNonEmptyQuery(queries)) { dispatch(runQueriesEmptyAction({ exploreId })); dispatch(stateSave()); // Remember to saves to state and update location return; @@ -565,14 +565,7 @@ function runQueriesForType( resultGetter?: any ) { return async (dispatch, getState) => { - const { - datasourceInstance, - eventBridge, - initialQueries: queries, - queryIntervals, - range, - scanning, - } = getState().explore[exploreId]; + const { datasourceInstance, eventBridge, queries, queryIntervals, range, scanning } = getState().explore[exploreId]; const datasourceId = datasourceInstance.meta.id; // Run all queries concurrently @@ -653,7 +646,7 @@ export function splitOpen(): ThunkResult { const itemState = { ...leftState, queryTransactions: [], - initialQueries: leftState.initialQueries.slice(), + queries: leftState.queries.slice(), }; dispatch(splitOpenAction({ itemState })); dispatch(stateSave()); @@ -670,7 +663,7 @@ export function stateSave() { const urlStates: { [index: string]: string } = {}; const leftUrlState: ExploreUrlState = { datasource: left.datasourceInstance.name, - queries: left.initialQueries.map(clearQueryKeys), + queries: left.queries.map(clearQueryKeys), range: left.range, ui: { showingGraph: left.showingGraph, @@ -682,13 +675,9 @@ export function stateSave() { if (split) { const rightUrlState: ExploreUrlState = { datasource: right.datasourceInstance.name, - queries: right.initialQueries.map(clearQueryKeys), + queries: right.queries.map(clearQueryKeys), range: right.range, - ui: { - showingGraph: right.showingGraph, - showingLogs: right.showingLogs, - showingTable: right.showingTable, - }, + ui: { showingGraph: right.showingGraph, showingLogs: right.showingLogs, showingTable: right.showingTable }, }; urlStates.right = serializeStateToUrlParam(rightUrlState, true); diff --git a/public/app/features/explore/state/reducers.ts b/public/app/features/explore/state/reducers.ts index 7c0a729d0ed..76fc7d5de32 100644 --- a/public/app/features/explore/state/reducers.ts +++ b/public/app/features/explore/state/reducers.ts @@ -60,7 +60,7 @@ export const makeExploreItemState = (): ExploreItemState => ({ datasourceMissing: false, exploreDatasources: [], history: [], - initialQueries: [], + queries: [], initialized: false, queryTransactions: [], queryIntervals: { interval: '15s', intervalMs: DEFAULT_GRAPH_INTERVAL }, @@ -92,23 +92,26 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: addQueryRowAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { index, query } = action.payload; - // Add to initialQueries, which will cause a new row to be rendered - const nextQueries = [...initialQueries.slice(0, index + 1), { ...query }, ...initialQueries.slice(index + 1)]; + // Add to queries, which will cause a new row to be rendered + const nextQueries = [...queries.slice(0, index + 1), { ...query }, ...queries.slice(index + 1)]; // Ongoing transactions need to update their row indices const nextQueryTransactions = queryTransactions.map(qt => { if (qt.rowIndex > index) { - return { ...qt, rowIndex: qt.rowIndex + 1 }; + return { + ...qt, + rowIndex: qt.rowIndex + 1, + }; } return qt; }); return { ...state, - initialQueries: nextQueries, + queries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), @@ -118,12 +121,12 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: changeQueryAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { query, index } = action.payload; // Override path: queries are completely reset const nextQuery: DataQuery = { ...query, ...generateEmptyQuery(index) }; - const nextQueries = [...initialQueries]; + const nextQueries = [...queries]; nextQueries[index] = nextQuery; // Discard ongoing transaction related to row query @@ -131,7 +134,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, - initialQueries: nextQueries, + queries: nextQueries, queryTransactions: nextQueryTransactions, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), }; @@ -162,7 +165,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const queries = ensureQueries(); return { ...state, - initialQueries: queries.slice(), + queries: queries.slice(), queryTransactions: [], showingStartPage: Boolean(state.StartPage), queryKeys: getQueryKeys(queries, state.datasourceInstance), @@ -186,7 +189,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta eventBridge, exploreDatasources, range, - initialQueries: queries, + queries, initialized: true, queryKeys: getQueryKeys(queries, state.datasourceInstance), ...ui, @@ -197,7 +200,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta filter: updateDatasourceInstanceAction, mapper: (state, action): ExploreItemState => { const { datasourceInstance } = action.payload; - return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.initialQueries, datasourceInstance) }; + return { ...state, datasourceInstance, queryKeys: getQueryKeys(state.queries, datasourceInstance) }; }, }) .addMapper({ @@ -254,13 +257,13 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: modifyQueriesAction, mapper: (state, action): ExploreItemState => { - const { initialQueries, queryTransactions } = state; + const { queries, queryTransactions } = state; const { modification, index, modifier } = action.payload; let nextQueries: DataQuery[]; let nextQueryTransactions; if (index === undefined) { // Modify all queries - nextQueries = initialQueries.map((query, i) => ({ + nextQueries = queries.map((query, i) => ({ ...modifier({ ...query }, modification), ...generateEmptyQuery(i), })); @@ -268,7 +271,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta nextQueryTransactions = []; } else { // Modify query only at index - nextQueries = initialQueries.map((query, i) => { + nextQueries = queries.map((query, i) => { // Synchronize all queries with local query cache to ensure consistency // TODO still needed? return i === index ? { ...modifier({ ...query }, modification), ...generateEmptyQuery(i) } : query; @@ -286,7 +289,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta } return { ...state, - initialQueries: nextQueries, + queries: nextQueries, queryKeys: getQueryKeys(nextQueries, state.datasourceInstance), queryTransactions: nextQueryTransactions, }; @@ -332,14 +335,14 @@ export const itemReducer = reducerFactory({} as ExploreItemSta .addMapper({ filter: removeQueryRowAction, mapper: (state, action): ExploreItemState => { - const { datasourceInstance, initialQueries, queryIntervals, queryTransactions, queryKeys } = state; + const { datasourceInstance, queries, queryIntervals, queryTransactions, queryKeys } = state; const { index } = action.payload; - if (initialQueries.length <= 1) { + if (queries.length <= 1) { return state; } - const nextQueries = [...initialQueries.slice(0, index), ...initialQueries.slice(index + 1)]; + const nextQueries = [...queries.slice(0, index), ...queries.slice(index + 1)]; const nextQueryKeys = [...queryKeys.slice(0, index), ...queryKeys.slice(index + 1)]; // Discard transactions related to row query @@ -353,7 +356,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta return { ...state, ...results, - initialQueries: nextQueries, + queries: nextQueries, logsHighlighterExpressions: undefined, queryTransactions: nextQueryTransactions, queryKeys: nextQueryKeys, @@ -398,7 +401,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const { queries } = action.payload; return { ...state, - initialQueries: queries.slice(), + queries: queries.slice(), queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, @@ -452,7 +455,7 @@ export const itemReducer = reducerFactory({} as ExploreItemSta const { queries } = action.payload; return { ...state, - initialQueries: queries, + queries, queryKeys: getQueryKeys(queries, state.datasourceInstance), }; }, diff --git a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx index e9912522f16..a1b9e7a5df9 100644 --- a/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx +++ b/public/app/plugins/datasource/loki/components/LokiQueryEditor.tsx @@ -59,7 +59,7 @@ export class LokiQueryEditor extends PureComponent {
{ // Send text change to parent - const { initialQuery, onQueryChange, onExecuteQuery } = this.props; + const { query, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { - const query = { - ...initialQuery, - expr: value, - }; - onQueryChange(query); + const nextQuery = { ...query, expr: value }; + onQueryChange(nextQuery); if (override && onExecuteQuery) { onExecuteQuery(); @@ -217,7 +214,7 @@ export class LokiQueryField extends React.PureComponent 0; @@ -237,7 +234,7 @@ export class LokiQueryField extends React.PureComponent { // Send text change to parent - const { initialQuery, onQueryChange, onExecuteQuery } = this.props; + const { query, onQueryChange, onExecuteQuery } = this.props; if (onQueryChange) { - const query: PromQuery = { - ...initialQuery, - expr: value, - }; - onQueryChange(query); + const nextQuery: PromQuery = { ...query, expr: value }; + onQueryChange(nextQuery); if (override && onExecuteQuery) { onExecuteQuery(); @@ -240,7 +237,7 @@ class PromQueryField extends React.PureComponent Date: Tue, 5 Feb 2019 07:03:16 +0100 Subject: [PATCH 41/76] Fixed so onBlur event trigger an QueryChange and QueryExecute if values differ --- public/app/features/explore/QueryField.tsx | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index a0e70e8066c..8ab7e56dc5a 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -50,6 +50,7 @@ export interface QueryFieldState { typeaheadPrefix: string; typeaheadText: string; value: Value; + lastExecutedValue: Value; } export interface TypeaheadInput { @@ -89,6 +90,7 @@ export class QueryField extends React.PureComponent { + handleBlur = (event, change) => { + const { lastExecutedValue } = this.state; + const previousValue = lastExecutedValue ? Plain.serialize(this.state.lastExecutedValue) : null; + const currentValue = Plain.serialize(change.value); + // If we dont wait here, menu clicks wont work because the menu // will be gone. this.resetTimer = setTimeout(this.resetTypeahead, 100); // Disrupting placeholder entry wipes all remaining placeholders needing input this.placeholdersBuffer.clearPlaceholders(); + + if (previousValue !== currentValue) { + this.executeOnQueryChangeAndExecuteQueries(); + } }; handleFocus = () => {}; From 275800cca94b79f6aac993e535e8d5f67bb9129a Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 4 Feb 2019 23:19:14 -0800 Subject: [PATCH 42/76] improve the stackdriver logo --- .../stackdriver/img/stackdriver_logo.png | Bin 15726 -> 0 bytes .../stackdriver/img/stackdriver_logo.svg | 1 + .../plugins/datasource/stackdriver/plugin.json | 4 ++-- 3 files changed, 3 insertions(+), 2 deletions(-) delete mode 100644 public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png create mode 100644 public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg diff --git a/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.png deleted file mode 100644 index cd52e773deb8e2fb4fbc27ff188541ce8e649025..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15726 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 UbDsl#Kn+rmQI@WdG!FWI0AbhjWdHyG diff --git a/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg new file mode 100644 index 00000000000..93878f20a06 --- /dev/null +++ b/public/app/plugins/datasource/stackdriver/img/stackdriver_logo.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/app/plugins/datasource/stackdriver/plugin.json b/public/app/plugins/datasource/stackdriver/plugin.json index e68100c0e59..1ee3d57e9b1 100644 --- a/public/app/plugins/datasource/stackdriver/plugin.json +++ b/public/app/plugins/datasource/stackdriver/plugin.json @@ -14,8 +14,8 @@ "description": "Google Stackdriver Datasource for Grafana", "version": "1.0.0", "logos": { - "small": "img/stackdriver_logo.png", - "large": "img/stackdriver_logo.png" + "small": "img/stackdriver_logo.svg", + "large": "img/stackdriver_logo.svg" }, "author": { "name": "Grafana Project", From bfdfb215f329eb5a04b5318db938a81bdddb3a2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 09:32:42 +0100 Subject: [PATCH 43/76] added missing typing to explore props --- public/app/features/explore/Explore.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 8eb177b8ad4..b210bcccc18 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -21,7 +21,7 @@ import TimePicker, { parseTime } from './TimePicker'; import { changeSize, changeTime, initializeExplore, modifyQueries, scanStart, setQueries } from './state/actions'; // Types -import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps } from '@grafana/ui'; +import { RawTimeRange, TimeRange, DataQuery, ExploreStartPageProps, ExploreDataSourceApi } from '@grafana/ui'; import { ExploreItemState, ExploreUrlState, RangeScanner, ExploreId } from 'app/types/explore'; import { StoreState } from 'app/types'; import { LAST_USED_DATASOURCE_KEY, ensureQueries, DEFAULT_RANGE, DEFAULT_UI_STATE } from 'app/core/utils/explore'; @@ -34,7 +34,7 @@ interface ExploreProps { changeSize: typeof changeSize; changeTime: typeof changeTime; datasourceError: string; - datasourceInstance: any; + datasourceInstance: ExploreDataSourceApi; datasourceLoading: boolean | null; datasourceMissing: boolean; exploreId: ExploreId; From 139fb65fa926c17cdb68f15c69da95c8eeefd7ee Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Feb 2019 12:36:12 +0100 Subject: [PATCH 44/76] docs: fixes #14940 --- docs/sources/installation/configuration.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 46bab83654e..ac3dc6ebfd0 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -393,9 +393,7 @@ Analytics ID here. By default this feature is disabled. ### check_for_updates -Set to false to disable all checks to https://grafana.com for new versions of Grafana and installed plugins. Check is used -in some UI views to notify that a Grafana or plugin update exists. This option does not cause any auto updates, nor -send any sensitive information. +Set to false to disable all checks to https://grafana.com for new versions of installed plugins and to the Grafana GitHub repository to check for a newer version of Grafana. The version information is used in some UI views to notify that a new Grafana update or a plugin update exists. This option does not cause any auto updates, nor send any sensitive information. The check is run every 10 minutes.
From 2802569529197d48e602da0f67bc9f2e1b1e75a1 Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Tue, 5 Feb 2019 12:47:42 +0100 Subject: [PATCH 45/76] minor layout change, simple render test --- .../AddPanelWidget/AddPanelWidget.test.tsx | 23 ++++++ .../AddPanelWidget/AddPanelWidget.tsx | 39 +++++---- .../AddPanelWidget/_AddPanelWidget.scss | 27 ++++--- .../AddPanelWidget.test.tsx.snap | 81 +++++++++++++++++++ 4 files changed, 146 insertions(+), 24 deletions(-) create mode 100644 public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx create mode 100644 public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx new file mode 100644 index 00000000000..91da066e4cc --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.test.tsx @@ -0,0 +1,23 @@ +import React from 'react'; +import { shallow } from 'enzyme'; +import { AddPanelWidget, Props } from './AddPanelWidget'; +import { DashboardModel, PanelModel } from '../../state'; + +const setup = (propOverrides?: object) => { + const props: Props = { + dashboard: {} as DashboardModel, + panel: {} as PanelModel, + }; + + Object.assign(props, propOverrides); + + return shallow(); +}; + +describe('Render', () => { + it('should render component', () => { + const wrapper = setup(); + + expect(wrapper).toMatchSnapshot(); + }); +}); diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 21c4451d9b9..e70615bde39 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -1,8 +1,8 @@ import React from 'react'; import _ from 'lodash'; import config from 'app/core/config'; -import { PanelModel } from '../../state/PanelModel'; -import { DashboardModel } from '../../state/DashboardModel'; +import { PanelModel } from '../../state'; +import { DashboardModel } from '../../state'; import store from 'app/core/store'; import { LS_PANEL_COPY_KEY } from 'app/core/constants'; import { updateLocation } from 'app/core/actions'; @@ -57,6 +57,7 @@ export class AddPanelWidget extends React.Component { copiedPanels.push(pluginCopy); } } + return _.sortBy(copiedPanels, 'sort'); } @@ -65,14 +66,6 @@ export class AddPanelWidget extends React.Component { this.props.dashboard.removePanel(this.props.dashboard.panels[0]); } - copyButton(panel) { - return ( - - ); - } - moveToEdit(location) { reduxStore.dispatch(updateLocation(location)); } @@ -151,7 +144,7 @@ export class AddPanelWidget extends React.Component { renderOptionLink = (icon, text, onClick) => { return (
- +
@@ -162,6 +155,8 @@ export class AddPanelWidget extends React.Component { }; render() { + const { copiedPanelPlugins } = this.state; + return (
@@ -172,9 +167,25 @@ export class AddPanelWidget extends React.Component {
- {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', () => this.onCreateNewPanel('visualization'))} - {this.renderOptionLink('queries', 'Convert to row', this.onCreateNewRow)} +
+ {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Panel type', () => + this.onCreateNewPanel('visualization') + )} +
+
+
+ Convert to row +
+ {copiedPanelPlugins.length === 1 && ( +
this.onPasteCopiedPanel(copiedPanelPlugins[0])} + > + Paste copied panel +
+ )} +
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index 587daa2703f..ab6ff8556d8 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -27,11 +27,8 @@ } .add-panel-widget__link { - display: block; margin: 0 8px; - width: 130px; - text-align: center; - padding: 8px 0; + width: 150px; } .add-panel-widget__icon { @@ -54,14 +51,24 @@ margin-right: -10px; } +.add-panel-widget__create { + display: inherit; + margin-bottom: 24px; +} + +.add-panel-widget__actions { + display: inherit; +} + +.add-panel-widget__action { + cursor: pointer; + margin: 0 4px; +} + .add-panel-widget__btn-container { + height: 100%; display: flex; justify-content: center; align-items: center; - height: 100%; - flex-direction: row; - - .btn { - margin-bottom: 10px; - } + flex-direction: column; } diff --git a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap new file mode 100644 index 00000000000..585f45210af --- /dev/null +++ b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap @@ -0,0 +1,81 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Render should render component 1`] = ` + +`; From 9ba98b87035e07714bf65c7a11c63a3f1c0c952f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 13:13:52 +0100 Subject: [PATCH 46/76] Fixes #15223 by handling onPaste event because of bug in Slate --- public/app/features/explore/QueryField.tsx | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/app/features/explore/QueryField.tsx b/public/app/features/explore/QueryField.tsx index 8ab7e56dc5a..810bca9ef5a 100644 --- a/public/app/features/explore/QueryField.tsx +++ b/public/app/features/explore/QueryField.tsx @@ -468,6 +468,14 @@ export class QueryField extends React.PureComponent { + const pastedValue = event.clipboardData.getData('Text'); + const newValue = change.value.change().insertText(pastedValue); + this.onChange(newValue); + + return true; + }; + render() { const { disabled } = this.props; const wrapperClassName = classnames('slate-query-field__wrapper', { @@ -484,6 +492,7 @@ export class QueryField extends React.PureComponent Date: Tue, 5 Feb 2019 09:34:04 +0100 Subject: [PATCH 47/76] chore: Replace sizeMe with AutoSizer in DashboardGrid --- .../dashboard/dashgrid/DashboardGrid.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 658bfad3816..5a65fadd74b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -5,13 +5,12 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; import classNames from 'classnames'; -import sizeMe from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; -interface GridWrapperProps { - size: { width: number; }; +interface SizedReactLayoutGridProps { layout: ReactGridLayout.Layout[]; onLayoutChange: (layout: ReactGridLayout.Layout[]) => void; children: JSX.Element | JSX.Element[]; @@ -25,8 +24,12 @@ interface GridWrapperProps { isFullscreen?: boolean; } +interface GridWrapperProps extends SizedReactLayoutGridProps { + sizedWidth: number; +} + function GridWrapper({ - size, + sizedWidth, layout, onLayoutChange, children, @@ -38,8 +41,8 @@ function GridWrapper({ isResizable, isDraggable, isFullscreen, -}: GridWrapperProps) { - const width = size.width > 0 ? size.width : lastGridWidth; +}: GridWrapperProps) { + const width = sizedWidth > 0 ? sizedWidth : lastGridWidth; // logic to ignore width changes (optimization) if (width !== lastGridWidth) { @@ -74,7 +77,16 @@ function GridWrapper({ ); } -const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); +const SizedReactLayoutGrid = (props: SizedReactLayoutGridProps) => ( + + {({width}) => ( + + )} + +); export interface DashboardGridProps { dashboard: DashboardModel; From 097396c517e1db4e5633f7625168c255511deb1c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 09:57:54 +0100 Subject: [PATCH 48/76] chore: Replace withSize with AutoSizer in explore/Graph.tsx --- public/app/features/explore/Graph.test.tsx | 1 + public/app/features/explore/Graph.tsx | 27 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/Graph.test.tsx b/public/app/features/explore/Graph.test.tsx index fe4deaf17aa..8976c677592 100644 --- a/public/app/features/explore/Graph.test.tsx +++ b/public/app/features/explore/Graph.test.tsx @@ -5,6 +5,7 @@ import { mockData } from './__mocks__/mockData'; const setup = (propOverrides?: object) => { const props = { + size: { width: 10, height: 20 }, data: mockData().slice(0, 19), range: { from: 'now-6h', to: 'now' }, ...propOverrides, diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 5d64dde28ce..b087f6a457d 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -1,7 +1,7 @@ import $ from 'jquery'; import React, { PureComponent } from 'react'; import moment from 'moment'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; @@ -80,12 +80,15 @@ interface GraphProps { id?: string; range: RawTimeRange; split?: boolean; - size?: { width: number; height: number }; userOptions?: any; onChangeTime?: (range: RawTimeRange) => void; onToggleSeries?: (alias: string, hiddenSeries: Set) => void; } +interface SizedGraphProps extends GraphProps { + size: { width: number; height: number }; +} + interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -95,7 +98,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -116,7 +119,7 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || @@ -261,4 +264,18 @@ export class Graph extends PureComponent { } } -export default withSize()(Graph); +export default (props: GraphProps) => ( + + {({width, height}) => { + return ( + + ); + }} + +); From d68df9d704d2fb637ba302c86e11107fe5e1953e Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:09:24 +0100 Subject: [PATCH 49/76] fix: Calculation issue with AutoSizer in explore --- public/app/features/explore/Graph.tsx | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index b087f6a457d..10006557349 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -265,17 +265,19 @@ export class Graph extends PureComponent { } export default (props: GraphProps) => ( - - {({width, height}) => { - return ( - - ); - }} - +
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} + + {({width, height}) => ( +
+ {width > 0 && } +
+ )} +
+
); From 260b6f5de83133f668aa15a3874eeeb20d46cbe7 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:12:24 +0100 Subject: [PATCH 50/76] chore: Remove react-sizeme --- package.json | 1 - yarn.lock | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/package.json b/package.json index 77fd92baf57..18c0a56f0c4 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-redux": "^5.0.7", - "react-sizeme": "^2.3.6", "react-table": "^6.8.6", "react-transition-group": "^2.2.1", "react-virtualized": "^9.21.0", diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..fd0c446fbce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3972,11 +3972,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -batch-processor@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" - integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -6613,13 +6608,6 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: - version "1.2.0" - resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.0.tgz#63344fd6f4e5ecff6f018d027e17b281fd4fa338" - integrity sha512-UmhNB8sIJVZeg56gEjgmMd6p37sCg8j8trVW0LZM7Wzv+kxQ5CnRHcgRKBTB/kFUSn3e7UP59kl2V2U8Du1hmg== - dependencies: - batch-processor "1.0.0" - elliptic@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -10900,11 +10888,6 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= - lodash.union@4.6.0, lodash.union@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" @@ -14215,17 +14198,6 @@ react-resizable@1.x: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" -react-sizeme@^2.3.6: - version "2.5.2" - resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.5.2.tgz#e7041390cfb895ed15d896aa91d76e147e3b70b5" - integrity sha512-hYvcncV1FxVzPm2EhVwlOLf7Tk+k/ttO6rI7bfKUL/aL1gYzrY3DXJsdZ6nFaFgGSU/i8KC6gCoptOhBbRJpXQ== - dependencies: - element-resize-detector "^1.1.12" - invariant "^2.2.2" - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - shallowequal "^1.0.2" - react-split-pane@^0.1.84: version "0.1.85" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.85.tgz#64819946a99b617ffa2d20f6f45a0056b6ee4faa" From f5431f521082743d930f54d1d93f6dcaface7f7a Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 13:48:00 +0100 Subject: [PATCH 51/76] chore: Explore: Remove inner AutoSizer, spread the size-object to width/height, change height type to number --- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Graph.tsx | 38 +++++-------------- .../app/features/explore/GraphContainer.tsx | 6 ++- public/app/features/explore/Logs.tsx | 2 +- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b210bcccc18..437b50db63c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -211,7 +211,7 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && !supportsLogs && } + {supportsGraph && !supportsLogs && } {supportsTable && } {supportsLogs && ( ) => void; } -interface SizedGraphProps extends GraphProps { - size: { width: number; height: number }; -} - interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -98,7 +94,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -119,13 +115,13 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || prevProps.split !== this.props.split || prevProps.height !== this.props.height || - (prevProps.size && prevProps.size.width !== this.props.size.width) || + prevProps.width !== this.props.width || !equal(prevState.hiddenSeries, this.state.hiddenSeries) ) { this.draw(); @@ -147,8 +143,8 @@ export class Graph extends PureComponent { }; getDynamicOptions() { - const { range, size } = this.props; - const ticks = (size.width || 0) / 100; + const { range, width } = this.props; + const ticks = (width || 0) / 100; let { from, to } = range; if (!moment.isMoment(from)) { from = dateMath.parse(from, false); @@ -240,7 +236,7 @@ export class Graph extends PureComponent { } render() { - const { height = '100px', id = 'graph' } = this.props; + const { height = 100, id = 'graph' } = this.props; const { hiddenSeries } = this.state; const data = this.getGraphData(); @@ -264,20 +260,4 @@ export class Graph extends PureComponent { } } -export default (props: GraphProps) => ( -
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} - - {({width, height}) => ( -
- {width > 0 && } -
- )} -
-
-); +export default Graph; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 7263fd09288..3950d89c11f 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -20,6 +20,7 @@ interface GraphContainerProps { split: boolean; toggleGraph: typeof toggleGraph; changeTime: typeof changeTime; + width: number; } export class GraphContainer extends PureComponent { @@ -32,8 +33,8 @@ export class GraphContainer extends PureComponent { }; render() { - const { exploreId, graphResult, loading, showingGraph, showingTable, range, split } = this.props; - const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width } = this.props; + const graphHeight = showingGraph && showingTable ? 200 : 400; if (!graphResult) { return null; @@ -48,6 +49,7 @@ export class GraphContainer extends PureComponent { onChangeTime={this.onChangeTime} range={range} split={split} + width={width} /> ); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 490257cb9a9..b6c903bc504 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -214,7 +214,7 @@ export default class Logs extends PureComponent {
Date: Tue, 5 Feb 2019 14:09:25 +0100 Subject: [PATCH 52/76] fix: Update snapshot --- .../app/features/explore/__snapshots__/Graph.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index a7ec6deb22c..c38fb26a252 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Render should render component 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -480,7 +480,7 @@ exports[`Render should render component with disclaimer 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -962,7 +962,7 @@ exports[`Render should show query return no time series 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> From e2ffaef88a0a67a9b155c0ce115bb3654d6af9b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:25:19 +0100 Subject: [PATCH 53/76] Fixed so that we close angular TimePicker when user clicks outside the dropdown --- public/app/routes/GrafanaCtrl.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..a860ba87e6b 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -280,6 +280,24 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop if (popover.length > 0 && target.parents('.graph-legend').length === 0) { popover.hide(); } + + // hide time picker + const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; + const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; + const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; + if ( + timePickerDropDownIsOpen && + !targetIsInTimePickerNav && + !targetIsInTimePickerDropDown && + !targetIsDatePickerRowBtn && + !targetIsDatePickerHeaderBtn + ) { + scope.$apply(() => { + scope.appEvent('closeTimepicker'); + }); + } }); }, }; From 0302c7afa7446b28baf02dec1b2c43e5e7f8d3d6 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 5 Feb 2019 15:28:03 +0100 Subject: [PATCH 54/76] stackdriver: add some more typings --- .../stackdriver/components/QueryEditor.tsx | 8 +++--- .../datasource/stackdriver/datasource.ts | 7 ++--- .../datasource/stackdriver/query_ctrl.ts | 4 +-- .../plugins/datasource/stackdriver/types.ts | 26 +++++++++++-------- 4 files changed, 25 insertions(+), 20 deletions(-) diff --git a/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx b/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx index 94521041416..c3bd9212b21 100644 --- a/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx +++ b/public/app/plugins/datasource/stackdriver/components/QueryEditor.tsx @@ -10,21 +10,21 @@ import { Alignments } from './Alignments'; import { AlignmentPeriods } from './AlignmentPeriods'; import { AliasBy } from './AliasBy'; import { Help } from './Help'; -import { Target, MetricDescriptor } from '../types'; +import { StackdriverQuery, MetricDescriptor } from '../types'; import { getAlignmentPickerData } from '../functions'; import StackdriverDatasource from '../datasource'; import { SelectOptionItem } from '@grafana/ui'; export interface Props { - onQueryChange: (target: Target) => void; + onQueryChange: (target: StackdriverQuery) => void; onExecuteQuery: () => void; - target: Target; + target: StackdriverQuery; events: any; datasource: StackdriverDatasource; templateSrv: TemplateSrv; } -interface State extends Target { +interface State extends StackdriverQuery { alignOptions: SelectOptionItem[]; lastQuery: string; lastQueryError: string; diff --git a/public/app/plugins/datasource/stackdriver/datasource.ts b/public/app/plugins/datasource/stackdriver/datasource.ts index 025955105a7..15c6350c8a0 100644 --- a/public/app/plugins/datasource/stackdriver/datasource.ts +++ b/public/app/plugins/datasource/stackdriver/datasource.ts @@ -2,9 +2,10 @@ import { stackdriverUnitMappings } from './constants'; import appEvents from 'app/core/app_events'; import _ from 'lodash'; import StackdriverMetricFindQuery from './StackdriverMetricFindQuery'; -import { MetricDescriptor } from './types'; +import { StackdriverQuery, MetricDescriptor } from './types'; +import { DataSourceApi, DataQueryOptions } from '@grafana/ui/src/types'; -export default class StackdriverDatasource { +export default class StackdriverDatasource implements DataSourceApi { id: number; url: string; baseUrl: string; @@ -103,7 +104,7 @@ export default class StackdriverDatasource { return unit; } - async query(options) { + async query(options: DataQueryOptions) { const result = []; const data = await this.getTimeSeries(options); if (data.results) { diff --git a/public/app/plugins/datasource/stackdriver/query_ctrl.ts b/public/app/plugins/datasource/stackdriver/query_ctrl.ts index c6a8a4d9782..3a2d0bb970a 100644 --- a/public/app/plugins/datasource/stackdriver/query_ctrl.ts +++ b/public/app/plugins/datasource/stackdriver/query_ctrl.ts @@ -1,7 +1,7 @@ import _ from 'lodash'; import { QueryCtrl } from 'app/plugins/sdk'; -import { Target } from './types'; +import { StackdriverQuery } from './types'; import { TemplateSrv } from 'app/features/templating/template_srv'; export class StackdriverQueryCtrl extends QueryCtrl { @@ -16,7 +16,7 @@ export class StackdriverQueryCtrl extends QueryCtrl { this.onExecuteQuery = this.onExecuteQuery.bind(this); } - onQueryChange(target: Target) { + onQueryChange(target: StackdriverQuery) { Object.assign(this.target, target); } diff --git a/public/app/plugins/datasource/stackdriver/types.ts b/public/app/plugins/datasource/stackdriver/types.ts index 29b12b4289d..83909bbafce 100644 --- a/public/app/plugins/datasource/stackdriver/types.ts +++ b/public/app/plugins/datasource/stackdriver/types.ts @@ -1,3 +1,5 @@ +import { DataQuery } from '@grafana/ui/src/types'; + export enum MetricFindQueryTypes { Services = 'services', MetricTypes = 'metricTypes', @@ -20,20 +22,22 @@ export interface VariableQueryData { services: Array<{ value: string; name: string }>; } -export interface Target { - defaultProject: string; - unit: string; +export interface StackdriverQuery extends DataQuery { + defaultProject?: string; + unit?: string; metricType: string; - service: string; + service?: string; refId: string; crossSeriesReducer: string; - alignmentPeriod: string; - perSeriesAligner: string; - groupBys: string[]; - filters: string[]; - aliasBy: string; - metricKind: string; - valueType: string; + alignmentPeriod?: string; + perSeriesAligner?: string; + groupBys?: string[]; + filters?: string[]; + aliasBy?: string; + metricKind?: string; + valueType?: string; + datasourceId: number; + view: string; } export interface AnnotationTarget { From a344091d82e74a8054fed8279ca271a41278980e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:29:19 +0100 Subject: [PATCH 55/76] Optimized so we only do checks when dropdown is opened --- public/app/routes/GrafanaCtrl.ts | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index a860ba87e6b..c6945f26d08 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -283,17 +283,21 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop // hide time picker const timePickerDropDownIsOpen = elem.find('.gf-timepicker-dropdown').length > 0; - const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; - const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; - const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; - const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; - if ( - timePickerDropDownIsOpen && - !targetIsInTimePickerNav && - !targetIsInTimePickerDropDown && - !targetIsDatePickerRowBtn && - !targetIsDatePickerHeaderBtn - ) { + if (timePickerDropDownIsOpen) { + const targetIsInTimePickerDropDown = target.parents('.gf-timepicker-dropdown').length > 0; + const targetIsInTimePickerNav = target.parents('.gf-timepicker-nav').length > 0; + const targetIsDatePickerRowBtn = target.parents('td[id^="datepicker-"]').length > 0; + const targetIsDatePickerHeaderBtn = target.parents('button[id^="datepicker-"]').length > 0; + + if ( + targetIsInTimePickerNav || + targetIsInTimePickerDropDown || + targetIsDatePickerRowBtn || + targetIsDatePickerHeaderBtn + ) { + return; + } + scope.$apply(() => { scope.appEvent('closeTimepicker'); }); From e42b670f5c29df535d4cb5477077011ab5ec9842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 5 Feb 2019 15:41:00 +0100 Subject: [PATCH 56/76] Closing timepicker when clicking outside the picker --- public/app/features/explore/ExploreToolbar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 35f06d11c81..4d9620e311e 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -8,6 +8,7 @@ import { DataSourcePicker } from 'app/core/components/Select/DataSourcePicker'; import { StoreState } from 'app/types/store'; import { changeDatasource, clearQueries, splitClose, runQueries, splitOpen } from './state/actions'; import TimePicker from './TimePicker'; +import { ClickOutsideWrapper } from 'app/core/components/ClickOutsideWrapper/ClickOutsideWrapper'; enum IconSide { left = 'left', @@ -79,6 +80,10 @@ export class UnConnectedExploreToolbar extends PureComponent { this.props.runQuery(this.props.exploreId); }; + onCloseTimePicker = () => { + this.props.timepickerRef.current.setState({ isOpen: false }); + }; + render() { const { datasourceMissing, @@ -137,7 +142,9 @@ export class UnConnectedExploreToolbar extends PureComponent {
) : null}
- + + +
- {this.renderOptionLink('queries', 'Add query', this.onCreateNewPanel)} - {this.renderOptionLink('visualization', 'Choose Panel type', () => + {this.renderOptionLink('queries', 'Add Query', this.onCreateNewPanel)} + {this.renderOptionLink('visualization', 'Choose Visualization', () => this.onCreateNewPanel('visualization') )}
-
- Convert to row -
+ {copiedPanelPlugins.length === 1 && ( -
this.onPasteCopiedPanel(copiedPanelPlugins[0])} > Paste copied panel -
+ )}
diff --git a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss index ab6ff8556d8..288b2e7a410 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss +++ b/public/app/features/dashboard/components/AddPanelWidget/_AddPanelWidget.scss @@ -14,6 +14,9 @@ align-items: center; width: 100%; cursor: move; + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; .gicon { font-size: 30px; @@ -26,9 +29,15 @@ } } +.add-panel-widget__title { + font-size: $font-size-md; + font-weight: $font-weight-semi-bold; + margin-right: $spacer*2; +} + .add-panel-widget__link { margin: 0 8px; - width: 150px; + width: 154px; } .add-panel-widget__icon { @@ -54,6 +63,8 @@ .add-panel-widget__create { display: inherit; margin-bottom: 24px; + // this is to have the big button appear centered + margin-top: 55px; } .add-panel-widget__actions { @@ -61,7 +72,6 @@ } .add-panel-widget__action { - cursor: pointer; margin: 0 4px; } diff --git a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap index 585f45210af..00faf48d8df 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap +++ b/public/app/features/dashboard/components/AddPanelWidget/__snapshots__/AddPanelWidget.test.tsx.snap @@ -13,6 +13,11 @@ exports[`Render should render component 1`] = ` + + New Panel +
- Add query + Add Query
@@ -60,7 +65,7 @@ exports[`Render should render component 1`] = ` />
- Choose Panel type + Choose Visualization
@@ -68,12 +73,12 @@ exports[`Render should render component 1`] = `
-
Convert to row -
+
diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index d7aafb89e55..bfdc13bc8f2 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import classNames from 'classnames'; import { QueriesTab } from './QueriesTab'; -import { VisualizationTab } from './VisualizationTab'; +import VisualizationTab from './VisualizationTab'; import { GeneralTab } from './GeneralTab'; import { AlertTab } from '../../alerting/AlertTab'; @@ -38,7 +38,7 @@ export class PanelEditor extends PureComponent { onChangeTab = (tab: PanelEditorTab) => { store.dispatch( updateLocation({ - query: { tab: tab.id }, + query: { tab: tab.id, openVizPicker: null }, partial: true, }) ); diff --git a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx index 1ca290d4051..94a403c11bf 100644 --- a/public/app/features/dashboard/panel_editor/VisualizationTab.tsx +++ b/public/app/features/dashboard/panel_editor/VisualizationTab.tsx @@ -3,7 +3,9 @@ import React, { PureComponent } from 'react'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; -import { store } from 'app/store/store'; +import { connectWithStore } from 'app/core/utils/connectWithReduxStore'; +import { StoreState } from 'app/types'; +import { updateLocation } from 'app/core/actions'; // Components import { EditorTabBody, EditorToolbarView } from './EditorTabBody'; @@ -22,6 +24,8 @@ interface Props { plugin: PanelPlugin; angularPanel?: AngularComponent; onTypeChanged: (newType: PanelPlugin) => void; + updateLocation: typeof updateLocation; + urlOpenVizPicker: boolean; } interface State { @@ -39,7 +43,7 @@ export class VisualizationTab extends PureComponent { super(props); this.state = { - isVizPickerOpen: store.getState().location.query.isVizPickerOpen === true, + isVizPickerOpen: this.props.urlOpenVizPicker, searchQuery: '', scrollTop: 0, }; @@ -150,6 +154,10 @@ export class VisualizationTab extends PureComponent { }; onCloseVizPicker = () => { + if (this.props.urlOpenVizPicker) { + this.props.updateLocation({ query: { openVizPicker: null }, partial: true }); + } + this.setState({ isVizPickerOpen: false }); }; @@ -237,3 +245,13 @@ export class VisualizationTab extends PureComponent { ); } } + +const mapStateToProps = (state: StoreState) => ({ + urlOpenVizPicker: !!state.location.query.openVizPicker +}); + +const mapDispatchToProps = { + updateLocation +}; + +export default connectWithStore(VisualizationTab, mapStateToProps, mapDispatchToProps); diff --git a/public/img/icons_dark_theme/icon_advanced.svg b/public/img/icons_dark_theme/icon_advanced.svg index 5fd18a86dd5..dea3ddff685 100644 --- a/public/img/icons_dark_theme/icon_advanced.svg +++ b/public/img/icons_dark_theme/icon_advanced.svg @@ -4,7 +4,7 @@ diff --git a/public/img/icons_dark_theme/icon_advanced_active.svg b/public/img/icons_dark_theme/icon_advanced_active.svg index 80672a2595b..1227ddc868c 100644 --- a/public/img/icons_dark_theme/icon_advanced_active.svg +++ b/public/img/icons_dark_theme/icon_advanced_active.svg @@ -5,7 +5,7 @@ width="121px" height="100px" viewBox="0 0 121 100" style="enable-background:new 0 0 121 100;" xml:space="preserve"> (test\.|test2) | Formats multi-value variable into a regex string `pipe` | ${servers:pipe} | `'test.', 'test2'` | test.|test2 | Formats multi-value variable into a pipe-separated string `csv`| ${servers:csv} | `'test1', 'test2'` | `test1,test2` | Formats multi-value variable as a comma-separated string +`json`| ${servers:json} | `'test1', 'test2'` | `["test1","test2"]` | Formats multi-value variable as a JSON string `distributed`| ${servers:distributed} | `'test1', 'test2'` | `test1,servers=test2` | Formats multi-value variable in custom format for OpenTSDB. `lucene`| ${servers:lucene} | `'test', 'test2'` | `("test" OR "test2")` | Formats multi-value variable as a lucene expression. `percentencode` | ${servers:percentencode} | `'foo()bar BAZ', 'test2'` | `{foo%28%29bar%20BAZ%2Ctest2}` | Formats multi-value variable into a glob, percent-encoded.