Merge pull request #15824 from grafana/cp-6.0.1
Cherry picks for v6.0.1
This commit is contained in:
+1
-1
@@ -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"
|
||||
|
||||
@@ -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<Props> {
|
||||
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() {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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) },
|
||||
|
||||
+14
-14
@@ -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)
|
||||
|
||||
@@ -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],
|
||||
})
|
||||
}
|
||||
|
||||
+1
-1
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
|
||||
+9
-2
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
+12
-1
@@ -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 {
|
||||
|
||||
+81
-3
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
|
||||
@@ -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"}');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -13,5 +13,5 @@ export function getMessageFromError(err: any): string | null {
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
return err;
|
||||
}
|
||||
|
||||
@@ -268,7 +268,12 @@ export class DashboardPage extends PureComponent<Props, State> {
|
||||
onAddPanel={this.onAddPanel}
|
||||
/>
|
||||
<div className="scroll-canvas scroll-canvas--dashboard">
|
||||
<CustomScrollbar autoHeightMin={'100%'} setScrollTop={this.setScrollTop} scrollTop={scrollTop}>
|
||||
<CustomScrollbar
|
||||
autoHeightMin={'100%'}
|
||||
setScrollTop={this.setScrollTop}
|
||||
scrollTop={scrollTop}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
{editview && <DashboardSettings dashboard={dashboard} />}
|
||||
|
||||
{initError && this.renderInitFailedState()}
|
||||
|
||||
@@ -113,6 +113,7 @@ exports[`DashboardPage Dashboard init completed Should render dashboard grid 1`
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
<div
|
||||
className="dashboard-container"
|
||||
@@ -349,6 +350,7 @@ exports[`DashboardPage When dashboard has editview url state should render setti
|
||||
hideTracksWhenNotNeeded={false}
|
||||
scrollTop={0}
|
||||
setScrollTop={[Function]}
|
||||
updateAfterMountMs={500}
|
||||
>
|
||||
<DashboardSettings
|
||||
dashboard={
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import React, { FC } from 'react';
|
||||
import React, { FC, ChangeEvent } from 'react';
|
||||
import { FormLabel } from '@grafana/ui';
|
||||
|
||||
interface Props {
|
||||
label: string;
|
||||
placeholder?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
onChange?: (evt: any) => void;
|
||||
name: string;
|
||||
value: string;
|
||||
onBlur: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onChange: (event: ChangeEvent<HTMLInputElement>) => void;
|
||||
tooltipInfo?: any;
|
||||
}
|
||||
|
||||
export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value, onChange, tooltipInfo }) => {
|
||||
export const DataSourceOption: FC<Props> = ({ label, placeholder, name, value, onBlur, onChange, tooltipInfo }) => {
|
||||
return (
|
||||
<div className="gf-form gf-form--flex-end">
|
||||
<FormLabel tooltip={tooltipInfo}>{label}</FormLabel>
|
||||
@@ -20,10 +21,10 @@ export const DataSourceOptions: FC<Props> = ({ label, placeholder, name, value,
|
||||
placeholder={placeholder}
|
||||
name={name}
|
||||
spellCheck={false}
|
||||
onBlur={evt => onChange(evt.target.value)}
|
||||
onBlur={onBlur}
|
||||
onChange={onChange}
|
||||
value={value}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default DataSourceOptions;
|
||||
|
||||
@@ -118,7 +118,7 @@ export class EditorTabBody extends PureComponent<Props, State> {
|
||||
{toolbarItems.map(item => this.renderButton(item))}
|
||||
</div>
|
||||
<div className="panel-editor__scroll">
|
||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop}>
|
||||
<CustomScrollbar autoHide={false} scrollTop={scrollTop} setScrollTop={setScrollTop} updateAfterMountMs={300}>
|
||||
<div className="panel-editor__content">
|
||||
<FadeIn in={isOpen} duration={200} unmountOnExit={true}>
|
||||
{openView && this.renderOpenView(openView)}
|
||||
|
||||
@@ -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<Props, State> {
|
||||
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{' '}
|
||||
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
|
||||
<code>$__interval</code> for time range string and <code>$__interval_ms</code> 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<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
relativeTime: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onTimeShiftChange = event => {
|
||||
onTimeShiftChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
this.setState({
|
||||
timeShift: event.target.value,
|
||||
});
|
||||
};
|
||||
|
||||
onOverrideTime = (evt, status: InputStatus) => {
|
||||
const { value } = evt.target;
|
||||
onOverrideTime = (event: FocusEvent<HTMLInputElement>, 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<Props, State> {
|
||||
}
|
||||
};
|
||||
|
||||
onTimeShift = (evt, status: InputStatus) => {
|
||||
const { value } = evt.target;
|
||||
onTimeShift = (event: FocusEvent<HTMLInputElement>, 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<Props, State> {
|
||||
|
||||
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<HTMLInputElement>) => {
|
||||
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{' '}
|
||||
<code>1m</code> if your data is written every minute. Access auto interval via variable{' '}
|
||||
<code>$__interval</code> for time range string and <code>$__interval_ms</code> for numeric variable that can
|
||||
be used in math expressions.
|
||||
</>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
return Object.keys(queryOptions).map(key => {
|
||||
const options = allOptions[key];
|
||||
return <DataSourceOption key={key} {...options} onChange={onChangeFn(allOptions[key].panelKey || key)} />;
|
||||
const options = this.allOptions[key];
|
||||
const panelKey = options.panelKey || key;
|
||||
return (
|
||||
<DataSourceOption
|
||||
key={key}
|
||||
{...options}
|
||||
onChange={this.onDataSourceOptionChange(panelKey)}
|
||||
onBlur={this.onDataSourceOptionBlur(panelKey)}
|
||||
value={this.state[panelKey]}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const hideTimeOverride = this.props.panel.hideTimeOverride;
|
||||
const { hideTimeOverride } = this.state;
|
||||
const { relativeTime, timeShift } = this.state;
|
||||
|
||||
return (
|
||||
<div className="gf-form-inline">
|
||||
{this.renderOptions()}
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -64,6 +64,14 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
||||
await loadDataSource(pageId);
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
const { dataSource } = this.props;
|
||||
|
||||
if (prevProps.dataSource !== dataSource) {
|
||||
this.setState({ dataSource });
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit = async (evt: React.FormEvent<HTMLFormElement>) => {
|
||||
evt.preventDefault();
|
||||
|
||||
@@ -95,9 +103,7 @@ export class DataSourceSettingsPage extends PureComponent<Props, State> {
|
||||
};
|
||||
|
||||
onModelChange = (dataSource: DataSourceSettings) => {
|
||||
this.setState({
|
||||
dataSource: dataSource,
|
||||
});
|
||||
this.setState({ dataSource });
|
||||
};
|
||||
|
||||
isReadOnly() {
|
||||
|
||||
@@ -43,6 +43,9 @@ export default class QueryEditor extends PureComponent<QueryEditorProps, any> {
|
||||
this.props.onQueryChange(target);
|
||||
this.props.onExecuteQuery();
|
||||
},
|
||||
onQueryChange: () => {
|
||||
this.props.onQueryChange(target);
|
||||
},
|
||||
events: exploreEvents,
|
||||
panel: { datasource, targets: [target] },
|
||||
dashboard: {},
|
||||
|
||||
@@ -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']));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,7 @@ export class Aggregations extends React.Component<Props, State> {
|
||||
value={crossSeriesReducer}
|
||||
variables={templateSrv.variables}
|
||||
options={aggOptions}
|
||||
placeholder="Select Aggregation"
|
||||
placeholder="Select Reducer"
|
||||
className="width-15"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -185,7 +185,7 @@ export class Metrics extends React.Component<Props, State> {
|
||||
},
|
||||
]}
|
||||
placeholder="Select Metric"
|
||||
className="width-15"
|
||||
className="width-26"
|
||||
/>
|
||||
</div>
|
||||
<div className="gf-form gf-form--grow">
|
||||
|
||||
+1
-1
@@ -28,7 +28,7 @@ Array [
|
||||
<div
|
||||
className="css-0 gf-form-select-box__placeholder"
|
||||
>
|
||||
Select Aggregation
|
||||
Select Reducer
|
||||
</div>
|
||||
<div
|
||||
className="css-0"
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ Array [
|
||||
Metric
|
||||
</span>
|
||||
<div
|
||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-15"
|
||||
className="css-0 gf-form-input gf-form-input--form-dropdown width-26"
|
||||
onKeyDown={[Function]}
|
||||
>
|
||||
<div
|
||||
@@ -196,7 +196,7 @@ Array [
|
||||
<div
|
||||
className="css-0 gf-form-select-box__placeholder"
|
||||
>
|
||||
Select Aggregation
|
||||
Select Reducer
|
||||
</div>
|
||||
<div
|
||||
className="css-0"
|
||||
|
||||
@@ -189,7 +189,7 @@ export const aggOptions = [
|
||||
ValueTypes.BOOL,
|
||||
ValueTypes.STRING,
|
||||
],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE],
|
||||
},
|
||||
{
|
||||
text: 'count true',
|
||||
@@ -207,25 +207,25 @@ export const aggOptions = [
|
||||
text: '99th percentile',
|
||||
value: 'REDUCE_PERCENTILE_99',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE],
|
||||
},
|
||||
{
|
||||
text: '95th percentile',
|
||||
value: 'REDUCE_PERCENTILE_95',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE],
|
||||
},
|
||||
{
|
||||
text: '50th percentile',
|
||||
value: 'REDUCE_PERCENTILE_50',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE],
|
||||
},
|
||||
{
|
||||
text: '5th percentile',
|
||||
value: 'REDUCE_PERCENTILE_05',
|
||||
valueTypes: [ValueTypes.INT64, ValueTypes.DOUBLE, ValueTypes.MONEY, ValueTypes.DISTRIBUTION],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA],
|
||||
metricKinds: [MetricKind.GAUGE, MetricKind.DELTA, MetricKind.CUMULATIVE],
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@
|
||||
label="Stack"
|
||||
label-class="width-7"
|
||||
checked="ctrl.panel.stack"
|
||||
on-change="ctrl.refresh()"
|
||||
on-change="ctrl.render()"
|
||||
>
|
||||
</gf-form-switch>
|
||||
<gf-form-switch
|
||||
|
||||
@@ -75,7 +75,7 @@ export class GrafanaCtrl {
|
||||
}
|
||||
}
|
||||
|
||||
function setViewModeBodyClass(body, mode: KioskUrlValue, sidemenuOpen: boolean) {
|
||||
function setViewModeBodyClass(body: JQuery, mode: KioskUrlValue, sidemenuOpen: boolean) {
|
||||
body.removeClass('view-mode--tv');
|
||||
body.removeClass('view-mode--kiosk');
|
||||
body.removeClass('view-mode--inactive');
|
||||
@@ -174,8 +174,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop
|
||||
});
|
||||
|
||||
// handle kiosk mode
|
||||
appEvents.on('toggle-kiosk-mode', options => {
|
||||
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);
|
||||
});
|
||||
|
||||
|
||||
@@ -81,6 +81,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
template: '<react-container />',
|
||||
pageClass: 'dashboard-solo',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => SoloPanelPage,
|
||||
},
|
||||
@@ -89,6 +90,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) {
|
||||
template: '<react-container />',
|
||||
pageClass: 'dashboard-solo',
|
||||
routeInfo: DashboardRouteInfo.Normal,
|
||||
reloadOnSearch: false,
|
||||
resolve: {
|
||||
component: () => SoloPanelPage,
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user