diff --git a/package.json b/package.json index 2a5c0312983..00e14eb48a5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "6.0.0", + "version": "6.0.1", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 40f6c6c3c37..c628d685f5c 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -14,6 +14,7 @@ interface Props { scrollTop?: number; setScrollTop: (event: any) => void; autoHeightMin?: number | string; + updateAfterMountMs?: number; } /** @@ -42,16 +43,26 @@ export class CustomScrollbar extends PureComponent { const ref = this.ref.current; if (ref && !_.isNil(this.props.scrollTop)) { - if (this.props.scrollTop > 10000) { - ref.scrollToBottom(); - } else { - ref.scrollTop(this.props.scrollTop); - } + ref.scrollTop(this.props.scrollTop); } } componentDidMount() { this.updateScroll(); + + // this logic is to make scrollbar visible when content is added body after mount + if (this.props.updateAfterMountMs) { + setTimeout(() => this.updateAfterMount(), this.props.updateAfterMountMs); + } + } + + updateAfterMount() { + if (this.ref && this.ref.current) { + const scrollbar = this.ref.current as any; + if (scrollbar.update) { + scrollbar.update(); + } + } } componentDidUpdate() { diff --git a/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss index f5adcedf0fe..07b6ae1b5b6 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss +++ b/packages/grafana-ui/src/components/CustomScrollbar/_CustomScrollbar.scss @@ -1,4 +1,4 @@ -.custom-scrollbars { +.custom-scrollbars { // Fix for Firefox. For some reason sometimes .view container gets a height of its content, but in order to // make scroll working it should fit outer container size (scroll appears only when inner container size is // greater than outer one). @@ -14,7 +14,7 @@ .track-vertical { border-radius: 3px; width: 6px !important; - right: 2px; + right: 0px; bottom: 2px; top: 2px; } diff --git a/packages/grafana-ui/src/utils/valueFormats/categories.ts b/packages/grafana-ui/src/utils/valueFormats/categories.ts index efba2cc5b79..bcd8741f851 100644 --- a/packages/grafana-ui/src/utils/valueFormats/categories.ts +++ b/packages/grafana-ui/src/utils/valueFormats/categories.ts @@ -125,7 +125,7 @@ export const getCategories = (): ValueFormatCategory[] => [ { name: 'Data (Metric)', formats: [ - { name: 'bits', id: 'decbits', fn: decimalSIPrefix('d') }, + { name: 'bits', id: 'decbits', fn: decimalSIPrefix('b') }, { name: 'bytes', id: 'decbytes', fn: decimalSIPrefix('B') }, { name: 'kilobytes', id: 'deckbytes', fn: decimalSIPrefix('B', 1) }, { name: 'megabytes', id: 'decmbytes', fn: decimalSIPrefix('B', 2) }, diff --git a/pkg/api/api.go b/pkg/api/api.go index 6da127fb550..81ea83eae61 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -33,17 +33,17 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/profile/", reqSignedIn, hs.Index) r.Get("/profile/password", reqSignedIn, hs.Index) r.Get("/profile/switch-org/:id", reqSignedIn, hs.ChangeActiveOrgAndRedirectToHome) - r.Get("/org/", reqSignedIn, hs.Index) - r.Get("/org/new", reqSignedIn, hs.Index) - r.Get("/datasources/", reqSignedIn, hs.Index) - r.Get("/datasources/new", reqSignedIn, hs.Index) - r.Get("/datasources/edit/*", reqSignedIn, hs.Index) - r.Get("/org/users", reqSignedIn, hs.Index) - r.Get("/org/users/new", reqSignedIn, hs.Index) - r.Get("/org/users/invite", reqSignedIn, hs.Index) - r.Get("/org/teams", reqSignedIn, hs.Index) - r.Get("/org/teams/*", reqSignedIn, hs.Index) - r.Get("/org/apikeys/", reqSignedIn, hs.Index) + r.Get("/org/", reqOrgAdmin, hs.Index) + r.Get("/org/new", reqGrafanaAdmin, hs.Index) + r.Get("/datasources/", reqOrgAdmin, hs.Index) + r.Get("/datasources/new", reqOrgAdmin, hs.Index) + r.Get("/datasources/edit/*", reqOrgAdmin, hs.Index) + r.Get("/org/users", reqOrgAdmin, hs.Index) + r.Get("/org/users/new", reqOrgAdmin, hs.Index) + r.Get("/org/users/invite", reqOrgAdmin, hs.Index) + r.Get("/org/teams", reqOrgAdmin, hs.Index) + r.Get("/org/teams/*", reqOrgAdmin, hs.Index) + r.Get("/org/apikeys/", reqOrgAdmin, hs.Index) r.Get("/dashboard/import/", reqSignedIn, hs.Index) r.Get("/configuration", reqGrafanaAdmin, hs.Index) r.Get("/admin", reqGrafanaAdmin, hs.Index) @@ -73,12 +73,12 @@ func (hs *HTTPServer) registerRoutes() { r.Get("/dashboards/", reqSignedIn, hs.Index) r.Get("/dashboards/*", reqSignedIn, hs.Index) - r.Get("/explore", reqEditorRole, hs.Index) + r.Get("/explore", reqSignedIn, middleware.EnsureEditorOrViewerCanEdit, hs.Index) r.Get("/playlists/", reqSignedIn, hs.Index) r.Get("/playlists/*", reqSignedIn, hs.Index) - r.Get("/alerting/", reqSignedIn, hs.Index) - r.Get("/alerting/*", reqSignedIn, hs.Index) + r.Get("/alerting/", reqEditorRole, hs.Index) + r.Get("/alerting/*", reqEditorRole, hs.Index) // sign up r.Get("/signup", hs.Index) diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index 5ca136c32c4..2757f245060 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -52,8 +52,10 @@ func populateDashboardsByTag(orgID int64, signedInUser *m.SignedInUser, dashboar for _, item := range searchQuery.Result { result = append(result, dtos.PlaylistDashboard{ Id: item.Id, + Slug: item.Slug, Title: item.Title, Uri: item.Uri, + Url: m.GetDashboardUrl(item.Uid, item.Slug), Order: dashboardTagOrder[tag], }) } diff --git a/pkg/api/user.go b/pkg/api/user.go index 6db9c6f6baf..9eb7e75eab0 100644 --- a/pkg/api/user.go +++ b/pkg/api/user.go @@ -121,7 +121,7 @@ func GetUserTeams(c *m.ReqContext) Response { return getUserTeamList(c.OrgId, c.ParamsInt64(":id")) } -func getUserTeamList(userID int64, orgID int64) Response { +func getUserTeamList(orgID int64, userID int64) Response { query := m.GetTeamsByUserQuery{OrgId: orgID, UserId: userID} if err := bus.Dispatch(&query); err != nil { diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index f663e6be895..cd026af2fc3 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -29,6 +29,8 @@ import ( // self registering services _ "github.com/grafana/grafana/pkg/extensions" _ "github.com/grafana/grafana/pkg/infra/serverlock" + + _ "github.com/grafana/grafana/pkg/infra/usagestats" _ "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/plugins" _ "github.com/grafana/grafana/pkg/services/alerting" diff --git a/pkg/log/log.go b/pkg/log/log.go index 2e3b6303a6e..eb739f855ea 100644 --- a/pkg/log/log.go +++ b/pkg/log/log.go @@ -25,6 +25,7 @@ var filters map[string]log15.Lvl func init() { loggersToClose = make([]DisposableHandler, 0) loggersToReload = make([]ReloadableHandler, 0) + filters = map[string]log15.Lvl{} Root = log15.Root() Root.SetHandler(log15.DiscardHandler()) } @@ -197,7 +198,7 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { // Log level. _, level := getLogLevelFromConfig("log."+mode, defaultLevelName, cfg) - filters := getFilters(util.SplitString(sec.Key("filters").String())) + modeFilters := getFilters(util.SplitString(sec.Key("filters").String())) format := getLogFormat(sec.Key("format").MustString("")) var handler log15.Handler @@ -230,12 +231,18 @@ func ReadLoggingConfig(modes []string, logsPath string, cfg *ini.File) { } for key, value := range defaultFilters { + if _, exist := modeFilters[key]; !exist { + modeFilters[key] = value + } + } + + for key, value := range modeFilters { if _, exist := filters[key]; !exist { filters[key] = value } } - handler = LogFilterHandler(level, filters, handler) + handler = LogFilterHandler(level, modeFilters, handler) handlers = append(handlers, handler) } diff --git a/pkg/login/ldap.go b/pkg/login/ldap.go index c15cb865bd3..8bb331b7e59 100644 --- a/pkg/login/ldap.go +++ b/pkg/login/ldap.go @@ -18,6 +18,7 @@ import ( type ILdapConn interface { Bind(username, password string) error + UnauthenticatedBind(username string) error Search(*ldap.SearchRequest) (*ldap.SearchResult, error) StartTLS(*tls.Config) error Close() @@ -259,7 +260,17 @@ func (a *ldapAuther) initialBind(username, userPassword string) error { bindPath = fmt.Sprintf(a.server.BindDN, username) } - if err := a.conn.Bind(bindPath, userPassword); err != nil { + bindFn := func() error { + return a.conn.Bind(bindPath, userPassword) + } + + if userPassword == "" { + bindFn = func() error { + return a.conn.UnauthenticatedBind(bindPath) + } + } + + if err := bindFn(); err != nil { a.log.Info("Initial bind failed", "error", err) if ldapErr, ok := err.(*ldap.Error); ok { diff --git a/pkg/login/ldap_test.go b/pkg/login/ldap_test.go index ef20feb1373..dabafee65a6 100644 --- a/pkg/login/ldap_test.go +++ b/pkg/login/ldap_test.go @@ -13,6 +13,70 @@ import ( ) func TestLdapAuther(t *testing.T) { + Convey("initialBind", t, func() { + Convey("Given bind dn and password configured", func() { + conn := &mockLdapConn{} + var actualUsername, actualPassword string + conn.bindProvider = func(username, password string) error { + actualUsername = username + actualPassword = password + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{ + BindDN: "cn=%s,o=users,dc=grafana,dc=org", + BindPassword: "bindpwd", + }, + } + err := ldapAuther.initialBind("user", "pwd") + So(err, ShouldBeNil) + So(ldapAuther.requireSecondBind, ShouldBeTrue) + So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org") + So(actualPassword, ShouldEqual, "bindpwd") + }) + + Convey("Given bind dn configured", func() { + conn := &mockLdapConn{} + var actualUsername, actualPassword string + conn.bindProvider = func(username, password string) error { + actualUsername = username + actualPassword = password + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{ + BindDN: "cn=%s,o=users,dc=grafana,dc=org", + }, + } + err := ldapAuther.initialBind("user", "pwd") + So(err, ShouldBeNil) + So(ldapAuther.requireSecondBind, ShouldBeFalse) + So(actualUsername, ShouldEqual, "cn=user,o=users,dc=grafana,dc=org") + So(actualPassword, ShouldEqual, "pwd") + }) + + Convey("Given empty bind dn and password", func() { + conn := &mockLdapConn{} + unauthenticatedBindWasCalled := false + var actualUsername string + conn.unauthenticatedBindProvider = func(username string) error { + unauthenticatedBindWasCalled = true + actualUsername = username + return nil + } + ldapAuther := &ldapAuther{ + conn: conn, + server: &LdapServerConf{}, + } + err := ldapAuther.initialBind("user", "pwd") + So(err, ShouldBeNil) + So(ldapAuther.requireSecondBind, ShouldBeTrue) + So(unauthenticatedBindWasCalled, ShouldBeTrue) + So(actualUsername, ShouldBeEmpty) + }) + }) Convey("When translating ldap user to grafana user", t, func() { @@ -365,12 +429,26 @@ func TestLdapAuther(t *testing.T) { } type mockLdapConn struct { - result *ldap.SearchResult - searchCalled bool - searchAttributes []string + result *ldap.SearchResult + searchCalled bool + searchAttributes []string + bindProvider func(username, password string) error + unauthenticatedBindProvider func(username string) error } func (c *mockLdapConn) Bind(username, password string) error { + if c.bindProvider != nil { + return c.bindProvider(username, password) + } + + return nil +} + +func (c *mockLdapConn) UnauthenticatedBind(username string) error { + if c.unauthenticatedBindProvider != nil { + return c.unauthenticatedBindProvider(username) + } + return nil } diff --git a/pkg/middleware/auth.go b/pkg/middleware/auth.go index 27248342c8d..e06409211eb 100644 --- a/pkg/middleware/auth.go +++ b/pkg/middleware/auth.go @@ -4,7 +4,7 @@ import ( "net/url" "strings" - "gopkg.in/macaron.v1" + macaron "gopkg.in/macaron.v1" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" @@ -52,6 +52,12 @@ func notAuthorized(c *m.ReqContext) { c.Redirect(setting.AppSubUrl + "/login") } +func EnsureEditorOrViewerCanEdit(c *m.ReqContext) { + if !c.SignedInUser.HasRole(m.ROLE_EDITOR) && !setting.ViewersCanEdit { + accessForbidden(c) + } +} + func RoleAuth(roles ...m.RoleType) macaron.Handler { return func(c *m.ReqContext) { ok := false diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 59d459f122e..0dc15badcb0 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -138,7 +138,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return err } - renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?panelId=%d", ref.Uid, ref.Slug, context.Rule.PanelId) + renderOpts.Path = fmt.Sprintf("d-solo/%s/%s?orgId=%d&panelId=%d", ref.Uid, ref.Slug, context.Rule.OrgId, context.Rule.PanelId) result, err := n.renderService.Render(context.Ctx, renderOpts) if err != nil { diff --git a/pkg/services/rendering/phantomjs.go b/pkg/services/rendering/phantomjs.go index 1bd7489c153..29c2f39fd77 100644 --- a/pkg/services/rendering/phantomjs.go +++ b/pkg/services/rendering/phantomjs.go @@ -36,7 +36,7 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( defer middleware.RemoveRenderAuthKey(renderKey) phantomDebugArg := "--debug=false" - if log.GetLogLevelFor("renderer") >= log.LvlDebug { + if log.GetLogLevelFor("rendering") >= log.LvlDebug { phantomDebugArg = "--debug=true" } @@ -64,13 +64,26 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( cmd := exec.CommandContext(commandCtx, binPath, cmdArgs...) cmd.Stderr = cmd.Stdout + timezone := "" + if opts.Timezone != "" { + timezone = isoTimeOffsetToPosixTz(opts.Timezone) baseEnviron := os.Environ() - cmd.Env = appendEnviron(baseEnviron, "TZ", isoTimeOffsetToPosixTz(opts.Timezone)) + cmd.Env = appendEnviron(baseEnviron, "TZ", timezone) } + rs.log.Debug("executing Phantomjs", "binPath", binPath, "cmdArgs", cmdArgs, "timezone", timezone) + out, err := cmd.Output() + if out != nil { + rs.log.Debug("Phantomjs output", "out", string(out)) + } + + if err != nil { + rs.log.Debug("Phantomjs error", "error", err) + } + // check for timeout first if commandCtx.Err() == context.DeadlineExceeded { rs.log.Info("Rendering timed out") @@ -82,8 +95,6 @@ func (rs *RenderingService) renderViaPhantomJS(ctx context.Context, opts Opts) ( return nil, err } - rs.log.Debug("Phantomjs output", "out", string(out)) - rs.log.Debug("Image rendered", "path", pngPath) return &RenderResult{FilePath: pngPath}, nil } diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index 2da09672f13..475cb4a3777 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -17,6 +17,7 @@ type Hit struct { Title string `json:"title"` Uri string `json:"uri"` Url string `json:"url"` + Slug string `json:"slug"` Type HitType `json:"type"` Tags []string `json:"tags"` IsStarred bool `json:"isStarred"` diff --git a/pkg/tsdb/stackdriver/stackdriver.go b/pkg/tsdb/stackdriver/stackdriver.go index 8317e2a748a..76b346553ad 100644 --- a/pkg/tsdb/stackdriver/stackdriver.go +++ b/pkg/tsdb/stackdriver/stackdriver.go @@ -336,6 +336,8 @@ func (e *StackdriverExecutor) unmarshalResponse(res *http.Response) (Stackdriver return StackdriverResponse{}, err } + // slog.Info("stackdriver", "response", string(body)) + if res.StatusCode/100 != 2 { slog.Error("Request failed", "status", res.Status, "body", string(body)) return StackdriverResponse{}, fmt.Errorf(string(body)) @@ -559,7 +561,7 @@ func calcBucketBound(bucketOptions StackdriverBucketOptions, n int) string { } else if bucketOptions.ExponentialBuckets != nil { bucketBound = strconv.FormatInt(int64(bucketOptions.ExponentialBuckets.Scale*math.Pow(bucketOptions.ExponentialBuckets.GrowthFactor, float64(n-1))), 10) } else if bucketOptions.ExplicitBuckets != nil { - bucketBound = strconv.FormatInt(bucketOptions.ExplicitBuckets.Bounds[(n-1)], 10) + bucketBound = fmt.Sprintf("%g", bucketOptions.ExplicitBuckets.Bounds[n]) } return bucketBound } diff --git a/pkg/tsdb/stackdriver/stackdriver_test.go b/pkg/tsdb/stackdriver/stackdriver_test.go index 4b16b6b5294..78c3086a94b 100644 --- a/pkg/tsdb/stackdriver/stackdriver_test.go +++ b/pkg/tsdb/stackdriver/stackdriver_test.go @@ -344,8 +344,8 @@ func TestStackdriver(t *testing.T) { }) }) - Convey("when data from query is distribution", func() { - data, err := loadTestFile("./test-data/3-series-response-distribution.json") + Convey("when data from query is distribution with exponential bounds", func() { + data, err := loadTestFile("./test-data/3-series-response-distribution-exponential.json") So(err, ShouldBeNil) So(len(data.TimeSeries), ShouldEqual, 1) @@ -370,6 +370,14 @@ func TestStackdriver(t *testing.T) { So(res.Series[0].Points[2][1].Float64, ShouldEqual, 1536669060000) }) + Convey("bucket bounds should be correct", func() { + So(res.Series[0].Name, ShouldEqual, "0") + So(res.Series[1].Name, ShouldEqual, "1") + So(res.Series[2].Name, ShouldEqual, "2") + So(res.Series[3].Name, ShouldEqual, "4") + So(res.Series[4].Name, ShouldEqual, "8") + }) + Convey("value should be correct", func() { So(res.Series[8].Points[0][0].Float64, ShouldEqual, 1) So(res.Series[9].Points[0][0].Float64, ShouldEqual, 1) @@ -383,6 +391,45 @@ func TestStackdriver(t *testing.T) { }) }) + Convey("when data from query is distribution with explicit bounds", func() { + data, err := loadTestFile("./test-data/4-series-response-distribution-explicit.json") + So(err, ShouldBeNil) + So(len(data.TimeSeries), ShouldEqual, 1) + + res := &tsdb.QueryResult{Meta: simplejson.New(), RefId: "A"} + query := &StackdriverQuery{AliasBy: "{{bucket}}"} + err = executor.parseResponse(res, data, query) + So(err, ShouldBeNil) + + So(len(res.Series), ShouldEqual, 33) + for i := 0; i < 33; i++ { + if i == 0 { + So(res.Series[i].Name, ShouldEqual, "0") + } + So(len(res.Series[i].Points), ShouldEqual, 2) + } + + Convey("timestamps should be in ascending order", func() { + So(res.Series[0].Points[0][1].Float64, ShouldEqual, 1550859086000) + So(res.Series[0].Points[1][1].Float64, ShouldEqual, 1550859146000) + }) + + Convey("bucket bounds should be correct", func() { + So(res.Series[0].Name, ShouldEqual, "0") + So(res.Series[1].Name, ShouldEqual, "0.01") + So(res.Series[2].Name, ShouldEqual, "0.05") + So(res.Series[3].Name, ShouldEqual, "0.1") + }) + + Convey("value should be correct", func() { + So(res.Series[8].Points[0][0].Float64, ShouldEqual, 381) + So(res.Series[9].Points[0][0].Float64, ShouldEqual, 212) + So(res.Series[10].Points[0][0].Float64, ShouldEqual, 56) + So(res.Series[8].Points[1][0].Float64, ShouldEqual, 375) + So(res.Series[9].Points[1][0].Float64, ShouldEqual, 213) + So(res.Series[10].Points[1][0].Float64, ShouldEqual, 56) + }) + }) }) Convey("when interpolating filter wildcards", func() { diff --git a/pkg/tsdb/stackdriver/test-data/3-series-response-distribution.json b/pkg/tsdb/stackdriver/test-data/3-series-response-distribution-exponential.json similarity index 100% rename from pkg/tsdb/stackdriver/test-data/3-series-response-distribution.json rename to pkg/tsdb/stackdriver/test-data/3-series-response-distribution-exponential.json diff --git a/pkg/tsdb/stackdriver/test-data/4-series-response-distribution-explicit.json b/pkg/tsdb/stackdriver/test-data/4-series-response-distribution-explicit.json new file mode 100644 index 00000000000..98435294762 --- /dev/null +++ b/pkg/tsdb/stackdriver/test-data/4-series-response-distribution-explicit.json @@ -0,0 +1,209 @@ +{ + "timeSeries": [ + { + "metric": { + "type": "custom.googleapis.com\/opencensus\/grpc.io\/client\/roundtrip_latency" + }, + "resource": { + "type": "global", + "labels": { + "project_id": "grafana-demo" + } + }, + "metricKind": "DELTA", + "valueType": "DISTRIBUTION", + "points": [ + { + "interval": { + "startTime": "2019-02-22T18:11:26Z", + "endTime": "2019-02-22T18:12:26Z" + }, + "value": { + "distributionValue": { + "count": "1878", + "mean": 17.813718392255, + "sumOfSquaredDeviation": 7141630.651914, + "bucketOptions": { + "explicitBuckets": { + "bounds": [ + 0, + 0.01, + 0.05, + 0.1, + 0.3, + 0.6, + 0.8, + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 10, + 13, + 16, + 20, + 25, + 30, + 40, + 50, + 65, + 80, + 100, + 130, + 160, + 200, + 250, + 300, + 400, + 500, + 650, + 800, + 1000, + 2000, + 5000, + 10000, + 20000, + 50000, + 100000 + ] + } + }, + "bucketCounts": [ + "0", + "0", + "0", + "0", + "8", + "403", + "297", + "184", + "375", + "213", + "56", + "31", + "15", + "13", + "4", + "1", + "5", + "2", + "8", + "13", + "26", + "13", + "45", + "48", + "61", + "10", + "3", + "6", + "7", + "4", + "7", + "12", + "8" + ] + } + } + }, + { + "interval": { + "startTime": "2019-02-22T18:10:26Z", + "endTime": "2019-02-22T18:11:26Z" + }, + "value": { + "distributionValue": { + "count": "1887", + "mean": 17.654277577766, + "sumOfSquaredDeviation": 7082587.2133073, + "bucketOptions": { + "explicitBuckets": { + "bounds": [ + 0, + 0.01, + 0.05, + 0.1, + 0.3, + 0.6, + 0.8, + 1, + 2, + 3, + 4, + 5, + 6, + 8, + 10, + 13, + 16, + 20, + 25, + 30, + 40, + 50, + 65, + 80, + 100, + 130, + 160, + 200, + 250, + 300, + 400, + 500, + 650, + 800, + 1000, + 2000, + 5000, + 10000, + 20000, + 50000, + 100000 + ] + } + }, + "bucketCounts": [ + "0", + "0", + "0", + "0", + "8", + "404", + "298", + "187", + "381", + "212", + "56", + "31", + "15", + "14", + "4", + "1", + "4", + "2", + "9", + "13", + "24", + "13", + "46", + "46", + "61", + "11", + "3", + "6", + "7", + "5", + "7", + "11", + "8" + ] + } + } + } + ] + } + ] +} diff --git a/pkg/tsdb/stackdriver/types.go b/pkg/tsdb/stackdriver/types.go index 3821ce7ceda..e4ede41d269 100644 --- a/pkg/tsdb/stackdriver/types.go +++ b/pkg/tsdb/stackdriver/types.go @@ -26,7 +26,7 @@ type StackdriverBucketOptions struct { Scale float64 `json:"scale"` } `json:"exponentialBuckets"` ExplicitBuckets *struct { - Bounds []int64 `json:"bounds"` + Bounds []float64 `json:"bounds"` } `json:"explicitBuckets"` } diff --git a/public/app/core/utils/errors.test.ts b/public/app/core/utils/errors.test.ts new file mode 100644 index 00000000000..a5783fe7204 --- /dev/null +++ b/public/app/core/utils/errors.test.ts @@ -0,0 +1,55 @@ +import { getMessageFromError } from 'app/core/utils/errors'; + +describe('errors functions', () => { + let message; + + describe('when getMessageFromError gets an error string', () => { + beforeEach(() => { + message = getMessageFromError('error string'); + }); + + it('should return the string', () => { + expect(message).toBe('error string'); + }); + }); + + describe('when getMessageFromError gets an error object with message field', () => { + beforeEach(() => { + message = getMessageFromError({ message: 'error string' }); + }); + + it('should return the message text', () => { + expect(message).toBe('error string'); + }); + }); + + describe('when getMessageFromError gets an error object with data.message field', () => { + beforeEach(() => { + message = getMessageFromError({ data: { message: 'error string' } }); + }); + + it('should return the message text', () => { + expect(message).toBe('error string'); + }); + }); + + describe('when getMessageFromError gets an error object with statusText field', () => { + beforeEach(() => { + message = getMessageFromError({ statusText: 'error string' }); + }); + + it('should return the statusText text', () => { + expect(message).toBe('error string'); + }); + }); + + describe('when getMessageFromError gets an error object', () => { + beforeEach(() => { + message = getMessageFromError({ customError: 'error string' }); + }); + + it('should return the stringified error', () => { + expect(message).toBe('{"customError":"error string"}'); + }); + }); +}); diff --git a/public/app/core/utils/errors.ts b/public/app/core/utils/errors.ts index 3f6f1cfbc8d..afdf5270ade 100644 --- a/public/app/core/utils/errors.ts +++ b/public/app/core/utils/errors.ts @@ -13,5 +13,5 @@ export function getMessageFromError(err: any): string | null { } } - return null; + return err; } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index c52774f7e0e..34483324fa9 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -268,7 +268,12 @@ export class DashboardPage extends PureComponent { onAddPanel={this.onAddPanel} />
- + {editview && } {initError && this.renderInitFailedState()} diff --git a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap index f60e60c43a8..83b5aaaa959 100644 --- a/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap +++ b/public/app/features/dashboard/containers/__snapshots__/DashboardPage.test.tsx.snap @@ -113,6 +113,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1` hideTracksWhenNotNeeded={false} scrollTop={0} setScrollTop={[Function]} + updateAfterMountMs={500} >
void; + name: string; + value: string; + onBlur: (event: ChangeEvent) => void; + onChange: (event: ChangeEvent) => void; tooltipInfo?: any; } -export const DataSourceOptions: FC = ({ label, placeholder, name, value, onChange, tooltipInfo }) => { +export const DataSourceOption: FC = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => { return (
{label} @@ -20,10 +21,10 @@ export const DataSourceOptions: FC = ({ label, placeholder, name, value, placeholder={placeholder} name={name} spellCheck={false} - onBlur={evt => onChange(evt.target.value)} + onBlur={onBlur} + onChange={onChange} + value={value} />
); }; - -export default DataSourceOptions; diff --git a/public/app/features/dashboard/panel_editor/EditorTabBody.tsx b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx index 0413cae8a7b..6d2ce838117 100644 --- a/public/app/features/dashboard/panel_editor/EditorTabBody.tsx +++ b/public/app/features/dashboard/panel_editor/EditorTabBody.tsx @@ -118,7 +118,7 @@ export class EditorTabBody extends PureComponent { {toolbarItems.map(item => this.renderButton(item))}
- +
{openView && this.renderOpenView(openView)} diff --git a/public/app/features/dashboard/panel_editor/QueryOptions.tsx b/public/app/features/dashboard/panel_editor/QueryOptions.tsx index d203f3bc25f..8c59edf456d 100644 --- a/public/app/features/dashboard/panel_editor/QueryOptions.tsx +++ b/public/app/features/dashboard/panel_editor/QueryOptions.tsx @@ -1,5 +1,5 @@ // Libraries -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent, FocusEvent } from 'react'; // Utils import { isValidTimeSpan } from 'app/core/utils/rangeutil'; @@ -9,7 +9,7 @@ import { Switch } from '@grafana/ui'; import { Input } from 'app/core/components/Form'; import { EventsWithValidation } from 'app/core/components/Form/Input'; import { InputStatus } from 'app/core/components/Form/Input'; -import DataSourceOption from './DataSourceOption'; +import { DataSourceOption } from './DataSourceOption'; import { FormLabel } from '@grafana/ui'; // Types @@ -43,32 +43,79 @@ interface Props { interface State { relativeTime: string; timeShift: string; + cacheTimeout: string; + maxDataPoints: string; + interval: string; + hideTimeOverride: boolean; } export class QueryOptions extends PureComponent { + allOptions = { + cacheTimeout: { + label: 'Cache timeout', + placeholder: '60', + name: 'cacheTimeout', + tooltipInfo: ( + <> + If your time series store has a query cache this option can override the default cache timeout. Specify a + numeric value in seconds. + + ), + }, + maxDataPoints: { + label: 'Max data points', + placeholder: 'auto', + name: 'maxDataPoints', + tooltipInfo: ( + <> + The maximum data points the query should return. For graphs this is automatically set to one data point per + pixel. + + ), + }, + minInterval: { + label: 'Min time interval', + placeholder: '0', + name: 'minInterval', + panelKey: 'interval', + tooltipInfo: ( + <> + A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '} + 1m if your data is written every minute. Access auto interval via variable{' '} + $__interval for time range string and $__interval_ms for numeric variable that can + be used in math expressions. + + ), + }, + }; + constructor(props) { super(props); this.state = { relativeTime: props.panel.timeFrom || '', timeShift: props.panel.timeShift || '', + cacheTimeout: props.panel.cacheTimeout || '', + maxDataPoints: props.panel.maxDataPoints || '', + interval: props.panel.interval || '', + hideTimeOverride: props.panel.hideTimeOverride || false, }; } - onRelativeTimeChange = event => { + onRelativeTimeChange = (event: ChangeEvent) => { this.setState({ relativeTime: event.target.value, }); }; - onTimeShiftChange = event => { + onTimeShiftChange = (event: ChangeEvent) => { this.setState({ timeShift: event.target.value, }); }; - onOverrideTime = (evt, status: InputStatus) => { - const { value } = evt.target; + onOverrideTime = (event: FocusEvent, status: InputStatus) => { + const { value } = event.target; const { panel } = this.props; const emptyToNullValue = emptyToNull(value); if (status === InputStatus.Valid && panel.timeFrom !== emptyToNullValue) { @@ -77,8 +124,8 @@ export class QueryOptions extends PureComponent { } }; - onTimeShift = (evt, status: InputStatus) => { - const { value } = evt.target; + onTimeShift = (event: FocusEvent, status: InputStatus) => { + const { value } = event.target; const { panel } = this.props; const emptyToNullValue = emptyToNull(value); if (status === InputStatus.Valid && panel.timeShift !== emptyToNullValue) { @@ -89,77 +136,49 @@ export class QueryOptions extends PureComponent { onToggleTimeOverride = () => { const { panel } = this.props; - panel.hideTimeOverride = !panel.hideTimeOverride; + this.setState({ hideTimeOverride: !this.state.hideTimeOverride }, () => { + panel.hideTimeOverride = this.state.hideTimeOverride; + panel.refresh(); + }); + }; + + onDataSourceOptionBlur = (panelKey: string) => () => { + const { panel } = this.props; + + panel[panelKey] = this.state[panelKey]; panel.refresh(); }; - renderOptions() { - const { datasource, panel } = this.props; + onDataSourceOptionChange = (panelKey: string) => (event: ChangeEvent) => { + this.setState({ ...this.state, [panelKey]: event.target.value }); + }; + + renderOptions = () => { + const { datasource } = this.props; const { queryOptions } = datasource.meta; if (!queryOptions) { return null; } - const onChangeFn = (panelKey: string) => { - return (value: string | number) => { - panel[panelKey] = value; - panel.refresh(); - }; - }; - - const allOptions = { - cacheTimeout: { - label: 'Cache timeout', - placeholder: '60', - name: 'cacheTimeout', - value: panel.cacheTimeout, - tooltipInfo: ( - <> - If your time series store has a query cache this option can override the default cache timeout. Specify a - numeric value in seconds. - - ), - }, - maxDataPoints: { - label: 'Max data points', - placeholder: 'auto', - name: 'maxDataPoints', - value: panel.maxDataPoints, - tooltipInfo: ( - <> - The maximum data points the query should return. For graphs this is automatically set to one data point per - pixel. - - ), - }, - minInterval: { - label: 'Min time interval', - placeholder: '0', - name: 'minInterval', - value: panel.interval, - panelKey: 'interval', - tooltipInfo: ( - <> - A lower limit for the auto group by time interval. Recommended to be set to write frequency, for example{' '} - 1m if your data is written every minute. Access auto interval via variable{' '} - $__interval for time range string and $__interval_ms for numeric variable that can - be used in math expressions. - - ), - }, - }; - return Object.keys(queryOptions).map(key => { - const options = allOptions[key]; - return ; + const options = this.allOptions[key]; + const panelKey = options.panelKey || key; + return ( + + ); }); - } + }; render() { - const hideTimeOverride = this.props.panel.hideTimeOverride; + const { hideTimeOverride } = this.state; const { relativeTime, timeShift } = this.state; - return (
{this.renderOptions()} diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 17af2dbd801..2a445c9a58c 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -887,8 +887,8 @@ export class DashboardModel { } // add back navbar height - if (kioskMode === KIOSK_MODE_TV) { - visibleHeight += 55; + if (kioskMode && kioskMode !== KIOSK_MODE_TV) { + visibleHeight += navbarHeight; } const visibleGridHeight = Math.floor(visibleHeight / (GRID_CELL_HEIGHT + GRID_CELL_VMARGIN)); diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index 01eea098ea4..27f60865c21 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -64,6 +64,14 @@ export class DataSourceSettingsPage extends PureComponent { await loadDataSource(pageId); } + componentDidUpdate(prevProps: Props) { + const { dataSource } = this.props; + + if (prevProps.dataSource !== dataSource) { + this.setState({ dataSource }); + } + } + onSubmit = async (evt: React.FormEvent) => { evt.preventDefault(); @@ -95,9 +103,7 @@ export class DataSourceSettingsPage extends PureComponent { }; onModelChange = (dataSource: DataSourceSettings) => { - this.setState({ - dataSource: dataSource, - }); + this.setState({ dataSource }); }; isReadOnly() { diff --git a/public/app/features/explore/QueryEditor.tsx b/public/app/features/explore/QueryEditor.tsx index 1d329f1c56e..d158f6bb9f3 100644 --- a/public/app/features/explore/QueryEditor.tsx +++ b/public/app/features/explore/QueryEditor.tsx @@ -43,6 +43,9 @@ export default class QueryEditor extends PureComponent { this.props.onQueryChange(target); this.props.onExecuteQuery(); }, + onQueryChange: () => { + this.props.onQueryChange(target); + }, events: exploreEvents, panel: { datasource, targets: [target] }, dashboard: {}, diff --git a/public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx b/public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx index 2402cc5da2c..5b14fa73a72 100644 --- a/public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx +++ b/public/app/plugins/datasource/stackdriver/components/Aggregations.test.tsx @@ -49,7 +49,8 @@ describe('Aggregations', () => { }); it('', () => { const options = wrapper.state().aggOptions[0].options; - expect(options.length).toEqual(5); + + expect(options.length).toEqual(10); expect(options.map(o => o.value)).toEqual(expect.arrayContaining(['REDUCE_NONE'])); }); }); diff --git a/public/app/plugins/datasource/stackdriver/components/Aggregations.tsx b/public/app/plugins/datasource/stackdriver/components/Aggregations.tsx index c616d55ba7a..9b8dca01151 100644 --- a/public/app/plugins/datasource/stackdriver/components/Aggregations.tsx +++ b/public/app/plugins/datasource/stackdriver/components/Aggregations.tsx @@ -73,7 +73,7 @@ export class Aggregations extends React.Component { value={crossSeriesReducer} variables={templateSrv.variables} options={aggOptions} - placeholder="Select Aggregation" + placeholder="Select Reducer" className="width-15" />
diff --git a/public/app/plugins/datasource/stackdriver/components/Metrics.tsx b/public/app/plugins/datasource/stackdriver/components/Metrics.tsx index 06094d0c1e9..a09b6108370 100644 --- a/public/app/plugins/datasource/stackdriver/components/Metrics.tsx +++ b/public/app/plugins/datasource/stackdriver/components/Metrics.tsx @@ -185,7 +185,7 @@ export class Metrics extends React.Component { }, ]} placeholder="Select Metric" - className="width-15" + className="width-26" />
diff --git a/public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap b/public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap index defd8ef01a4..ed9fe98bbf8 100644 --- a/public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap +++ b/public/app/plugins/datasource/stackdriver/components/__snapshots__/Aggregations.test.tsx.snap @@ -28,7 +28,7 @@ Array [
- Select Aggregation + Select Reducer
- Select Aggregation + Select Reducer
{ - const search = $location.search(); + appEvents.on('toggle-kiosk-mode', (options: { exit?: boolean }) => { + const search: { kiosk?: KioskUrlValue } = $location.search(); if (options && options.exit) { search.kiosk = '1'; @@ -197,7 +197,7 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } } - $location.search(search); + $timeout(() => $location.search(search)); setViewModeBodyClass(body, search.kiosk, sidemenuOpen); }); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 4c9c5fd5304..442fb5acb0c 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -81,6 +81,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { template: '', pageClass: 'dashboard-solo', routeInfo: DashboardRouteInfo.Normal, + reloadOnSearch: false, resolve: { component: () => SoloPanelPage, }, @@ -89,6 +90,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { template: '', pageClass: 'dashboard-solo', routeInfo: DashboardRouteInfo.Normal, + reloadOnSearch: false, resolve: { component: () => SoloPanelPage, },